diff --git a/src/cliproxy/auth/__tests__/oauth-handler-paste-callback.test.ts b/src/cliproxy/auth/__tests__/oauth-handler-paste-callback.test.ts index 9bf3436b..b8aa6646 100644 --- a/src/cliproxy/auth/__tests__/oauth-handler-paste-callback.test.ts +++ b/src/cliproxy/auth/__tests__/oauth-handler-paste-callback.test.ts @@ -306,7 +306,11 @@ describe('handlePasteCallbackMode traceability', () => { }, { url: /\/v0\/management\/oauth-callback$/, - response: { status: 'error', error: 'invalid_grant: expired code' }, + response: { + status: 'error', + error: + 'invalid_grant: bad redirect http://localhost:1455/callback?code=oauth-code-secret&state=upstream-state-secret', + }, status: 400, }, ]); diff --git a/src/cliproxy/auth/__tests__/oauth-trace-recorder.test.ts b/src/cliproxy/auth/__tests__/oauth-trace-recorder.test.ts index 86dc7fc8..7b7839b8 100644 --- a/src/cliproxy/auth/__tests__/oauth-trace-recorder.test.ts +++ b/src/cliproxy/auth/__tests__/oauth-trace-recorder.test.ts @@ -120,9 +120,29 @@ describe('createOAuthTraceRecorder', () => { expect(snap[0].error).toEqual({ code: 'E1', message: 'boom' }); }); - test('Error instance accepted', () => { + test('redacts OAuth secrets from error messages before they reach sinks', () => { + const lines: string[] = []; + const { rec } = makeRecorder(true, lines); + rec.record(OAuthTracePhase.Error, undefined, { + code: 'CALLBACK_REJECTED', + message: + 'bad redirect http://localhost:1455/callback?code=AUTHCODE_SECRET&state=STATE_SECRET', + }); + + const blob = JSON.stringify(rec.snapshot()) + '\n' + lines.join('\n'); + expect(blob).not.toContain('AUTHCODE_SECRET'); + expect(blob).not.toContain('STATE_SECRET'); + expect(blob).toContain(REDACTED_PLACEHOLDER); + }); + + test('Error instance accepted and redacted', () => { const { rec } = makeRecorder(); - rec.record(OAuthTracePhase.Error, undefined, new Error('plain')); - expect(rec.snapshot()[0].error?.message).toBe('plain'); + rec.record( + OAuthTracePhase.Error, + undefined, + new Error('bad redirect http://localhost:1455/callback?code=AUTHCODE_SECRET') + ); + expect(rec.snapshot()[0].error?.message).toContain(REDACTED_PLACEHOLDER); + expect(rec.snapshot()[0].error?.message).not.toContain('AUTHCODE_SECRET'); }); }); diff --git a/src/cliproxy/auth/oauth-handler.ts b/src/cliproxy/auth/oauth-handler.ts index 3c0c06f3..dea6cddc 100644 --- a/src/cliproxy/auth/oauth-handler.ts +++ b/src/cliproxy/auth/oauth-handler.ts @@ -82,6 +82,7 @@ import { generateSessionId } from './project-selection-handler'; import { createFileSink } from './oauth-trace/sink-file'; import { createOAuthTraceRecorder, OAuthTracePhase, type OAuthTraceRecorder } from './oauth-trace'; import { diagnoseFailure, formatErrorMessage } from './oauth-trace/diagnose-failure'; +import { redactString } from './oauth-trace/redactor'; interface PasteCallbackStartData { url?: string; @@ -889,8 +890,9 @@ export async function handlePasteCallbackMode( if (!callbackResponse.ok || callbackData.status === 'error') { const callbackError = callbackData.error || `OAuth callback failed with status ${callbackResponse.status}`; - console.log(fail(callbackError)); - warnPossible403Ban(provider, callbackError); + const redactedCallbackError = redactString(callbackError); + console.log(fail(redactedCallbackError)); + warnPossible403Ban(provider, redactedCallbackError); trace.record( OAuthTracePhase.Error, { status: callbackResponse.status }, @@ -915,8 +917,9 @@ export async function handlePasteCallbackMode( ); if (tokenWaitError) { - console.log(fail(tokenWaitError)); - warnPossible403Ban(provider, tokenWaitError); + const redactedTokenWaitError = redactString(tokenWaitError); + console.log(fail(redactedTokenWaitError)); + warnPossible403Ban(provider, redactedTokenWaitError); trace.record( OAuthTracePhase.Error, {}, diff --git a/src/cliproxy/auth/oauth-trace/trace-recorder.ts b/src/cliproxy/auth/oauth-trace/trace-recorder.ts index 5cf14c17..7b8d8a72 100644 --- a/src/cliproxy/auth/oauth-trace/trace-recorder.ts +++ b/src/cliproxy/auth/oauth-trace/trace-recorder.ts @@ -1,7 +1,7 @@ import { OAuthTraceEvent, OAuthTracePhase, OAuthTraceSink } from './trace-events'; import { createMemorySink } from './sink-memory'; import { createVerboseStdoutSink } from './sink-verbose-stdout'; -import { redactJsonShallow } from './redactor'; +import { redactJsonShallow, redactString } from './redactor'; export interface OAuthTraceRecorder { record( @@ -53,9 +53,9 @@ export function createOAuthTraceRecorder(options: OAuthTraceRecorderOptions): OA ): { code?: string; message: string } | undefined { if (!err) return undefined; if (err instanceof Error) { - return { message: err.message }; + return { message: redactString(err.message) }; } - return { code: err.code, message: err.message }; + return { code: err.code, message: redactString(err.message) }; } return {