fix(express): send object error bodies with a json content type

ExpressAdapter.reply() replaced a non-JSON Content-Type set on the
route (e.g. with @Header()) only when the body had a statusCode of 400
or more. A custom HttpException body, as in
new HttpException({ ... }, 404), need not have one, so res.json() sent
the error under the route's text/html type, and request text echoed in
the body was rendered as markup.

An object body sent with an error status code now gets the JSON
Content-Type too. Behaviour change: on a route with a non-JSON
Content-Type, such a response is labelled application/json and logs the
same warning as error bodies with a statusCode already did; text bodies
keep the route's type.

Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-DGP0MUQQ.
This commit is contained in:
Kamil Myśliwiec committed 2026-10-16 14:19:45 +02:00
1 parent 67761deac7
commit 7dc6e349d5
2 files changed
+35 -1

No files matched your search

@@ -190,7 +190,12 @@ export class ExpressAdapter extends AbstractHttpAdapter<
if (
typeof responseContentType === 'string' &&
!isJsonContentType(responseContentType) &&
body?.statusCode >= HttpStatus.BAD_REQUEST
(body?.statusCode >= HttpStatus.BAD_REQUEST ||
// A custom error body (e.g. "new HttpException({ ... }, 404)") need
// not have a "statusCode" of its own
(isObject(body) &&
!isNil(statusCode) &&
statusCode >= HttpStatus.BAD_REQUEST))
) {
this.logger.warn(
"Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses",
@@ -327,6 +327,35 @@ describe('ExpressAdapter', () => {
);
});
it('should force a JSON content type for error bodies without a "statusCode"', () => {
const response = createResponse();
response.getHeader.mockReturnValue('text/html');
const warnSpy = vi
.spyOn((expressAdapter as any).logger, 'warn')
.mockImplementation(() => {});
// A custom HttpException body, e.g. new HttpException({ ... }, 404)
const body = { status: 404, error: '<img src=x onerror=alert(1)>' };
expressAdapter.reply(response, body, 404);
expect(response.setHeader).toHaveBeenCalledWith(
'Content-Type',
'application/json',
);
expect(warnSpy).toHaveBeenCalled();
expect(response.json).toHaveBeenCalledWith(body);
});
it('should keep the content type of a text body sent with an error status', () => {
const response = createResponse();
response.getHeader.mockReturnValue('text/html');
expressAdapter.reply(response, '<h1>Not found</h1>', 404);
expect(response.setHeader).not.toHaveBeenCalled();
expect(response.send).toHaveBeenCalledWith('<h1>Not found</h1>');
});
describe('when the body is a StreamableFile', () => {
const createStreamResponse = () =>
Object.assign(new PassThrough(), {