From 0e8f65c457847b0d418b230881959fcce979347e Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Thu, 25 Jun 2026 21:42:59 +0300 Subject: [PATCH] fix: mask internal Error.message in unhandled 500 responses --- .../filters/http-exception.filter.spec.ts | 67 +++++++++++++++++++ .../common/filters/http-exception.filter.ts | 3 +- 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 apps/backend/src/common/filters/http-exception.filter.spec.ts diff --git a/apps/backend/src/common/filters/http-exception.filter.spec.ts b/apps/backend/src/common/filters/http-exception.filter.spec.ts new file mode 100644 index 0000000..1a50bac --- /dev/null +++ b/apps/backend/src/common/filters/http-exception.filter.spec.ts @@ -0,0 +1,67 @@ +import { ArgumentsHost, BadRequestException, HttpStatus } from '@nestjs/common'; +import { HttpExceptionFilter } from './http-exception.filter'; + +describe('HttpExceptionFilter', () => { + const createHost = () => { + const json = vi.fn(); + const status = vi.fn(() => ({ json })); + const host = { + switchToHttp: () => ({ + getResponse: () => ({ status }), + getRequest: () => ({ url: '/api/v1/test' }), + }), + } as unknown as ArgumentsHost; + + return { host, status, json }; + }; + + it('does not expose internal Error.message for unhandled exceptions', () => { + const filter = new HttpExceptionFilter(); + const { host, status, json } = createHost(); + + filter.catch(new Error('Prisma failed at file:///secret/path'), host); + + expect(status).toHaveBeenCalledWith(HttpStatus.INTERNAL_SERVER_ERROR); + expect(json).toHaveBeenCalledWith( + expect.objectContaining({ + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + message: 'Internal server error', + error: 'Internal Server Error', + path: '/api/v1/test', + }), + ); + expect(json.mock.calls[0][0].message).not.toContain('Prisma failed'); + }); + + it('returns safe defaults for non-Error thrown values', () => { + const filter = new HttpExceptionFilter(); + const { host, status, json } = createHost(); + + filter.catch('some string error', host); + + expect(status).toHaveBeenCalledWith(HttpStatus.INTERNAL_SERVER_ERROR); + expect(json).toHaveBeenCalledWith( + expect.objectContaining({ + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + message: 'Internal server error', + error: 'Internal Server Error', + }), + ); + }); + + it('keeps HttpException response messages intact', () => { + const filter = new HttpExceptionFilter(); + const { host, status, json } = createHost(); + + filter.catch(new BadRequestException('Invalid request'), host); + + expect(status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST); + expect(json).toHaveBeenCalledWith( + expect.objectContaining({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'Invalid request', + error: 'Bad Request', + }), + ); + }); +}); diff --git a/apps/backend/src/common/filters/http-exception.filter.ts b/apps/backend/src/common/filters/http-exception.filter.ts index e44966f..c95e41a 100644 --- a/apps/backend/src/common/filters/http-exception.filter.ts +++ b/apps/backend/src/common/filters/http-exception.filter.ts @@ -26,8 +26,9 @@ export class HttpExceptionFilter implements ExceptionFilter { error = (r.error as string) || exception.name; } } else if (exception instanceof Error) { - message = exception.message; this.logger.error(`Unhandled exception: ${exception.message}`, exception.stack); + } else { + this.logger.error(`Unhandled non-error exception: ${String(exception)}`); } response.status(status).json({