VYPR
Critical severity9.8NVD Advisory· Published Feb 27, 2026· Updated Apr 14, 2026

CVE-2026-2293

CVE-2026-2293

Description

A NestJS application using @nestjs/platform-fastify can allow bypass of authentication/authorization middleware when Fastify path-normalization options are enabled.

This issue affects nest.Js: 11.1.13.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@nestjs/platform-fastifynpm
< 11.1.1411.1.14

Affected products

1
  • cpe:2.3:a:nestjs:nest:11.1.13:*:*:*:*:node.js:*:*

Patches

1
fd8d073e0e04

Merge pull request #16384 from nestjs/fix/fastify-middleware-vulnerability

https://github.com/nestjs/nestKamil MysliwiecFeb 17, 2026via ghsa
3 files changed · +272 7
  • integration/hello-world/e2e/middleware-fastify.spec.ts+154 0 modified
    @@ -612,4 +612,158 @@ describe('Middleware (FastifyAdapter)', () => {
           await app.close();
         });
       });
    +
    +  describe('should respect fastify routing options', () => {
    +    const MIDDLEWARE_RETURN_VALUE = 'middleware_return';
    +
    +    @Controller()
    +    class TestController {
    +      @Get('abc/def')
    +      included() {
    +        return 'whatnot';
    +      }
    +    }
    +    @Module({
    +      imports: [AppModule],
    +      controllers: [TestController],
    +    })
    +    class TestModule {
    +      configure(consumer: MiddlewareConsumer) {
    +        consumer
    +          .apply((req, res, next) => res.end(MIDDLEWARE_RETURN_VALUE))
    +          .forRoutes({ path: 'abc/def', method: RequestMethod.GET });
    +      }
    +    }
    +
    +    describe('[ignoreTrailingSlash] attribute', () => {
    +      beforeEach(async () => {
    +        app = (
    +          await Test.createTestingModule({
    +            imports: [TestModule],
    +          }).compile()
    +        ).createNestApplication<NestFastifyApplication>(
    +          new FastifyAdapter({
    +            ignoreTrailingSlash: true,
    +            // routerOptions: {
    +            //   ignoreTrailingSlash: true,
    +            // },
    +          }),
    +        );
    +
    +        await app.init();
    +      });
    +
    +      it(`GET forRoutes(GET /abc/def/)`, () => {
    +        return app
    +          .inject({
    +            method: 'GET',
    +            url: '/abc/def/', // trailing slash
    +          })
    +          .then(({ payload }) =>
    +            expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
    +          );
    +      });
    +
    +      afterEach(async () => {
    +        await app.close();
    +      });
    +    });
    +
    +    describe('[ignoreDuplicateSlashes] attribute', () => {
    +      beforeEach(async () => {
    +        app = (
    +          await Test.createTestingModule({
    +            imports: [TestModule],
    +          }).compile()
    +        ).createNestApplication<NestFastifyApplication>(
    +          new FastifyAdapter({
    +            routerOptions: {
    +              ignoreDuplicateSlashes: true,
    +            },
    +          }),
    +        );
    +
    +        await app.init();
    +      });
    +
    +      it(`GET forRoutes(GET /abc//def)`, () => {
    +        return app
    +          .inject({
    +            method: 'GET',
    +            url: '/abc//def', // duplicate slashes
    +          })
    +          .then(({ payload }) =>
    +            expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
    +          );
    +      });
    +
    +      afterEach(async () => {
    +        await app.close();
    +      });
    +    });
    +
    +    describe('[caseSensitive] attribute', () => {
    +      beforeEach(async () => {
    +        app = (
    +          await Test.createTestingModule({
    +            imports: [TestModule],
    +          }).compile()
    +        ).createNestApplication<NestFastifyApplication>(
    +          new FastifyAdapter({
    +            routerOptions: {
    +              caseSensitive: true,
    +            },
    +          }),
    +        );
    +
    +        await app.init();
    +      });
    +
    +      it(`GET forRoutes(GET /ABC/DEF)`, () => {
    +        return app
    +          .inject({
    +            method: 'GET',
    +            url: '/ABC/DEF', // different case
    +          })
    +          .then(({ payload }) =>
    +            expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
    +          );
    +      });
    +
    +      afterEach(async () => {
    +        await app.close();
    +      });
    +    });
    +
    +    describe('[useSemicolonDelimiter] attribute', () => {
    +      beforeEach(async () => {
    +        app = (
    +          await Test.createTestingModule({
    +            imports: [TestModule],
    +          }).compile()
    +        ).createNestApplication<NestFastifyApplication>(
    +          new FastifyAdapter({
    +            routerOptions: { useSemicolonDelimiter: true } as any,
    +          }),
    +        );
    +
    +        await app.init();
    +      });
    +
    +      it(`GET forRoutes(GET /abc/def;foo=bar)`, () => {
    +        return app
    +          .inject({
    +            method: 'GET',
    +            url: '/abc/def;foo=bar', // semicolon delimiter
    +          })
    +          .then(({ payload }) =>
    +            expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
    +          );
    +      });
    +
    +      afterEach(async () => {
    +        await app.close();
    +      });
    +    });
    +  });
     });
    
  • packages/platform-fastify/adapters/fastify-adapter.ts+48 1 modified
    @@ -708,7 +708,8 @@ export class FastifyAdapter<
                   queryParamsIndex >= 0
                     ? req.originalUrl.slice(0, queryParamsIndex)
                     : req.originalUrl;
    -            pathname = safeDecodeURI(pathname).path;
    +
    +            pathname = this.sanitizeUrl(pathname);
     
                 if (!re.exec(pathname + '/') && normalizedPath) {
                   return next();
    @@ -867,4 +868,50 @@ export class FastifyAdapter<
         }
         return this.instance.route(routeToInject);
       }
    +
    +  private sanitizeUrl(url: string): string {
    +    const initialConfig = this.instance.initialConfig as FastifyServerOptions;
    +    const routerOptions =
    +      initialConfig.routerOptions as Partial<FastifyServerOptions>;
    +
    +    if (
    +      routerOptions.ignoreDuplicateSlashes ||
    +      initialConfig.ignoreDuplicateSlashes
    +    ) {
    +      url = this.removeDuplicateSlashes(url);
    +    }
    +
    +    if (
    +      routerOptions.ignoreTrailingSlash ||
    +      initialConfig.ignoreTrailingSlash
    +    ) {
    +      url = this.trimLastSlash(url);
    +    }
    +
    +    if (
    +      routerOptions.caseSensitive === false ||
    +      initialConfig.caseSensitive === false
    +    ) {
    +      url = url.toLowerCase();
    +    }
    +    return safeDecodeURI(
    +      url,
    +      routerOptions.useSemicolonDelimiter ||
    +        initialConfig.useSemicolonDelimiter,
    +    ).path;
    +  }
    +
    +  private removeDuplicateSlashes(path: string) {
    +    const REMOVE_DUPLICATE_SLASHES_REGEXP = /\/\/+/g;
    +    return path.indexOf('//') !== -1
    +      ? path.replace(REMOVE_DUPLICATE_SLASHES_REGEXP, '/')
    +      : path;
    +  }
    +
    +  private trimLastSlash(path: string) {
    +    if (path.length > 1 && path.charCodeAt(path.length - 1) === 47) {
    +      return path.slice(0, -1);
    +    }
    +    return path;
    +  }
     }
    
  • packages/platform-fastify/adapters/middie/fastify-middie.ts+70 6 modified
    @@ -5,6 +5,7 @@ import {
       FastifyPluginCallback,
       FastifyReply,
       FastifyRequest,
    +  FastifyServerOptions,
       HookHandlerDoneFunction,
     } from 'fastify';
     import fp from 'fastify-plugin';
    @@ -28,6 +29,17 @@ interface MiddlewareEntry<
       fn: MiddlewareFn<Req, Res, Ctx>;
     }
     
    +function bindLast<F extends (...args: any[]) => any>(
    +  fn: F,
    +  last: Last<Parameters<F>>,
    +): (...args: DropLast<Parameters<F>>) => ReturnType<F> {
    +  return (...args: any[]) => fn(...args, last);
    +}
    +
    +// Helper types
    +type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
    +type DropLast<T extends any[]> = T extends [...infer Rest, any] ? Rest : never;
    +
     /**
      * A clone of `@fastify/middie` engine https://github.com/fastify/middie
      * with an extra vulnerability fix. Path is now decoded before matching to
    @@ -37,13 +49,16 @@ function middie<
       Req extends { url: string; originalUrl?: string },
       Res extends { finished?: boolean; writableEnded?: boolean },
       Ctx = unknown,
    ->(complete: (err: unknown, req: Req, res: Res, ctx: Ctx) => void) {
    +>(
    +  complete: (err: unknown, req: Req, res: Res, ctx: Ctx) => void,
    +  initialConfig: FastifyServerOptions | null,
    +) {
       const middlewares: MiddlewareEntry<Req, Res, Ctx>[] = [];
       const pool = reusify(Holder as any);
     
       return {
         use,
    -    run,
    +    run: bindLast(run, initialConfig),
       };
     
       function use(
    @@ -79,7 +94,12 @@ function middie<
         return this;
       }
     
    -  function run(req: Req, res: Res, ctx: Ctx) {
    +  function run(
    +    req: Req,
    +    res: Res,
    +    ctx: Ctx,
    +    initialConfig: FastifyServerOptions | null,
    +  ) {
         if (!middlewares.length) {
           complete(null, req, res, ctx);
           return;
    @@ -92,6 +112,7 @@ function middie<
         holder.res = res;
         holder.url = sanitizeUrl(req.url);
         holder.context = ctx;
    +    holder.initialConfig = initialConfig;
         holder.done();
       }
     
    @@ -100,6 +121,7 @@ function middie<
         res: Res | null;
         url: string | null;
         context: Ctx | null;
    +    initialConfig: FastifyServerOptions | null;
         i: number;
         done: (err?: unknown) => void;
       }
    @@ -109,6 +131,7 @@ function middie<
         this.res = null;
         this.url = null;
         this.context = null;
    +    this.initialConfig = null;
         this.i = 0;
     
         const that = this;
    @@ -135,7 +158,33 @@ function middie<
     
             if (regexp) {
               // Decode URL before matching to avoid bypassing middleware
    -          const decodedUrl = safeDecodeURI(url).path;
    +          let sanitizedUrl = url;
    +          if (
    +            that.initialConfig!.ignoreDuplicateSlashes ||
    +            that.initialConfig!.routerOptions?.ignoreDuplicateSlashes
    +          ) {
    +            sanitizedUrl = removeDuplicateSlashes(sanitizedUrl);
    +          }
    +
    +          if (
    +            that.initialConfig!.ignoreTrailingSlash ||
    +            that.initialConfig!.routerOptions?.ignoreTrailingSlash
    +          ) {
    +            sanitizedUrl = trimLastSlash(sanitizedUrl);
    +          }
    +
    +          if (
    +            that.initialConfig!.caseSensitive === false ||
    +            that.initialConfig!.routerOptions?.caseSensitive === false
    +          ) {
    +            sanitizedUrl = sanitizedUrl.toLowerCase();
    +          }
    +
    +          const decodedUrl = safeDecodeURI(
    +            sanitizedUrl,
    +            (that.initialConfig?.routerOptions as any)?.useSemicolonDelimiter ||
    +              that.initialConfig?.useSemicolonDelimiter,
    +          ).path;
               const result = regexp.exec(decodedUrl);
               if (result) {
                 req.url = req.url.replace(result[0], '');
    @@ -154,12 +203,27 @@ function middie<
           that.req = null;
           that.res = null;
           that.context = null;
    +      that.initialConfig = null;
           that.i = 0;
           pool.release(that as any);
         }
       }
     }
     
    +function removeDuplicateSlashes(path: string) {
    +  const REMOVE_DUPLICATE_SLASHES_REGEXP = /\/\/+/g;
    +  return path.indexOf('//') !== -1
    +    ? path.replace(REMOVE_DUPLICATE_SLASHES_REGEXP, '/')
    +    : path;
    +}
    +
    +function trimLastSlash(path: string) {
    +  if (path.length > 1 && path.charCodeAt(path.length - 1) === 47) {
    +    return path.slice(0, -1);
    +  }
    +  return path;
    +}
    +
     function sanitizeUrl(url: string): string {
       for (let i = 0, len = url.length; i < len; i++) {
         const charCode = url.charCodeAt(i);
    @@ -214,7 +278,7 @@ function fastifyMiddie(
       fastify.decorate('use', use as any);
       fastify[kMiddlewares] = [];
       fastify[kMiddieHasMiddlewares] = false;
    -  fastify[kMiddie] = middie(onMiddieEnd);
    +  fastify[kMiddie] = middie(onMiddieEnd, fastify.initialConfig);
     
       const hook = options.hook || 'onRequest';
     
    @@ -295,7 +359,7 @@ function fastifyMiddie(
       function onRegister(instance: FastifyInstance) {
         const middlewares = instance[kMiddlewares].slice() as Array<Array<unknown>>;
         instance[kMiddlewares] = [];
    -    instance[kMiddie] = middie(onMiddieEnd);
    +    instance[kMiddie] = middie(onMiddieEnd, instance.initialConfig);
         instance[kMiddieHasMiddlewares] = false;
         instance.decorate('use', use as any);
         for (const middleware of middlewares) {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.