Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

fastify-helmet

Package Overview
Dependencies
Maintainers
17
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fastify-helmet - npm Package Compare versions

Comparing version 7.0.0 to 7.0.1

168

index.js

@@ -7,3 +7,3 @@ 'use strict'

function helmetPlugin (fastify, options, next) {
async function helmetPlugin (fastify, options) {
// helmet will throw when any option is explicitly set to "true"

@@ -15,61 +15,70 @@ // using ECMAScript destructuring is a clean workaround as we do not need to alter options

// We initialize the `helmet` reply decorator
fastify.decorateReply('helmet', null)
// We initialize the `helmet` reply decorator only if it does not already exists
if (!fastify.hasReplyDecorator('helmet')) {
fastify.decorateReply('helmet', null)
}
// We will add the onRequest helmet middleware functions through the onRoute hook if needed
// 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 (typeof routeOptions.helmet !== 'undefined') {
if (typeof routeOptions.helmet === 'object') {
const { enableCSPNonces: enableRouteCSPNonces, ...helmetRouteConfiguration } = routeOptions.helmet
// If route helmet options are set they overwrite the global helmet configuration
const mergedHelmetConfiguration = Object.assign({}, globalConfiguration, helmetRouteConfiguration)
buildRouteHooks(mergedHelmetConfiguration, routeOptions)
if (enableRouteCSPNonces) {
routeOptions.onRequest.push(buildCSPNonce(fastify, mergedHelmetConfiguration))
}
routeOptions.config = Object.assign(routeOptions.config || Object.create(null), { helmet: routeOptions.helmet })
} 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)
routeOptions.config = Object.assign(routeOptions.config || Object.create(null), { helmet: { skipRoute: 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)
}
})
if (enableCSPNonces) {
routeOptions.onRequest.push(buildCSPNonce(fastify, globalConfiguration))
}
fastify.addHook('onRequest', async (request, reply) => {
const { helmet: routeOptions } = request.context.config
if (typeof 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)
} 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)
// We decorate the reply with a fallback to the global helmet options
return replyDecorators(request, reply, globalConfiguration, enableCSPNonces)
}
})
next()
}
fastify.addHook('onRequest', (request, reply, next) => {
const { helmet: routeOptions } = request.context.config
function buildCSPNonce (fastify, configuration) {
const cspDirectives = configuration.contentSecurityPolicy
? configuration.contentSecurityPolicy.directives
: helmet.contentSecurityPolicy.getDefaultDirectives()
const cspReportOnly = configuration.contentSecurityPolicy
? configuration.contentSecurityPolicy.reportOnly
: undefined
if (typeof routeOptions !== 'undefined') {
const { enableCSPNonces: enableRouteCSPNonces, skipRoute, ...helmetRouteConfiguration } = routeOptions
return function (request, reply, next) {
if (!fastify.hasReplyDecorator('cspNonce')) {
fastify.decorateReply('cspNonce', null)
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()
} 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.
return buildHelmetOnRoutes(request, reply, globalConfiguration, enableCSPNonces)
} else {
// if the plugin is not global we can skip the route
}
// prevent object reference: https://github.com/fastify/fastify-helmet/issues/118
const directives = { ...cspDirectives }
return next()
})
}
// create csp nonce
async function replyDecorators (request, reply, configuration, enableCSP) {
if (enableCSP) {
reply.cspNonce = {

@@ -79,53 +88,52 @@ script: 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}'`)
reply.helmet = function (opts) {
const helmetConfiguration = opts
? Object.assign(Object.create(null), configuration, opts)
: configuration
const cspMiddleware = helmet.contentSecurityPolicy({ directives, reportOnly: cspReportOnly })
cspMiddleware(request.raw, reply.raw, next)
return helmet(helmetConfiguration)(request.raw, reply.raw, done)
}
}
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]
}
async function buildHelmetOnRoutes (request, reply, configuration, enableCSP) {
if (enableCSP === true) {
const cspDirectives = configuration.contentSecurityPolicy
? configuration.contentSecurityPolicy.directives
: helmet.contentSecurityPolicy.getDefaultDirectives()
const cspReportOnly = configuration.contentSecurityPolicy
? configuration.contentSecurityPolicy.reportOnly
: undefined
const middleware = helmet(configuration)
// We get the csp nonce from the reply
const { script: scriptCSPNonce, style: styleCSPNonce } = reply.cspNonce
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)
// We prevent object reference: https://github.com/fastify/fastify-helmet/issues/118
const directives = { ...cspDirectives }
next()
}
// 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}'`)
if (decorateOnly) {
return
}
const contentSecurityPolicy = { directives, reportOnly: cspReportOnly }
const mergedHelmetConfiguration = Object.assign(Object.create(null), configuration, { contentSecurityPolicy })
// 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)
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(helmetPlugin, {

@@ -132,0 +140,0 @@ fastify: '3.x',

{
"name": "fastify-helmet",
"version": "7.0.0",
"version": "7.0.1",
"description": "Important security headers for Fastify",

@@ -5,0 +5,0 @@ "main": "index.js",

'use strict'
const stream = require('stream')
const { test } = require('tap')
const fp = require('fastify-plugin')
const Fastify = require('fastify')

@@ -378,6 +380,3 @@ const helmet = require('..')

path: '/three',
method: 'GET',
headers: {
'accept-encoding': 'deflate'
}
method: 'GET'
}).then((response) => {

@@ -423,2 +422,42 @@ t.equal(response.statusCode, 200)

test('It should not throw when trying to add the `helmet` and `cspNonce` reply decorators if they already exist', async (t) => {
t.plan(7)
const fastify = Fastify()
// We decorate the reply with helmet and cspNonce to trigger the existence check
fastify.decorateReply('helmet', null)
fastify.decorateReply('cspNonce', null)
await fastify.register(helmet, { enableCSPNonces: true, global: true })
fastify.get('/', async (request, reply) => {
t.ok(reply.helmet)
t.not(reply.helmet, null)
t.ok(reply.cspNonce)
t.not(reply.cspNonce, null)
reply.send(reply.cspNonce)
})
const response = await fastify.inject({
method: 'GET',
path: '/'
})
const cspCache = response.json()
t.ok(cspCache.script)
t.ok(cspCache.style)
const expected = {
'x-dns-prefetch-control': 'off',
'x-frame-options': 'SAMEORIGIN',
'x-download-options': 'noopen',
'x-content-type-options': 'nosniff',
'x-xss-protection': '0'
}
t.has(response.headers, expected)
})
test('It should be able to pass custom options to the `helmet` reply decorator', async (t) => {

@@ -459,3 +498,3 @@ t.plan(4)

test('It should be able to conditionally apply the middlewares through the `helmet` reply decorator', async (t) => {
t.plan(8)
t.plan(10)

@@ -489,19 +528,247 @@ const fastify = Fastify()

}
let response
response = await fastify.inject({
{
const response = await fastify.inject({
method: 'GET',
path: '/no-frameguard'
})
t.equal(response.statusCode, 200)
t.notMatch(response.headers, maybeExpected)
t.has(response.headers, expected)
}
const response = await fastify.inject({
method: 'GET',
path: '/no-frameguard'
path: '/frameguard'
})
t.notMatch(response.headers, maybeExpected)
t.equal(response.statusCode, 200)
t.has(response.headers, maybeExpected)
t.has(response.headers, expected)
})
response = await fastify.inject({
test('It should apply helmet headers when returning error messages', async (t) => {
t.plan(6)
const fastify = Fastify()
await fastify.register(helmet, { enableCSPNonces: true })
fastify.get('/', {
onRequest: async (request, reply) => {
reply.code(401)
reply.send({ message: 'Unauthorized' })
}
}, async (request, reply) => {
return { message: 'ok' }
})
fastify.get('/error-handler', {
}, async (request, reply) => {
return Promise.reject(new Error('error handler triggered'))
})
const expected = {
'x-dns-prefetch-control': 'off',
'x-frame-options': 'SAMEORIGIN',
'x-download-options': 'noopen',
'x-content-type-options': 'nosniff',
'x-xss-protection': '0'
}
{
const response = await fastify.inject({
method: 'GET',
path: '/'
})
t.equal(response.statusCode, 401)
t.has(response.headers, expected)
}
{
const response = await fastify.inject({
method: 'GET',
path: '/error-handler'
})
t.equal(response.statusCode, 500)
t.has(response.headers, expected)
}
{
const response = await fastify.inject({
method: 'GET',
path: '/404-route'
})
t.equal(response.statusCode, 404)
t.has(response.headers, expected)
}
})
// To avoid regressions.
// ref.: https://github.com/fastify/fastify-helmet/pull/169#issuecomment-1017413835
test('It should not return a fastify `FST_ERR_REP_ALREADY_SENT - Reply already sent` error', async (t) => {
t.plan(5)
const logs = []
const destination = new stream.Writable({
write: function (chunk, encoding, next) {
logs.push(JSON.parse(chunk))
next()
}
})
const fastify = Fastify({ logger: { level: 'info', stream: destination } })
await fastify.register(helmet)
await fastify.register(fp(async (instance, options) => {
instance.addHook('onRequest', async (request, reply) => {
const unauthorized = new Error('Unauthorized')
const errorResponse = (err) => {
return { error: err.message }
}
// We want to crash in the scope of this test
const crash = request.context.config.fail
Promise.resolve(crash).then((fail) => {
if (fail === true) {
reply.code(401)
reply.send(errorResponse(unauthorized))
return reply
}
}).catch(() => undefined)
})
}, {
name: 'regression-plugin-test'
}))
fastify.get('/fail', {
config: { fail: true }
}, async (request, reply) => {
return { message: 'unreachable' }
})
const expected = {
'x-dns-prefetch-control': 'off',
'x-frame-options': 'SAMEORIGIN',
'x-download-options': 'noopen',
'x-content-type-options': 'nosniff',
'x-xss-protection': '0'
}
const response = await fastify.inject({
method: 'GET',
path: '/frameguard'
path: '/fail'
})
t.has(response.headers, maybeExpected)
const failure = logs.find((entry) => entry.err && entry.err.statusCode === 500)
if (failure) {
t.not(failure.err.message, 'Reply was already sent.')
t.not(failure.err.name, 'FastifyError')
t.not(failure.err.code, 'FST_ERR_REP_ALREADY_SENT')
t.not(failure.err.statusCode, 500)
t.not(failure.msg, 'Reply already sent')
}
t.equal(failure, undefined)
t.equal(response.statusCode, 401)
t.has(response.headers, expected)
t.equal(JSON.parse(response.payload).error, 'Unauthorized')
t.not(JSON.parse(response.payload).message, 'unreachable')
})
test('It should forward `helmet` errors to `fastify-helmet`', async (t) => {
t.plan(3)
const fastify = Fastify()
await fastify.register(helmet, {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'", () => 'bad;value']
}
}
})
fastify.get('/', async (request, reply) => {
return { message: 'ok' }
})
const notExpected = {
'x-dns-prefetch-control': 'off',
'x-frame-options': 'SAMEORIGIN',
'x-download-options': 'noopen',
'x-content-type-options': 'nosniff',
'x-xss-protection': '0'
}
const response = await fastify.inject({
method: 'GET',
path: '/'
})
t.equal(response.statusCode, 500)
t.equal(
JSON.parse(response.payload).message,
'Content-Security-Policy received an invalid directive value for "default-src"'
)
t.notMatch(response.headers, notExpected)
})
test('It should be able to catch `helmet` errors with a fastify `onError` hook', async (t) => {
t.plan(7)
const errorDetected = []
const fastify = Fastify()
await fastify.register(helmet, {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'", () => 'bad;value']
}
}
})
fastify.addHook('onError', async (request, reply, error) => {
if (error) {
t.ok(error)
errorDetected.push(error)
}
})
fastify.get('/', async (request, reply) => {
return { message: 'ok' }
})
const notExpected = {
'x-dns-prefetch-control': 'off',
'x-frame-options': 'SAMEORIGIN',
'x-download-options': 'noopen',
'x-content-type-options': 'nosniff',
'x-xss-protection': '0'
}
t.equal(errorDetected.length, 0)
const response = await fastify.inject({
method: 'GET',
path: '/'
})
t.equal(response.statusCode, 500)
t.equal(
JSON.parse(response.payload).message,
'Content-Security-Policy received an invalid directive value for "default-src"'
)
t.notMatch(response.headers, notExpected)
t.equal(errorDetected.length, 1)
t.equal(
errorDetected[0].message,
'Content-Security-Policy received an invalid directive value for "default-src"'
)
})

@@ -197,3 +197,3 @@ 'use strict'

test('It should be able to conditionally apply the middlewares through the `helmet` reply decorator', async (t) => {
t.plan(8)
t.plan(10)

@@ -227,13 +227,15 @@ const fastify = Fastify()

}
let response
response = await fastify.inject({
method: 'GET',
path: '/no-frameguard'
})
{
const response = await fastify.inject({
method: 'GET',
path: '/no-frameguard'
})
t.notMatch(response.headers, maybeExpected)
t.has(response.headers, expected)
t.equal(response.statusCode, 200)
t.notMatch(response.headers, maybeExpected)
t.has(response.headers, expected)
}
response = await fastify.inject({
const response = await fastify.inject({
method: 'GET',

@@ -243,2 +245,3 @@ path: '/frameguard'

t.equal(response.statusCode, 200)
t.has(response.headers, maybeExpected)

@@ -248,3 +251,3 @@ t.has(response.headers, expected)

test('It should throw an error when route specific helmet options are of an invalid type', (t) => {
test('It should throw an error when route specific helmet options are of an invalid type', async (t) => {
t.plan(2)

@@ -259,11 +262,73 @@

fastify.inject({
method: 'GET',
path: '/'
}).catch((err) => {
if (err) {
t.ok(err)
t.equal(err.message, 'Unknown value for route helmet configuration')
try {
await fastify.ready()
} catch (error) {
t.ok(error)
t.equal(error.message, 'Unknown value for route helmet configuration')
}
})
test('It should forward `helmet` reply decorator and route specific errors to `fastify-helmet`', async (t) => {
t.plan(6)
const fastify = Fastify()
await fastify.register(helmet, { global: false })
fastify.get('/helmet-reply-decorator-error', async (request, reply) => {
await reply.helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'", () => 'bad;value']
}
}
})
return { message: 'ok' }
})
fastify.get('/helmet-route-configuration-error', {
helmet: {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'", () => 'bad;value']
}
}
}
}, async (request, reply) => {
return { message: 'ok' }
})
const notExpected = {
'x-dns-prefetch-control': 'off',
'x-frame-options': 'SAMEORIGIN',
'x-download-options': 'noopen',
'x-content-type-options': 'nosniff',
'x-xss-protection': '0'
}
{
const response = await fastify.inject({
method: 'GET',
path: '/helmet-reply-decorator-error'
})
t.equal(response.statusCode, 500)
t.equal(
JSON.parse(response.payload).message,
'Content-Security-Policy received an invalid directive value for "default-src"'
)
t.notMatch(response.headers, notExpected)
}
const response = await fastify.inject({
method: 'GET',
path: '/helmet-route-configuration-error'
})
t.equal(response.statusCode, 500)
t.equal(
JSON.parse(response.payload).message,
'Content-Security-Policy received an invalid directive value for "default-src"'
)
t.notMatch(response.headers, notExpected)
})
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc