fix(express): run wildcard middleware on "/api/" for a named wildcard

Under a global prefix or a URI version, a wildcard middleware is
registered on the exact "/api" next to "/api/<wildcard>". As Express
routes are not strict, the exact entry also matches "/api/", and it left
that path to the wildcard entry, assuming an optional "{*path}". A named
wildcard such as forRoutes('*path') needs one more segment, so
"GET /api/" was served by the "/api" handler without the middleware.

A middleware now runs for the first of its paths that matches a request,
and the others skip that request, so "/api/" runs it once through the
exact entry whatever the wildcard. With forRoutes('*') or '{*path}', the
middleware then sees req.url "/api/" for that request, as for "/api",
instead of "/" with req.baseUrl "/api".

Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-08GP7DTU.
This commit is contained in:
Kamil Myśliwiec committed 2026-10-16 15:14:07 +02:00
1 parent 98e3ba4ef2
commit b75244ffd2
3 files changed
+89 -18

No files matched your search

@@ -4,6 +4,7 @@ import {
INestApplication,
MiddlewareConsumer,
Module,
Req,
} from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
@@ -105,6 +106,55 @@ describe('Middleware', () => {
});
});
@Controller()
class RootController {
@Get()
root(@Req() req: { runs?: number }) {
return `${req.runs ?? 0}`;
}
}
describe('Middleware - wildcard at the global prefix root', () => {
let app: INestApplication;
const createAppWithWildcard = async (wildcard: string) => {
@Module({
controllers: [RootController],
})
class RootModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => {
req.runs = (req.runs ?? 0) + 1;
next();
})
.forRoutes(wildcard);
}
}
app = (
await Test.createTestingModule({
imports: [RootModule],
}).compile()
).createNestApplication();
app.setGlobalPrefix('api');
await listenOnLoopback(app);
};
afterEach(async () => {
await app.close();
});
it.each(['*', '{*path}', '*path', '/*splat'])(
'should run the middleware once for "/api" and "/api/" (%s)',
async wildcard => {
await createAppWithWildcard(wildcard);
await request(app.getHttpServer()).get('/api').expect(200, '1');
await request(app.getHttpServer()).get('/api/').expect(200, '1');
},
);
});
async function createApp(
beforeInit?: (app: INestApplication) => void,
): Promise<INestApplication> {
@@ -400,6 +400,7 @@ export class ExpressAdapter extends AbstractHttpAdapter<
public createMiddlewareFactory(
requestMethod: RequestMethod,
): (path: string, callback: Function) => any {
const handledRequests = new WeakMap<Function, WeakSet<object>>();
return (path: string, callback: Function) => {
try {
// The core marks an exact-match path with a trailing "$" (e.g. "/api$").
@@ -413,13 +414,19 @@ export class ExpressAdapter extends AbstractHttpAdapter<
if (isExactPath && router === this.instance.use) {
router = this.instance.all;
}
// Express routes are not strict, so "/api" also matches "/api/". That
// path is left to the wildcard entry registered next to it (an optional
// "{*path}" matches it), so the middleware doesn't run twice.
const handler = isExactPath
? (req: any, res: any, next: Function) =>
req.path.endsWith('/') ? next() : callback(req, res, next)
: callback;
// Express routes are not strict, so "/api" also matches "/api/", which
// the wildcard path registered next to it may match too ("/api/{*path}"
// does, "/api/*path" does not). The middleware runs for the first of
// its paths that matches a request, and not again for the others.
const requests = handledRequests.get(callback) ?? new WeakSet();
handledRequests.set(callback, requests);
const handler = (req: any, res: any, next: Function) => {
if (requests.has(req)) {
return next();
}
requests.add(req);
return callback(req, res, next);
};
return router.call(this.instance, convertedPath, handler);
} catch (e) {
if (e instanceof TypeError) {
@@ -198,24 +198,38 @@ describe('ExpressAdapter', () => {
},
);
it('should leave "/api/" of the exact-match path to the wildcard entry', () => {
it('should run the middleware once for a request matching more than one of its paths', () => {
const allSpy = vi.spyOn(expressAdapter.getInstance(), 'all');
const useSpy = vi.spyOn(expressAdapter.getInstance(), 'use');
const middleware = vi.fn();
const next = vi.fn();
expressAdapter.createMiddlewareFactory(NO_REQUEST_METHOD)(
'/api$',
middleware,
);
const [, handler] = allSpy.mock.calls[0] as unknown as [string, Function];
const registerMiddleware =
expressAdapter.createMiddlewareFactory(NO_REQUEST_METHOD);
registerMiddleware('/api$', middleware);
registerMiddleware('/api/{*path}', middleware);
const [, exactHandler] = allSpy.mock.calls[0] as unknown as [
string,
Function,
];
const [, wildcardHandler] = useSpy.mock.calls[0] as unknown as [
string,
Function,
];
handler({ path: '/api/' }, {}, next);
expect(middleware).not.toHaveBeenCalled();
// Both "/api" (not strict) and "/api/{*path}" match "/api/", while
// "/api/*path" would not
const req = { path: '/api/' };
exactHandler(req, {}, next);
expect(middleware).toHaveBeenCalledExactlyOnceWith(req, {}, next);
wildcardHandler(req, {}, next);
expect(middleware).toHaveBeenCalledOnce();
expect(next).toHaveBeenCalledOnce();
const req = { path: '/api' };
handler(req, {}, next);
expect(middleware).toHaveBeenCalledExactlyOnceWith(req, {}, next);
const anotherReq = { path: '/api/' };
wildcardHandler(anotherReq, {}, next);
expect(middleware).toHaveBeenLastCalledWith(anotherReq, {}, next);
});
});