From 05ee6303c2b3ed23a4b582d2f3c9f4a5ea1b0b9f Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Mon, 16 Feb 2026 15:48:10 -0600 Subject: [PATCH] fix(#411): sanitize Bearer tokens in verifySession logs + warn on non-Error thrown values - Redact Bearer tokens from error stacks/messages before logging to prevent session token leakage into server logs - Add logger.warn for non-Error thrown values in verifySession catch block for observability - Add tests for token redaction and non-Error warn logging Co-Authored-By: Claude Opus 4.6 --- apps/api/src/auth/auth.service.spec.ts | 89 ++++++++++++++++++++++++++ apps/api/src/auth/auth.service.ts | 14 +++- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/apps/api/src/auth/auth.service.spec.ts b/apps/api/src/auth/auth.service.spec.ts index 4e88469..5cc01b9 100644 --- a/apps/api/src/auth/auth.service.spec.ts +++ b/apps/api/src/auth/auth.service.spec.ts @@ -609,5 +609,94 @@ describe("AuthService", () => { await expect(service.verifySession("any-token")).rejects.toThrow("Database connection lost"); }); + + it("should redact Bearer tokens from logged error messages", async () => { + const auth = service.getAuth(); + const errorWithToken = new Error( + "Request failed: Bearer eyJhbGciOiJIUzI1NiJ9.secret-payload in header" + ); + const mockGetSession = vi.fn().mockRejectedValue(errorWithToken); + auth.api = { getSession: mockGetSession } as any; + + const loggerError = vi.spyOn(service["logger"], "error"); + + await expect(service.verifySession("any-token")).rejects.toThrow(); + + expect(loggerError).toHaveBeenCalledWith( + "Session verification failed due to unexpected error", + expect.stringContaining("Bearer [REDACTED]") + ); + expect(loggerError).toHaveBeenCalledWith( + "Session verification failed due to unexpected error", + expect.not.stringContaining("eyJhbGciOiJIUzI1NiJ9") + ); + }); + + it("should redact Bearer tokens from error stack traces", async () => { + const auth = service.getAuth(); + const errorWithToken = new Error("Something went wrong"); + errorWithToken.stack = + "Error: Something went wrong\n at fetch (Bearer abc123-secret-token)\n at verifySession"; + const mockGetSession = vi.fn().mockRejectedValue(errorWithToken); + auth.api = { getSession: mockGetSession } as any; + + const loggerError = vi.spyOn(service["logger"], "error"); + + await expect(service.verifySession("any-token")).rejects.toThrow(); + + expect(loggerError).toHaveBeenCalledWith( + "Session verification failed due to unexpected error", + expect.stringContaining("Bearer [REDACTED]") + ); + expect(loggerError).toHaveBeenCalledWith( + "Session verification failed due to unexpected error", + expect.not.stringContaining("abc123-secret-token") + ); + }); + + it("should warn when a non-Error string value is thrown", async () => { + const auth = service.getAuth(); + const mockGetSession = vi.fn().mockRejectedValue("string-error"); + auth.api = { getSession: mockGetSession } as any; + + const loggerWarn = vi.spyOn(service["logger"], "warn"); + + const result = await service.verifySession("any-token"); + + expect(result).toBeNull(); + expect(loggerWarn).toHaveBeenCalledWith( + "Session verification received non-Error thrown value", + "string-error" + ); + }); + + it("should warn with JSON when a non-Error object is thrown", async () => { + const auth = service.getAuth(); + const mockGetSession = vi.fn().mockRejectedValue({ code: "ERR_UNKNOWN" }); + auth.api = { getSession: mockGetSession } as any; + + const loggerWarn = vi.spyOn(service["logger"], "warn"); + + const result = await service.verifySession("any-token"); + + expect(result).toBeNull(); + expect(loggerWarn).toHaveBeenCalledWith( + "Session verification received non-Error thrown value", + JSON.stringify({ code: "ERR_UNKNOWN" }) + ); + }); + + it("should not warn for expected auth errors (Error instances)", async () => { + const auth = service.getAuth(); + const mockGetSession = vi.fn().mockRejectedValue(new Error("Invalid token provided")); + auth.api = { getSession: mockGetSession } as any; + + const loggerWarn = vi.spyOn(service["logger"], "warn"); + + const result = await service.verifySession("bad-token"); + + expect(result).toBeNull(); + expect(loggerWarn).not.toHaveBeenCalled(); + }); }); }); diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index 0d659f4..e0a5083 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -139,14 +139,24 @@ export class AuthService { if (!isExpectedAuthError) { // Infrastructure or unexpected — propagate as 500 + const safeMessage = (error.stack ?? error.message).replace( + /Bearer\s+\S+/gi, + "Bearer [REDACTED]" + ); this.logger.error( "Session verification failed due to unexpected error", - error.stack ?? error.message + safeMessage ); throw error; } } - // Non-Error thrown values or expected auth errors + // Non-Error thrown values — log for observability, treat as auth failure + if (!(error instanceof Error)) { + this.logger.warn( + "Session verification received non-Error thrown value", + typeof error === "object" ? JSON.stringify(error) : String(error), + ); + } return null; } }