🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@fastify/middie

Package Overview
Dependencies
Maintainers
18
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fastify/middie - npm Package Compare versions

Comparing version
9.3.1
to
9.3.2
+19
.github/workflows/lock-threads.yml
name: Lock Threads
on:
schedule:
- cron: '0 0 1 * *'
workflow_dispatch:
concurrency:
group: lock
permissions:
contents: read
jobs:
lock-threads:
permissions:
issues: write
pull-requests: write
uses: fastify/workflows/.github/workflows/lock-threads.yml@v6
'use strict'
const { test } = require('node:test')
const Fastify = require('fastify')
const middiePlugin = require('../index')
const API_KEY = 'mock-api-key-123'
function guardMiddie (req, res, next) {
if (req.headers['x-api-key'] !== API_KEY) {
res.statusCode = 401
res.setHeader('content-type', 'application/json; charset=utf-8')
res.end(JSON.stringify({ error: 'Unauthorized', where: 'middie /secret guard' }))
return
}
next()
}
async function buildGuardedApp (fastifyOptions) {
const app = Fastify(fastifyOptions)
await app.register(middiePlugin)
app.use('/secret', guardMiddie)
app.get('/secret', async () => ({ ok: true, app: 'guarded' }))
await app.ready()
return app
}
async function buildPlainApp (fastifyOptions) {
const app = Fastify(fastifyOptions)
app.get('/secret', async () => ({ ok: true, app: 'plain' }))
await app.ready()
return app
}
test('security: deprecated top-level ignoreDuplicateSlashes cannot bypass middie use(/secret)', async (t) => {
const options = { ignoreDuplicateSlashes: true }
const guarded = await buildGuardedApp(options)
const plain = await buildPlainApp(options)
t.after(() => guarded.close())
t.after(() => plain.close())
const control = await plain.inject({ method: 'GET', url: '//secret' })
const secured = await guarded.inject({ method: 'GET', url: '//secret' })
t.assert.strictEqual(control.statusCode, 200, 'fastify route matches //secret with top-level option')
t.assert.strictEqual(secured.statusCode, 401, 'middie guard must also match //secret and block')
})
test('security: deprecated top-level useSemicolonDelimiter cannot bypass middie use(/secret)', async (t) => {
const options = { useSemicolonDelimiter: true }
const guarded = await buildGuardedApp(options)
const plain = await buildPlainApp(options)
t.after(() => guarded.close())
t.after(() => plain.close())
const control = await plain.inject({ method: 'GET', url: '/secret;foo=bar' })
const secured = await guarded.inject({ method: 'GET', url: '/secret;foo=bar' })
t.assert.strictEqual(control.statusCode, 200, 'fastify route matches semicolon variant with top-level option')
t.assert.strictEqual(secured.statusCode, 401, 'middie guard must also match semicolon variant and block')
})
test('security: combined deprecated top-level options cannot bypass middie use(/secret)', async (t) => {
const options = {
ignoreDuplicateSlashes: true,
useSemicolonDelimiter: true,
ignoreTrailingSlash: true
}
const guarded = await buildGuardedApp(options)
const plain = await buildPlainApp(options)
t.after(() => guarded.close())
t.after(() => plain.close())
const control = await plain.inject({ method: 'GET', url: '//secret;foo=bar/' })
const secured = await guarded.inject({ method: 'GET', url: '//secret;foo=bar/' })
t.assert.strictEqual(control.statusCode, 200, 'fastify route matches crafted variant with deprecated top-level options')
t.assert.strictEqual(secured.statusCode, 401, 'middie guard must also match crafted variant and block')
})
+1
-1

@@ -30,5 +30,5 @@ name: CI

pull-requests: write
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v6
with:
license-check: true
lint: true
+21
-14

@@ -27,2 +27,13 @@ 'use strict'

function resolveNormalizationOptions (instance) {
const initialConfig = instance.initialConfig || {}
const routerOptions = initialConfig.routerOptions || {}
return {
ignoreDuplicateSlashes: routerOptions.ignoreDuplicateSlashes ?? initialConfig.ignoreDuplicateSlashes,
useSemicolonDelimiter: routerOptions.useSemicolonDelimiter ?? initialConfig.useSemicolonDelimiter,
ignoreTrailingSlash: routerOptions.ignoreTrailingSlash ?? initialConfig.ignoreTrailingSlash
}
}
function fastifyMiddie (fastify, options, next) {

@@ -32,9 +43,4 @@ fastify.decorate('use', use)

fastify[kMiddieHasMiddlewares] = false
const routerOptions = fastify.initialConfig?.routerOptions || {}
fastify[kMiddie] = Middie(onMiddieEnd, {
ignoreDuplicateSlashes: routerOptions.ignoreDuplicateSlashes,
useSemicolonDelimiter: routerOptions.useSemicolonDelimiter,
ignoreTrailingSlash: routerOptions.ignoreTrailingSlash
})
fastify[kMiddie] = Middie(onMiddieEnd, resolveNormalizationOptions(fastify))

@@ -98,12 +104,13 @@ const hook = options.hook || 'onRequest'

instance[kMiddlewares] = []
const instanceRouterOptions = (instance.initialConfig && instance.initialConfig.routerOptions) || {}
instance[kMiddie] = Middie(onMiddieEnd, {
ignoreDuplicateSlashes: instanceRouterOptions.ignoreDuplicateSlashes,
useSemicolonDelimiter: instanceRouterOptions.useSemicolonDelimiter,
ignoreTrailingSlash: instanceRouterOptions.ignoreTrailingSlash
})
instance[kMiddie] = Middie(onMiddieEnd, resolveNormalizationOptions(instance))
instance[kMiddieHasMiddlewares] = false
instance.decorate('use', use)
for (const middleware of middlewares) {
instance.use(...middleware)
for (const [path, fn] of middlewares) {
instance[kMiddlewares].push([path, fn])
if (fn == null) {
instance[kMiddie].use(path)
} else {
instance[kMiddie].use(path, fn)
}
instance[kMiddieHasMiddlewares] = true
}

@@ -110,0 +117,0 @@ }

{
"name": "@fastify/middie",
"version": "9.3.1",
"version": "9.3.2",
"description": "Middleware engine for Fastify",

@@ -68,3 +68,3 @@ "main": "index.js",

"helmet": "^8.0.0",
"neostandard": "^0.12.0",
"neostandard": "^0.13.0",
"serve-static": "^2.2.0",

@@ -71,0 +71,0 @@ "tsd": "^0.33.0"

@@ -746,1 +746,24 @@ 'use strict'

})
test('fastifyMiddie supports instances without initialConfig', (t) => {
const { fastifyMiddie } = require('../index')
const hooks = []
const fakeFastify = {
decorate (name, value) {
this[name] = value
return this
},
addHook (name, fn) {
hooks.push([name, fn])
return this
}
}
fastifyMiddie(fakeFastify, {}, (err) => {
t.assert.ifError(err)
})
t.assert.strictEqual(typeof fakeFastify.use, 'function')
t.assert.ok(hooks.some(([name]) => name === 'onRequest'))
t.assert.ok(hooks.some(([name]) => name === 'onRegister'))
})

