Skip to content

SMOODEV-2716: Redact client_secret from url-encoded request-body logging - #94

Closed
brentrager wants to merge 1 commit into
mainfrom
SMOODEV-2716-redact-form-credentials
Closed

SMOODEV-2716: Redact client_secret from url-encoded request-body logging#94
brentrager wants to merge 1 commit into
mainfrom
SMOODEV-2716-redact-form-credentials

Conversation

@brentrager

Copy link
Copy Markdown
Contributor

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 OAuth client_credentials token exchange logged client_secret=sk_... in plaintext to CloudWatch.

Surfaced during the AuthIssuer P0 (SMOODEV-2716): @smooai/config's TokenProvider.refresh does POST /token with grant_type=client_credentials&client_id=...&client_secret=sk_..., and that whole body landed in logs.

Fix

getRequestBody now runs string bodies through the new exported redactFormCredentials, which scrubs sensitive x-www-form-urlencoded params (client_secret, client_id, access_token, refresh_token, token, password, secret, code, assertion, client_assertion, api_key) to [REDACTED]. It:

  • splits each pair on the first = so base64 values with = padding survive,
  • leaves JSON string bodies untouched (they start with {/[; the logger key-redacts those),
  • routes through the single choke point, so all 6 body log sites are covered.

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 --noEmit clean.

Scope / follow-up

TypeScript only. This is the language @smooai/config uses, so it stops the reported leak. The Python / Rust / Go / .NET implementations of @smooai/fetch have 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

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-bot

changeset-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f0e4980

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@smooai/fetch Patch

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

@brentrager

Copy link
Copy Markdown
Contributor Author

Superseded by #98, which fixes the same leak at the root: @smooai/logger does no redaction at all, so the Authorization header, the raw query string and the full URL in the log message were leaking on every request too — not just the url-encoded body. #98 also covers JSON bodies (this PR skipped them on the premise that the logger key-redacts objects, which it does not), ports the URL case to Rust, and drives both suites off a shared corpus. Closing in favour of that.

Note one deliberate difference: #98 does not redact client_id. It is public by design in OAuth and load-bearing when debugging a token exchange.

@brentrager brentrager closed this Aug 20, 2026
brentrager added a commit that referenced this pull request Aug 20, 2026
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
brentrager added a commit that referenced this pull request Aug 20, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant