diff --git a/apps/electron-backend/src/app/services/debug-trace.spec.ts b/apps/electron-backend/src/app/services/debug-trace.spec.ts index def4a2af0..91da7daed 100644 --- a/apps/electron-backend/src/app/services/debug-trace.spec.ts +++ b/apps/electron-backend/src/app/services/debug-trace.spec.ts @@ -27,4 +27,21 @@ describe('debug trace redaction', () => { expect(output).toContain('diagnostic-request-id'); expect(output).toContain('get_profile'); }); + + it('redacts serialized credentials before truncating trace strings', () => { + jest.spyOn(console, 'log').mockImplementation(() => undefined); + const secret = 'long-trace-json-password-secret'; + const diagnostic = JSON.stringify({ + password: secret, + operation: 'get_profile', + padding: 'x'.repeat(300), + }); + + trace('portal', 'request', { diagnostic }); + + const output = JSON.stringify((console.log as jest.Mock).mock.calls); + expect(output).not.toContain(secret); + expect(output).toContain('[Redacted]'); + expect(output).toContain('get_profile'); + }); }); diff --git a/apps/electron-backend/src/app/services/debug-trace.ts b/apps/electron-backend/src/app/services/debug-trace.ts index f9a0037ae..f134dc7f3 100644 --- a/apps/electron-backend/src/app/services/debug-trace.ts +++ b/apps/electron-backend/src/app/services/debug-trace.ts @@ -168,7 +168,7 @@ export function trace(scope: string, message: string, payload?: unknown): void { console.log( `${TRACE_PREFIX}[${scope}] ${message} ${safeStringifyForTrace( - summarizeForTrace(payload) + summarizeForTrace(redactSensitiveData(payload)) )}` ); } diff --git a/libs/shared/logging/src/lib/redact-sensitive-data.spec.ts b/libs/shared/logging/src/lib/redact-sensitive-data.spec.ts index 9d33e63a8..4ca3b35c4 100644 --- a/libs/shared/logging/src/lib/redact-sensitive-data.spec.ts +++ b/libs/shared/logging/src/lib/redact-sensitive-data.spec.ts @@ -77,6 +77,17 @@ describe('redactSensitiveData', () => { expect(output).toContain('401'); }); + it('does not repeat a redacted Error message secret in its stack', () => { + const secret = 'error-stack-password-secret'; + const error = new Error(`password=${secret}&operation=get_profile`); + + const output = serialized(redactSensitiveData(error)); + + expect(output).not.toContain(secret); + expect(output).toContain(encodeURIComponent(REDACTED_VALUE)); + expect(output).toContain('get_profile'); + }); + it('redacts credentials nested inside non-sensitive query values', () => { const nestedUrl = `https://identity.example/callback?token=${TEST_SECRETS[3]}&step=authorize`; const url = new URL('https://example.com/portal'); diff --git a/libs/shared/logging/src/lib/redact-sensitive-data.ts b/libs/shared/logging/src/lib/redact-sensitive-data.ts index a73d5bd47..e1b994b6e 100644 --- a/libs/shared/logging/src/lib/redact-sensitive-data.ts +++ b/libs/shared/logging/src/lib/redact-sensitive-data.ts @@ -239,7 +239,13 @@ export function redactSensitiveData( output['name'] = visitString(error.name, depth + 1); output['message'] = visitString(error.message, depth + 1); if (error.stack) { - output['stack'] = visitString(error.stack, depth + 1); + const [, ...stackFrames] = error.stack.split('\n'); + output['stack'] = visitString( + [`${output['name']}: ${output['message']}`, ...stackFrames].join( + '\n' + ), + depth + 1 + ); } if ('cause' in error) { output['cause'] = visit(error.cause, depth + 1);