@@ -461,1 +461,319 @@ 'use strict'

})
test('should not double-prefix inherited middleware paths in child scopes', async t => {
t.plan(4)
const instance = fastify()
t.after(() => instance.close())
await instance.register(middiePlugin)
instance.use('/admin', function (req, res, next) {
if (req.headers.authorization == null) {
res.statusCode = 403
res.end('forbidden')
return
}
next()
})
instance.get('/admin/root-data', async function () {
return { data: 'root-secret' }
})
await instance.register(async function (child) {
child.get('/secret', async function (req) {
return { data: 'child-secret' }
})
}, { prefix: '/admin' })
const address = await instance.listen({ port: 0 })
const rootNoAuth = await fetch(address + '/admin/root-data')
t.assert.deepStrictEqual(rootNoAuth.status, 403)
const childNoAuth = await fetch(address + '/admin/secret')
t.assert.deepStrictEqual(childNoAuth.status, 403)
const childWithAuth = await fetch(address + '/admin/secret', {
headers: {
authorization: 'Bearer test'
}
})
t.assert.deepStrictEqual(childWithAuth.status, 200)
t.assert.deepStrictEqual(await childWithAuth.json(), { data: 'child-secret' })
})
test('should allow child scopes register middleware with same prefix', async t => {
t.plan(7)
const instance = fastify()
t.after(() => instance.close())
await instance.register(middiePlugin)
const count = { admin: 0, child: 0 }
instance.use('/admin', function (req, res, next) {
count.admin++
next()
})
instance.get('/admin/root-data', async function () {
return { data: 'admin' }
})
await instance.register(async function (child) {
child.use('/admin', function (req, res, next) {
count.child++
next()
})
child.get('/secret', async function (req) {
return { data: 'child' }
})
child.get('/admin', async function (req) {
return { data: 'child-admin' }
})
}, { prefix: '/admin' })
const address = await instance.listen({ port: 0 })
const root = await fetch(address + '/admin/root-data')
t.assert.deepStrictEqual(root.status, 200)
t.assert.deepStrictEqual(await root.json(), { data: 'admin' })
const child = await fetch(address + '/admin/secret')
t.assert.deepStrictEqual(child.status, 200)
t.assert.deepStrictEqual(await child.json(), { data: 'child' })
const childAdmin = await fetch(address + '/admin/admin')
t.assert.deepStrictEqual(childAdmin.status, 200)
t.assert.deepStrictEqual(await childAdmin.json(), { data: 'child-admin' })
t.assert.deepStrictEqual(count, { admin: 3, child: 1 })
})
test('should enforce inherited middleware in nested grandchild scopes', async t => {
t.plan(6)
const instance = fastify()
t.after(() => instance.close())
await instance.register(middiePlugin)
instance.use('/admin', function (req, res, next) {
if (req.headers.authorization == null) {
res.statusCode = 403
res.end('forbidden')
return
}
next()
})
instance.get('/admin/root-data', async function () {
return { data: 'root-secret' }
})
await instance.register(async function (parent) {
parent.get('/info', async function () {
return { data: 'parent-info' }
})
await parent.register(async function (grandchild) {
grandchild.get('/deep', async function () {
return { data: 'nested-secret' }
})
}, { prefix: '/sub' })
}, { prefix: '/admin' })
const address = await instance.listen({ port: 0 })
const rootNoAuth = await fetch(address + '/admin/root-data')
t.assert.deepStrictEqual(rootNoAuth.status, 403)
const parentNoAuth = await fetch(address + '/admin/info')
t.assert.deepStrictEqual(parentNoAuth.status, 403)
const grandchildNoAuth = await fetch(address + '/admin/sub/deep')
t.assert.deepStrictEqual(grandchildNoAuth.status, 403)
const grandchildWithAuth = await fetch(address + '/admin/sub/deep', {
headers: { authorization: 'Bearer test' }
})
t.assert.deepStrictEqual(grandchildWithAuth.status, 200)
t.assert.deepStrictEqual(await grandchildWithAuth.json(), { data: 'nested-secret' })
const parentWithAuth = await fetch(address + '/admin/info', {
headers: { authorization: 'Bearer test' }
})
t.assert.deepStrictEqual(parentWithAuth.status, 200)
})
test('should enforce inherited middleware across three nesting levels', async t => {
t.plan(3)
const instance = fastify()
t.after(() => instance.close())
await instance.register(middiePlugin)
instance.use('/api', function (req, res, next) {
if (req.headers.authorization == null) {
res.statusCode = 403
res.end('forbidden')
return
}
next()
})
await instance.register(async function (l1) {
await l1.register(async function (l2) {
await l2.register(async function (l3) {
l3.get('/resource', async function () {
return { data: 'deep-resource' }
})
}, { prefix: '/c' })
}, { prefix: '/b' })
}, { prefix: '/api/a' })
const address = await instance.listen({ port: 0 })
const noAuth = await fetch(address + '/api/a/b/c/resource')
t.assert.deepStrictEqual(noAuth.status, 403)
const withAuth = await fetch(address + '/api/a/b/c/resource', {
headers: { authorization: 'Bearer test' }
})
t.assert.deepStrictEqual(withAuth.status, 200)
t.assert.deepStrictEqual(await withAuth.json(), { data: 'deep-resource' })
})
test('should not apply middleware to unrelated nested prefixes', async t => {
t.plan(4)
const instance = fastify()
t.after(() => instance.close())
await instance.register(middiePlugin)
instance.use('/admin', function (req, res, next) {
if (req.headers.authorization == null) {
res.statusCode = 403
res.end('forbidden')
return
}
next()
})
await instance.register(async function (child) {
child.get('/data', async function () {
return { data: 'public' }
})
await child.register(async function (grandchild) {
grandchild.get('/info', async function () {
return { data: 'public-nested' }
})
}, { prefix: '/nested' })
}, { prefix: '/public' })
const address = await instance.listen({ port: 0 })
const publicData = await fetch(address + '/public/data')
t.assert.deepStrictEqual(publicData.status, 200)
t.assert.deepStrictEqual(await publicData.json(), { data: 'public' })
const publicNested = await fetch(address + '/public/nested/info')
t.assert.deepStrictEqual(publicNested.status, 200)
t.assert.deepStrictEqual(await publicNested.json(), { data: 'public-nested' })
})
test('should not apply middleware when prefix shares string prefix but not path segment', async t => {
t.plan(4)
const instance = fastify()
t.after(() => instance.close())
await instance.register(middiePlugin)
instance.use('/admin', function (req, res, next) {
if (req.headers.authorization == null) {
res.statusCode = 403
res.end('forbidden')
return
}
next()
})
await instance.register(async function (child) {
child.get('/settings', async function () {
return { data: 'panel-settings' }
})
}, { prefix: '/admin-panel' })
await instance.register(async function (child) {
child.get('/settings', async function () {
return { data: 'admin-settings' }
})
}, { prefix: '/admin/real' })
const address = await instance.listen({ port: 0 })
const panelNoAuth = await fetch(address + '/admin-panel/settings')
t.assert.deepStrictEqual(panelNoAuth.status, 200)
t.assert.deepStrictEqual(await panelNoAuth.json(), { data: 'panel-settings' })
const realNoAuth = await fetch(address + '/admin/real/settings')
t.assert.deepStrictEqual(realNoAuth.status, 403)
const realWithAuth = await fetch(address + '/admin/real/settings', {
headers: { authorization: 'Bearer test' }
})
t.assert.deepStrictEqual(realWithAuth.status, 200)
})
test('should enforce middleware with partial prefix overlap in nested scopes', async t => {
t.plan(3)
const instance = fastify()
t.after(() => instance.close())
await instance.register(middiePlugin)
instance.use('/admin', function (req, res, next) {
if (req.headers.authorization == null) {
res.statusCode = 403
res.end('forbidden')
return
}
next()
})
await instance.register(async function (child) {
await child.register(async function (grandchild) {
grandchild.get('/settings', async function () {
return { data: 'admin-settings' }
})
}, { prefix: '/panel' })
}, { prefix: '/admin' })
const address = await instance.listen({ port: 0 })
const noAuth = await fetch(address + '/admin/panel/settings')
t.assert.deepStrictEqual(noAuth.status, 403)
const withAuth = await fetch(address + '/admin/panel/settings', {
headers: { authorization: 'Bearer test' }
})
t.assert.deepStrictEqual(withAuth.status, 200)
t.assert.deepStrictEqual(await withAuth.json(), { data: 'admin-settings' })
})