mirror of
https://github.com/nestjs/nest.git
synced 2026-10-28 09:45:43 +08:00
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:
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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user