Files
simple-mail-cleaner/backend/node_modules/@fastify/helmet/index.js
2026-01-22 15:49:12 +01:00

152 lines
6.2 KiB
JavaScript

'use strict'
const { randomBytes } = require('node:crypto')
const fp = require('fastify-plugin')
const helmet = require('helmet')
async function fastifyHelmet (fastify, options) {
// helmet will throw when any option is explicitly set to "true"
// using ECMAScript destructuring is a clean workaround as we do not need to alter options
const { enableCSPNonces, global, ...globalConfiguration } = options
const isGlobal = typeof global === 'boolean' ? global : true
// We initialize the `helmet` reply decorator only if it does not already exists
if (!fastify.hasReplyDecorator('helmet')) {
fastify.decorateReply('helmet', null)
}
// We initialize the `cspNonce` reply decorator only if it does not already exists
if (!fastify.hasReplyDecorator('cspNonce')) {
fastify.decorateReply('cspNonce', null)
}
fastify.addHook('onRoute', (routeOptions) => {
if (routeOptions.helmet !== undefined) {
if (typeof routeOptions.helmet === 'object') {
routeOptions.config = Object.assign(routeOptions.config || Object.create(null), { helmet: routeOptions.helmet })
} else if (routeOptions.helmet === false) {
routeOptions.config = Object.assign(routeOptions.config || Object.create(null), { helmet: { skipRoute: true } })
} else {
throw new Error('Unknown value for route helmet configuration')
}
}
})
fastify.addHook('onRequest', async function helmetConfigureReply (request, reply) {
const { helmet: routeOptions } = request.routeOptions?.config
if (routeOptions !== undefined) {
const { enableCSPNonces: enableRouteCSPNonces, skipRoute, ...helmetRouteConfiguration } = routeOptions
// If route helmet options are set they overwrite the global helmet configuration
const mergedHelmetConfiguration = Object.assign(Object.create(null), globalConfiguration, helmetRouteConfiguration)
// We decorate the reply with a fallback to the route scoped helmet options
return replyDecorators(request, reply, mergedHelmetConfiguration, enableRouteCSPNonces)
}
// We decorate the reply with a fallback to the global helmet options
return replyDecorators(request, reply, globalConfiguration, enableCSPNonces)
})
fastify.addHook('onRequest', function helmetApplyHeaders (request, reply, next) {
const { helmet: routeOptions } = request.routeOptions?.config
if (routeOptions !== undefined) {
const { enableCSPNonces: enableRouteCSPNonces, skipRoute, ...helmetRouteConfiguration } = routeOptions
if (skipRoute === true) {
// If helmet route option is set to `false` we skip the route
} else {
// If route helmet options are set they overwrite the global helmet configuration
const mergedHelmetConfiguration = Object.assign(Object.create(null), globalConfiguration, helmetRouteConfiguration)
return buildHelmetOnRoutes(request, reply, mergedHelmetConfiguration, enableRouteCSPNonces)
}
return next()
}
if (isGlobal) {
// if the plugin is set globally (meaning that all the routes will be decorated)
// As the endpoint, does not have a custom helmet configuration, use the global one.
return buildHelmetOnRoutes(request, reply, globalConfiguration, enableCSPNonces)
}
// if the plugin is not global we can skip the route
return next()
})
}
async function replyDecorators (request, reply, configuration, enableCSP) {
if (enableCSP) {
reply.cspNonce = {
script: randomBytes(16).toString('hex'),
style: randomBytes(16).toString('hex')
}
}
reply.helmet = function (opts) {
const helmetConfiguration = opts
? Object.assign(Object.create(null), configuration, opts)
: configuration
return helmet(helmetConfiguration)(request.raw, reply.raw, done)
}
}
async function buildHelmetOnRoutes (request, reply, configuration, enableCSP) {
if (enableCSP === true && configuration.contentSecurityPolicy !== false) {
const cspDirectives = configuration.contentSecurityPolicy
? configuration.contentSecurityPolicy.directives
: helmet.contentSecurityPolicy.getDefaultDirectives()
const cspReportOnly = configuration.contentSecurityPolicy
? configuration.contentSecurityPolicy.reportOnly
: undefined
const cspUseDefaults = configuration.contentSecurityPolicy
? configuration.contentSecurityPolicy.useDefaults
: undefined
// We get the csp nonce from the reply
const { script: scriptCSPNonce, style: styleCSPNonce } = reply.cspNonce
// We prevent object reference: https://github.com/fastify/fastify-helmet/issues/118
const directives = { ...cspDirectives }
// We push nonce to csp
// We allow both 'script-src' or 'scriptSrc' syntax
const scriptKey = Array.isArray(directives['script-src']) ? 'script-src' : 'scriptSrc'
directives[scriptKey] = Array.isArray(directives[scriptKey]) ? [...directives[scriptKey]] : []
directives[scriptKey].push(`'nonce-${scriptCSPNonce}'`)
// allow both style-src or styleSrc syntax
const styleKey = Array.isArray(directives['style-src']) ? 'style-src' : 'styleSrc'
directives[styleKey] = Array.isArray(directives[styleKey]) ? [...directives[styleKey]] : []
directives[styleKey].push(`'nonce-${styleCSPNonce}'`)
const contentSecurityPolicy = { directives, reportOnly: cspReportOnly, useDefaults: cspUseDefaults }
const mergedHelmetConfiguration = Object.assign(Object.create(null), configuration, { contentSecurityPolicy })
helmet(mergedHelmetConfiguration)(request.raw, reply.raw, done)
} else {
helmet(configuration)(request.raw, reply.raw, done)
}
}
function done (error) {
// Helmet used to forward an Error object, so we could just rethrow it.
// Since Helmet v8.1.0 (see PR https://github.com/helmetjs/helmet/pull/485 ),
// errors are thrown directly instead of being passed to a callback.
// We keep the argument for compatibility, as v8.1.0 still accepts it
// (see https://github.com/helmetjs/helmet/blob/v8.1.0/index.ts#L109 ).
/* c8 ignore next */
if (error) throw error
}
module.exports = fp(fastifyHelmet, {
fastify: '5.x',
name: '@fastify/helmet'
})
module.exports.default = fastifyHelmet
module.exports.fastifyHelmet = fastifyHelmet
module.exports.contentSecurityPolicy = helmet.contentSecurityPolicy