fastify-helmet
Advanced tools
Comparing version 6.0.0 to 7.0.0
@@ -1,19 +0,33 @@ | ||
import { FastifyPluginCallback } from "fastify"; | ||
import helmet from "helmet"; | ||
import { FastifyPluginCallback, RawServerBase, RawServerDefault } from 'fastify'; | ||
import helmet, { contentSecurityPolicy, HelmetOptions } from 'helmet'; | ||
declare module 'fastify' { | ||
export interface RouteShorthandOptions< | ||
RawServer extends RawServerBase = RawServerDefault | ||
> extends FastifyHelmetRouteOptions {} | ||
interface FastifyReply { | ||
cspNonce: { | ||
script: string | ||
style: string | ||
} | ||
script: string; | ||
style: string; | ||
}, | ||
helmet: (opts?: HelmetOptions) => typeof helmet | ||
} | ||
export interface RouteOptions extends FastifyHelmetRouteOptions {} | ||
} | ||
export type FastifyHelmetOptions = NonNullable<Parameters<typeof helmet>[0] & { enableCSPNonces?: boolean }>; | ||
export interface FastifyHelmetRouteOptions { | ||
helmet?: Omit<FastifyHelmetOptions, 'global'> | false; | ||
} | ||
export interface FastifyHelmetOptions extends NonNullable<HelmetOptions> { | ||
enableCSPNonces?: boolean, | ||
global?: boolean; | ||
} | ||
export const fastifyHelmet: FastifyPluginCallback<FastifyHelmetOptions> & { | ||
contentSecurityPolicy: typeof helmet.contentSecurityPolicy; | ||
contentSecurityPolicy: typeof contentSecurityPolicy; | ||
}; | ||
export default fastifyHelmet; |
148
index.js
'use strict' | ||
const { randomBytes } = require('crypto') | ||
const fp = require('fastify-plugin') | ||
const helmet = require('helmet') | ||
const crypto = require('crypto') | ||
module.exports = fp(function (app, options, next) { | ||
const enableCSPNonces = options.enableCSPNonces | ||
// clear options as helmet will throw when any options is "true" | ||
options.enableCSPNonces = undefined | ||
function helmetPlugin (fastify, options, next) { | ||
// 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 middleware = helmet(options) | ||
const isGlobal = typeof global === 'boolean' ? global : true | ||
app.addHook('onRequest', function (req, reply, next) { | ||
middleware(req.raw, reply.raw, next) | ||
}) | ||
// We initialize the `helmet` reply decorator | ||
fastify.decorateReply('helmet', null) | ||
if (enableCSPNonces) { | ||
// outside onRequest hooks so that it can be reused in every route | ||
const cspDirectives = options.contentSecurityPolicy ? options.contentSecurityPolicy.directives : helmet.contentSecurityPolicy.getDefaultDirectives() | ||
const cspReportOnly = options.contentSecurityPolicy ? options.contentSecurityPolicy.reportOnly : undefined | ||
// We will add the onRequest helmet middleware functions through the onRoute hook if needed | ||
fastify.addHook('onRoute', (routeOptions) => { | ||
if (typeof routeOptions.helmet !== 'undefined') { | ||
if (typeof routeOptions.helmet === 'object') { | ||
const { enableCSPNonces: enableRouteCSPNonces, ...helmetRouteConfiguration } = routeOptions.helmet | ||
app.decorateReply('cspNonce', null) | ||
app.addHook('onRequest', function (req, reply, next) { | ||
// prevent object reference #118 | ||
const directives = { ...cspDirectives } | ||
// If route helmet options are set they overwrite the global helmet configuration | ||
const mergedHelmetConfiguration = Object.assign({}, globalConfiguration, helmetRouteConfiguration) | ||
// create csp nonce | ||
reply.cspNonce = { | ||
script: crypto.randomBytes(16).toString('hex'), | ||
style: crypto.randomBytes(16).toString('hex') | ||
buildRouteHooks(mergedHelmetConfiguration, routeOptions) | ||
if (enableRouteCSPNonces) { | ||
routeOptions.onRequest.push(buildCSPNonce(fastify, mergedHelmetConfiguration)) | ||
} | ||
} else if (routeOptions.helmet === false) { | ||
// don't apply any helmet settings but decorate the reply with a fallback to the | ||
// global helmet options | ||
buildRouteHooks(globalConfiguration, routeOptions, true) | ||
} else { | ||
throw new Error('Unknown value for route helmet configuration') | ||
} | ||
} else 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. | ||
buildRouteHooks(globalConfiguration, routeOptions) | ||
// push nonce to csp | ||
// 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-${reply.cspNonce.script}'`) | ||
// 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-${reply.cspNonce.style}'`) | ||
if (enableCSPNonces) { | ||
routeOptions.onRequest.push(buildCSPNonce(fastify, globalConfiguration)) | ||
} | ||
} else { | ||
// if no options are specified and the plugin is not global, then we still want to decorate | ||
// the reply in this case | ||
buildRouteHooks(globalConfiguration, routeOptions, true) | ||
} | ||
}) | ||
const cspMiddleware = helmet.contentSecurityPolicy({ directives, reportOnly: cspReportOnly }) | ||
cspMiddleware(req.raw, reply.raw, next) | ||
}) | ||
next() | ||
} | ||
function buildCSPNonce (fastify, configuration) { | ||
const cspDirectives = configuration.contentSecurityPolicy | ||
? configuration.contentSecurityPolicy.directives | ||
: helmet.contentSecurityPolicy.getDefaultDirectives() | ||
const cspReportOnly = configuration.contentSecurityPolicy | ||
? configuration.contentSecurityPolicy.reportOnly | ||
: undefined | ||
return function (request, reply, next) { | ||
if (!fastify.hasReplyDecorator('cspNonce')) { | ||
fastify.decorateReply('cspNonce', null) | ||
} | ||
// prevent object reference: https://github.com/fastify/fastify-helmet/issues/118 | ||
const directives = { ...cspDirectives } | ||
// create csp nonce | ||
reply.cspNonce = { | ||
script: randomBytes(16).toString('hex'), | ||
style: randomBytes(16).toString('hex') | ||
} | ||
// push nonce to csp | ||
// 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-${reply.cspNonce.script}'`) | ||
// 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-${reply.cspNonce.style}'`) | ||
const cspMiddleware = helmet.contentSecurityPolicy({ directives, reportOnly: cspReportOnly }) | ||
cspMiddleware(request.raw, reply.raw, next) | ||
} | ||
} | ||
next() | ||
}, { | ||
function buildRouteHooks (configuration, routeOptions, decorateOnly) { | ||
if (Array.isArray(routeOptions.onRequest)) { | ||
routeOptions.onRequest.push(addHelmetReplyDecorator) | ||
} else if (typeof routeOptions.onRequest === 'function') { | ||
routeOptions.onRequest = [routeOptions.onRequest, addHelmetReplyDecorator] | ||
} else { | ||
routeOptions.onRequest = [addHelmetReplyDecorator] | ||
} | ||
const middleware = helmet(configuration) | ||
function addHelmetReplyDecorator (request, reply, next) { | ||
// We decorate `reply.helmet` with all helmet middleware functions | ||
// NB: we allow users to pass a custom helmet options object with a fallback | ||
// to global helmet configuration. | ||
reply.helmet = (opts) => opts | ||
? helmet(opts)(request.raw, reply.raw) | ||
: helmet(configuration)(request.raw, reply.raw) | ||
next() | ||
} | ||
if (decorateOnly) { | ||
return | ||
} | ||
// At this point `routeOptions.onRequest` is an array | ||
// we just have to push our `onRequest` function | ||
routeOptions.onRequest.push(onRequest) | ||
function onRequest (request, reply, next) { | ||
middleware(request.raw, reply.raw, next) | ||
} | ||
} | ||
module.exports = fp(helmetPlugin, { | ||
fastify: '3.x', | ||
@@ -52,0 +130,0 @@ name: 'fastify-helmet' |
{ | ||
"name": "fastify-helmet", | ||
"version": "6.0.0", | ||
"version": "7.0.0", | ||
"description": "Important security headers for Fastify", | ||
"main": "index.js", | ||
"types": "index.d.ts", | ||
"scripts": { | ||
"lint": "standard", | ||
"lint:fix": "standard --fix", | ||
"test": "standard | snazzy && tap test.js && npm run typescript", | ||
"test:ci": "standard | snazzy && tap test.js --coverage-report=lcovonly && npm run typescript", | ||
"coverage": "npm run unit -- --coverage-report=lcovonly", | ||
"lint": "standard | snazzy", | ||
"lint:fix": "standard --fix | snazzy", | ||
"test": "npm run lint && npm run unit && npm run typescript", | ||
"test:ci": "npm run lint && npm run coverage && npm run typescript", | ||
"unit": "tap -J \"test/*.test.js\"", | ||
"unit:report": "npm run unit -- --coverage-report=html", | ||
"unit:verbose": "npm run unit -- -Rspec", | ||
"typescript": "tsd" | ||
@@ -34,10 +39,10 @@ }, | ||
"devDependencies": { | ||
"@types/node": "^17.0.0", | ||
"fastify": "^3.0.0", | ||
"@types/node": "^17.0.8", | ||
"fastify": "^3.25.3", | ||
"pre-commit": "^1.2.2", | ||
"snazzy": "^9.0.0", | ||
"standard": "^16.0.2", | ||
"tap": "^15.0.0", | ||
"tsd": "^0.19.0", | ||
"typescript": "^4.0.2" | ||
"standard": "^16.0.4", | ||
"tap": "^15.1.6", | ||
"tsd": "^0.19.1", | ||
"typescript": "^4.5.4" | ||
}, | ||
@@ -47,3 +52,6 @@ "dependencies": { | ||
"helmet": "^5.0.1" | ||
}, | ||
"tsd": { | ||
"directory": "test/types" | ||
} | ||
} |
144
README.md
@@ -36,11 +36,137 @@ # fastify-helmet | ||
## How it works | ||
`fastify-helmet` is a tiny wrapper around helmet that adds an `'onRequest'` hook | ||
and a `reply.helmet` decorator. | ||
It accepts the same options as helmet, and you can see more in [the helmet documentation](https://helmetjs.github.io/docs/). | ||
### Apply Helmet to all your application routes | ||
By passing `{ global: true }` into the options, `fastify-helmet` allows you to register Helmet for all your application | ||
routes by default. If you want a more granular control on how to apply Helmet to your application you can choose to | ||
disable it on a global scope by passing `{ global: false }` to the options. By default, this option is set to `true`. | ||
#### Example - enable `fastify-helmet` globally | ||
```js | ||
fastify.register(helmet) | ||
// or | ||
fastify.register(helmet, { global: true }) | ||
``` | ||
#### Example - disable `fastify-helmet` globally | ||
```js | ||
// register the package with the `{ global: false }` option | ||
fastify.register(helmet, { global: false }) | ||
fastify.get('/route-with-disabled-helmet', async (request, reply) => { | ||
return { message: 'helmet is not enabled here' } | ||
}) | ||
fastify.get('/route-with-enabled-helmet', { | ||
// We enable and configure helmet for this route only | ||
helmet: { | ||
dnsPrefetchControl: { | ||
allow: true | ||
}, | ||
expectCt: { | ||
maxAge: 1, | ||
enforce: true, | ||
reportUri: 'foo' | ||
}, | ||
frameguard: { | ||
action: 'foo' | ||
}, | ||
referrerPolicy: false | ||
} | ||
}, async (request, reply) => { | ||
return { message: 'helmet is enabled here' } | ||
}) | ||
// helmet is disabled on this route but we have access to `reply.helmet` decorator | ||
// that allows us to apply helmet conditionally | ||
fastify.get('/here-we-use-helmet-reply-decorator', async (request, reply) => { | ||
if (condition) { | ||
// we apply the default options | ||
await reply.helmet() | ||
} else { | ||
// we apply customized options | ||
await reply.helmet({ frameguard: false }) | ||
} | ||
return { | ||
message: 'we use the helmet reply decorator to conditionally apply helmet middlewares' | ||
} | ||
}) | ||
``` | ||
### `helmet` route option | ||
`fastify-helmet` allows you to enable, disable, and customize helmet for each one of your application hooks by using the | ||
`helmet` shorthand route option when you register your application routes. | ||
If you want to disable helmet for a specific endpoint you must pass `{ helmet: false }` to your route options. | ||
If you want to enable or customize helmet for a specific endpoint you must pass a helmet configuration object to your | ||
route options. E.g.: `{ helmet: { frameguard: false } }`. | ||
#### Example - `fastify-helmet` configuration using the `helmet` shorthand route option | ||
```js | ||
// register the package with the `{ global: true }` option | ||
fastify.register(helmet, { global: true }) | ||
fastify.get('/route-with-disabled-helmet', { helmet: false }, async (request, reply) => { | ||
return { message: 'helmet is not enabled here' } | ||
}) | ||
fastify.get('/route-with-enabled-helmet', async (request, reply) => { | ||
return { message: 'helmet is enabled by default here' } | ||
}) | ||
fastify.get('/route-with-custom-helmet-configuration', { | ||
// We change the helmet configuration for this route only | ||
helmet: { | ||
enableCSPNonces: true, | ||
contentSecurityPolicy: { | ||
directives: { | ||
'directive-1': ['foo', 'bar'] | ||
}, | ||
reportOnly: true | ||
}, | ||
dnsPrefetchControl: { | ||
allow: true | ||
}, | ||
expectCt: { | ||
maxAge: 1, | ||
enforce: true, | ||
reportUri: 'foo' | ||
}, | ||
frameguard: { | ||
action: 'foo' | ||
}, | ||
hsts: { | ||
maxAge: 1, | ||
includeSubDomains: true, | ||
preload: true | ||
}, | ||
permittedCrossDomainPolicies: { | ||
permittedPolicies: 'foo' | ||
}, | ||
referrerPolicy: false | ||
} | ||
}, async (request, reply) => { | ||
return { message: 'helmet is enabled with a custom configuration on this route' } | ||
}) | ||
``` | ||
### Content-Security-Policy Nonce | ||
`fastify-helmet` provide a simple way for `csp nonces generation`. You can enable | ||
this behavior by passing `{ enableCSPNonces: true }` into the options. Then, you can | ||
retrieve the `nonces` through `reply.cspNonce`. | ||
`fastify-helmet` provide a simple way for `csp nonces generation`. You can enable this behavior by passing | ||
`{ enableCSPNonces: true }` into the options. Then, you can retrieve the `nonces` through `reply.cspNonce`. | ||
Note: This feature is implemented inside this module. It is not a valid option or | ||
supported by helmet. If you need to use helmet feature only for csp nonce you | ||
can follow the example [here](#example---generate-by-helmet). | ||
> Note: This feature is implemented inside this module. It is not a valid option or supported by helmet. | ||
> If you need to use helmet feature only for csp nonce you can follow the example [here](#example---generate-by-helmet). | ||
@@ -111,10 +237,4 @@ #### Example - Generate by options | ||
## How it works | ||
`fastify-helmet` is just a tiny wrapper around helmet that adds an `'onRequest'` hook. | ||
It accepts the same options of Helmet, and you can see more in [the helmet documentation](https://helmetjs.github.io/docs/). | ||
## License | ||
MIT |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
45589
16
1013
239
1