diff --git a/src/cliproxy/auth/__tests__/oauth-trace-redactor.test.ts b/src/cliproxy/auth/__tests__/oauth-trace-redactor.test.ts index beec9578..8b48261c 100644 --- a/src/cliproxy/auth/__tests__/oauth-trace-redactor.test.ts +++ b/src/cliproxy/auth/__tests__/oauth-trace-redactor.test.ts @@ -51,6 +51,40 @@ describe('redactString', () => { test('empty input passthrough', () => { expect(redactString('')).toBe(''); }); + + test('redacts JSON-style token payloads in plain strings', () => { + const out = redactString( + '{"access_token":"AT_SECRET","refresh_token":"RT_SECRET","token":"TOKEN_SECRET"}' + ); + expect(out).not.toContain('AT_SECRET'); + expect(out).not.toContain('RT_SECRET'); + expect(out).not.toContain('TOKEN_SECRET'); + expect(out).toContain(REDACTED_PLACEHOLDER); + }); + + test('preserves JSON-style separator spacing while redacting token payloads', () => { + const out = redactString('{"access_token" : "AT_SECRET"}'); + expect(out).not.toContain('AT_SECRET'); + expect(out).toContain(`"access_token" : "${REDACTED_PLACEHOLDER}"`); + }); + + test('redacts line-leading key/value formats', () => { + const out = redactString( + 'access_token=AT_SECRET refresh_token=RT_SECRET client_secret: CS_SECRET' + ); + expect(out).not.toContain('AT_SECRET'); + expect(out).not.toContain('RT_SECRET'); + expect(out).not.toContain('CS_SECRET'); + expect(out).toContain(`access_token=${REDACTED_PLACEHOLDER}`); + expect(out).toContain(`refresh_token=${REDACTED_PLACEHOLDER}`); + expect(out).toContain(`client_secret: ${REDACTED_PLACEHOLDER}`); + }); + + test('preserves URL-style suffixes in line-leading key/value snippets', () => { + const out = redactString('access_token=AT_SECRET&keep=1'); + expect(out).not.toContain('AT_SECRET'); + expect(out).toBe(`access_token=${REDACTED_PLACEHOLDER}&keep=1`); + }); }); describe('redactUrl', () => { diff --git a/src/cliproxy/auth/oauth-trace/redactor.ts b/src/cliproxy/auth/oauth-trace/redactor.ts index 328d9744..62c74902 100644 --- a/src/cliproxy/auth/oauth-trace/redactor.ts +++ b/src/cliproxy/auth/oauth-trace/redactor.ts @@ -40,12 +40,34 @@ const QUERY_PARAM_REGEX = new RegExp( const BEARER_REGEX = /Bearer\s+[A-Za-z0-9._\-~+/=]+/gi; +const KV_SEPARATORS = String.raw`(?:=|:)`; +const VALUE_FRAGMENT = String.raw`[^\s,;&\]}\"']+`; +const STRING_KV_KEYS = [...SENSITIVE_QUERY_KEYS, 'token'].filter((key) => key !== 'authorization'); +const SENSITIVE_KEY_GROUP = `(${STRING_KV_KEYS.join('|')})`; +const LEADING_KV_REGEX = new RegExp( + String.raw`(^|\s)${SENSITIVE_KEY_GROUP}(\s*${KV_SEPARATORS}\s*)${VALUE_FRAGMENT}`, + 'gi' +); +const QUOTED_JSON_KV_REGEX = new RegExp( + String.raw`([\"'])${SENSITIVE_KEY_GROUP}\1(\s*:\s*)([\"'])(?:\\.|(?!\4).)*?\4`, + 'gi' +); + /** Redact sensitive query-param values inside any string. Idempotent. */ export function redactString(s: string): string { if (!s) return s; return s .replace(QUERY_PARAM_REGEX, (_full, key) => `${key}=${REDACTED}`) - .replace(BEARER_REGEX, `Bearer ${REDACTED}`); + .replace(BEARER_REGEX, `Bearer ${REDACTED}`) + .replace( + QUOTED_JSON_KV_REGEX, + (_full, quoteKey, key, separator, quoteVal) => + `${quoteKey}${key}${quoteKey}${separator}${quoteVal}${REDACTED}${quoteVal}` + ) + .replace( + LEADING_KV_REGEX, + (_full, prefix, key, separator) => `${prefix}${key}${separator}${REDACTED}` + ); } /** Redact a parsed URL by name; returns redacted href or original on parse error. */