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 <noreply@anthropic.com>
This commit is contained in:
@@ -609,5 +609,94 @@ describe("AuthService", () => {
|
|||||||
|
|
||||||
await expect(service.verifySession("any-token")).rejects.toThrow("Database connection lost");
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -139,14 +139,24 @@ export class AuthService {
|
|||||||
|
|
||||||
if (!isExpectedAuthError) {
|
if (!isExpectedAuthError) {
|
||||||
// Infrastructure or unexpected — propagate as 500
|
// Infrastructure or unexpected — propagate as 500
|
||||||
|
const safeMessage = (error.stack ?? error.message).replace(
|
||||||
|
/Bearer\s+\S+/gi,
|
||||||
|
"Bearer [REDACTED]"
|
||||||
|
);
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
"Session verification failed due to unexpected error",
|
"Session verification failed due to unexpected error",
|
||||||
error.stack ?? error.message
|
safeMessage
|
||||||
);
|
);
|
||||||
throw error;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user