SMOODEV-2716: Redact client_secret from url-encoded request-body logging - #94
SMOODEV-2716: Redact client_secret from url-encoded request-body logging#94brentrager wants to merge 1 commit into
Conversation
The request/response logger stored the raw form body as an opaque string, so an OAuth client_credentials token exchange leaked its client_secret to CloudWatch in plaintext (surfaced during the AuthIssuer P0). The logger redacts by object key, but a url-encoded body isn't an object it can see into. getRequestBody now scrubs sensitive x-www-form-urlencoded params via the new exported redactFormCredentials (splits on the first `=` so base64 padding survives; leaves JSON bodies untouched). Covers all body log sites since they route through getRequestBody. TypeScript only — Python/Rust/Go/.NET parity is a follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tRq2Rge3gGHCjJdNx6xDD
🦋 Changeset detectedLatest commit: f0e4980 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Superseded by #98, which fixes the same leak at the root: Note one deliberate difference: #98 does not redact |
PR #94 diagnosed this as a url-encoded-body problem and fixed only that. The diagnosis was too narrow: @smooai/logger performs no redaction of any kind, so on every request this client was also logging the Authorization header, the raw query string, and the full URL inside the log message -- all in plaintext, at debug level. Fix it once, in the two helpers every log site already routes through (getHeadersObject / getRequestBody), plus the query string and the URL interpolated into each message. A new src/redact.ts covers headers, query strings, URLs (userinfo password included) and bodies in url-encoded, JSON string and object form. Log-only: the request on the wire is untouched. Rust's single sink is the `url` field on the `Sending HTTP request` tracing event, so it gets URL redaction and nothing more. Python, Go and .NET log nothing about a request -- .NET stores an ILogger it never calls -- so there is nothing there to redact and no unwired helper is added. Cases live in spec/redaction-corpus.json, loaded by both the TS and Rust suites, so the two implementations cannot drift. client_id is deliberately not redacted: it is public in OAuth and load-bearing when debugging. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0152bbE1veqfG1SVJdyLCBxC
PR #94 diagnosed this as a url-encoded-body problem and fixed only that. The diagnosis was too narrow: @smooai/logger performs no redaction of any kind, so on every request this client was also logging the Authorization header, the raw query string, and the full URL inside the log message -- all in plaintext, at debug level. Fix it once, in the two helpers every log site already routes through (getHeadersObject / getRequestBody), plus the query string and the URL interpolated into each message. A new src/redact.ts covers headers, query strings, URLs (userinfo password included) and bodies in url-encoded, JSON string and object form. Log-only: the request on the wire is untouched. Rust's single sink is the `url` field on the `Sending HTTP request` tracing event, so it gets URL redaction and nothing more. Python, Go and .NET log nothing about a request -- .NET stores an ILogger it never calls -- so there is nothing there to redact and no unwired helper is added. Cases live in spec/redaction-corpus.json, loaded by both the TS and Rust suites, so the two implementations cannot drift. client_id is deliberately not redacted: it is public in OAuth and load-bearing when debugging. Claude-Session: https://claude.ai/code/session_0152bbE1veqfG1SVJdyLCBxC Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Problem
The request/response logger records the outbound request body via
getRequestBody, which returned a url-encoded form body verbatim as an opaque string. The logger redacts by object key (headers, JSON fields) but can't see into a string, so an OAuthclient_credentialstoken exchange loggedclient_secret=sk_...in plaintext to CloudWatch.Surfaced during the AuthIssuer P0 (SMOODEV-2716):
@smooai/config'sTokenProvider.refreshdoesPOST /tokenwithgrant_type=client_credentials&client_id=...&client_secret=sk_..., and that whole body landed in logs.Fix
getRequestBodynow runs string bodies through the new exportedredactFormCredentials, which scrubs sensitivex-www-form-urlencodedparams (client_secret,client_id,access_token,refresh_token,token,password,secret,code,assertion,client_assertion,api_key) to[REDACTED]. It:=so base64 values with=padding survive,{/[; the logger key-redacts those),Tests
src/fetch.redact.test.ts— 3 tests (redacts secret/id while preserving base64 padding; leaves benign form bodies unchanged; doesn't corrupt JSON).tsc --noEmitclean.Scope / follow-up
TypeScript only. This is the language
@smooai/configuses, so it stops the reported leak. The Python / Rust / Go / .NET implementations of@smooai/fetchhave the same body-logging shape and need the same redaction — parity follow-up, intentionally not bundled into this incident fix.Note: the leaked secret is already in CloudWatch history until log retention expires — rotation is flagged for Brent separately.
Jira: SMOODEV-2716
🤖 Generated with Claude Code
https://claude.ai/code/session_018tRq2Rge3gGHCjJdNx6xDD