feat(core): redact sensitive fields in fetch-log bodies + URL query strings#1598
Conversation
…oses #1593) Extends the header redaction from #1585. Before a fetch entry reaches any sink, mask known-sensitive credentials in the request/response bodies and the URL query string, in addition to headers: - redactBody(): form-urlencoded + JSON bodies, recursive over nested objects/arrays, best-effort (never throws), values-only. - redactUrlQuery(): masks sensitive query params, preserves path, non-sensitive params, and any fragment. - Sensitive fields: client_secret, code, refresh_token, access_token, id_token, code_verifier, client_assertion, assertion, password, token (case-insensitive), reusing the [REDACTED] sentinel. Applied to the request success + error entries and the async response-body callback. Live outbound requests are byte-identical — only the logged copy is redacted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
|
@claude review |
|
Claude finished @cliffhall's task in 1m 49s —— View job Review: body + URL-query redactionRead the implementation ( Discussion — the tracker wraps all MCP traffic, not just OAuth
Mitigating factor: matching is exact key match ( Worth confirming this is the intended trade-off (#1593). Options if over-redaction becomes a problem: scope body redaction to OAuth/token endpoints (by URL/host), or drop the most generic names ( Minor notes (non-blocking)
Nits
TestsCoverage is strong: form + JSON (request and async response), URL redaction on success and error paths, case-insensitivity, fragment preservation, repeated-param collapse, nested object/array recursion, JSON-scalar and malformed no-throw passthrough, and the live-body-untouched assertion. Nothing obvious missing. One small addition worth considering: a case asserting a nested sensitive key in a request body is redacted (the existing nested test exercises Nice work — the security intent is met and the best-effort/no-throw discipline is exactly right for a logging path. My only substantive ask is confirming the broad-redaction trade-off in note above is intended. |
Address round-1 review feedback on fetchTracking redaction: - Cross-reference REDACTED_VALUE / REDACTED_HEADER_VALUE so a reader understands why two identical "[REDACTED]" constants coexist. - Document that redactBody deliberately scopes to form-encoded + JSON (multipart/binary bodies pass through; OAuth never uses multipart). - Add a tracker-path test asserting a nested sensitive key in a JSON request body is redacted in the recorded copy while the live outbound body stays byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
|
Thanks for the thorough review. Responses to each point (pushed in 775b98a): Broad-redaction trade-off (substantive ask) — intended, confirming. Wrapping all MCP traffic is deliberate for #1593: the tracker is the single choke point every body flows through before it reaches any sink (in-memory log, pino, persisted session storage), so redacting there gives one uniform guarantee rather than URL/host-sniffing per request. The exact-key-match design (no substring) is exactly what keeps the blast radius acceptable — Minor 1 (JSON round-trip cost on every non-form body) — acknowledged, no change. Correct that a parse+re-stringify runs on any body that parses as a JSON object, and that whitespace in the logged copy isn't preserved. This is on the fire-and-forget async read path (never blocks the transport), and correctness/security win over preserving log-copy formatting. Noting, not changing. Minor 2 (form re-serialization re-encodes non-sensitive fields) — acknowledged, no change. Only fires when a sensitive field is present, and only affects the logged copy (the live body is provably byte-identical — directly tested). Cosmetic; leaving as-is. Minor 3 (multipart not redacted) — done. Added a JSDoc note on Nit (two identical Tests (nested sensitive key through the request path) — done. Added a test that sends a JSON request body with a nested fetchTracking.ts coverage remains ≥90 on all four dims (98.27 / 98.55 / 100 / 98.18). validate + coverage + integration all green. |
|
@claude review |
|
Claude finished @cliffhall's task in 1m 0s —— View job Re-review: body + URL-query redaction (after
|
| Item | Status |
|---|---|
| Broad-redaction trade-off | ✅ Confirmed intended; exact-key-match keeps blast radius acceptable |
| JSON round-trip cost on non-form bodies | ✅ Acknowledged (async read path, correctness > log-copy formatting) |
| Form re-serialization cosmetic re-encode | ✅ Acknowledged (logged copy only; live body proven byte-identical) |
multipart/form-data scope |
✅ JSDoc note added (fetchTracking.ts:133-136) |
Two identical [REDACTED] constants |
✅ Cross-reference comment added (fetchTracking.ts:44-50) |
| Nested sensitive key through the request path | ✅ Test added (fetchTracking.test.ts:480-507) — asserts nested access_token masked, siblings kept, live body byte-identical |
The redactBody JSDoc is now precise about scope, and the new test closes the one coverage gap I flagged (nested redaction through the tracker, not just the direct unit call).
One new minor nit (non-blocking)
redactUrlQuery splits on the first ? (fetchTracking.ts:83). If a URL carries a ? inside its fragment with no real query — e.g. https://x/p#a?b=c — the ? in the fragment is treated as the query start and b would be considered a param. Real OAuth URLs always put the query before the fragment, so this can't occur in the targeted flows; flagging only for completeness. The #-after-? case (the normal ordering) is handled correctly (fetchTracking.ts:88-91).
Also note the same cosmetic re-encode you acknowledged for form bodies applies to redactUrlQuery when it hits params.toString() (non-sensitive params in the logged URL may re-encode) — logged copy only, live input untouched. Purely cosmetic; no action needed.
Tests
Comprehensive and correct: form + JSON (request and async response), URL redaction on success and error paths, case-insensitivity, fragment preservation, repeated-param collapse, nested object/array recursion, JSON-scalar and malformed no-throw passthrough, non-sensitive passthrough, and the live-body-byte-identical assertions on both the form and nested-JSON paths. No gaps I can see.
The security intent is met, the best-effort/no-throw discipline is right for a logging path, and the "live request is byte-identical, only the logged copy is redacted" guarantee is directly tested on every redaction path. LGTM. ✅
• 1593-redact-bodies
|
📋 Smoke test & AGENTS.md audit for this PR: #1593 (comment) (posted on the linked issue #1593). |
Closes #1593
Follow-up to the header-redaction slice (#1585).
core/mcp/fetchTracking.tsrecordsurl,headers,requestBody, andresponseBodyper request; before this change onlyheaderswas redacted, so the secrets that ride in OAuth token exchanges still leaked verbatim through the request/response body and the URL query string. This extends redaction to cover both, alongside the existing header logic (which is untouched).What is redacted
Known-sensitive field / param names (case-insensitive), reusing the
[REDACTED]sentinel style:client_secret,code,refresh_token,access_token,id_token,code_verifier,client_assertion,assertion,password,token.How
redactBody(body, contentType)— masks values inapplication/x-www-form-urlencodedbodies (re-serialized viaURLSearchParams) and JSON bodies (recursive walk over nested objects/arrays, values-only, structure preserved). JSON is also sniffed when the content-type is missing. Best-effort: empty, non-string, scalar-JSON, and unparseable bodies are returned unchanged — it never throws.redactUrlQuery(url)— masks sensitive query params while keeping the path, non-sensitive params, and any trailing#fragmentreadable; repeated sensitive params collapse to one redacted value. Returns the URL unchanged when there is nothing to redact.init.bodyis unchanged).Tests
Added coverage for form + JSON body redaction (request and async response), URL-query redaction on both success and error paths, non-sensitive passthrough, malformed-body no-throw, nested-object/array recursion, fragment/case-insensitive/repeated-param handling, and live-object untouched. Unit tests for
redactBody/redactUrlQuerydirectly.clients/webper-file coverage forfetchTracking.ts: 98.27% stmts / 98.55% branch / 100% funcs / 98.18% lines (gate is 90 on all four). Webvalidate, fulltest:coveragegate, andtest:integrationall pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX