-
Notifications
You must be signed in to change notification settings - Fork 46
/
index.js
148 lines (120 loc) · 6 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
'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) {
/* c8 ignore next */
const { helmet: routeOptions } = request.routeOptions?.config || request.routeConfig
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) {
/* c8 ignore next */
const { helmet: routeOptions } = request.routeOptions?.config || request.routeConfig
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)
}
}
// Helmet forward a typeof Error object so we just need to throw it as is.
function done (error) {
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