feat: add MCP redaction filtering for sensitive data#1607
feat: add MCP redaction filtering for sensitive data#1607silasjmatson wants to merge 3 commits intomasterfrom
Conversation
Add a secure-by-default redaction system that filters sensitive data from all MCP resource and tool responses. Uses a two-key model where default rules apply automatically and disabling requires agreement from both the client app and the Reactotron desktop app. Redaction engine: - Key matching: redacts object keys matching sensitiveKeys (case-insensitive) - Header matching: redacts HTTP headers matching headerNames - Value patterns: regex-based detection of Bearer tokens, JWTs, API keys - State path patterns: dot-separated paths with wildcard support - URL query params: redacts query param values matching sensitiveKeys - Handles circular references, nested objects, and arrays Client apps can send additionalRules (always allowed) or removeRules/ disableRedaction (requires server permission via settings modal). Desktop app: settings modal for editing rules and client permissions, redaction status indicator (shield icon) in footer, config persisted via electron-store. 44 tests (30 unit + 14 integration). Docs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ed body support (#1608) ## Summary Stacks on top of #1607. Expands the MCP redactor's default denylists to match the cross-tool industry consensus and adds per-field redaction for `application/x-www-form-urlencoded` request bodies. Research comparing how other developer tools handle this is below — the short version: the closest analogs (Proxyman MCP, Sentry MCP, GitHub MCP, Postman) all redact at the server boundary by default, and their built-in denylists are broader than what #1607 currently ships. ## Changes ### Default rules — additions **Header names** - CSRF / XSRF variants: `x-csrf-token`, `x-xsrf-token`, `csrf-token` - IP-forwarding PII headers: `x-forwarded-for`, `x-real-ip` **Sensitive keys** - Password aliases: `passwd`, `pwd` - Generic auth-token names: `token`, `bearer`, `jwt`, `id_token`, `idtoken` - Session & CSRF: `session`, `sessionid`, `session_id`, `csrf`, `xsrf`, `csrf_token`, `xsrf_token` - OAuth: `client_secret`, `clientsecret`, `x-api-key` **Value patterns** - Anthropic API keys (`sk-ant-…`) - AWS access key IDs (`AKIA…`) - Google API keys (`AIza…` + 35 chars) - Stripe secret/publishable/restricted keys, live + test (`(?:sk|pk|rk)_(?:test|live)_…`) - PEM-encoded private key blocks (RSA, EC, DSA, OPENSSH, PGP, generic) - GitHub PAT regex broadened from `ghp_` only to `gh[pousr]_` — covers classic, server-to-server, OAuth, user-to-server, and refresh tokens ### Form-urlencoded body redaction A new code path catches strings shaped like `k=v&k=v` with no URL prefix (typical `application/x-www-form-urlencoded` POST bodies). If any key matches `sensitiveKeys`, just that value is redacted — the same semantics already used for URL query params. A strict full-match regex prevents false positives on prose that happens to contain `=`. ### Tests 105 tests passing. New coverage: - Each category of new default rule - Each new value pattern, with test literals constructed at runtime so GitHub secret-scanning doesn't flag the test file - Form-urlencoded body redaction, including negative tests for casual strings and URL-containing strings ### Docs `docs/mcp.md` updated to reflect the expanded default list and call out form-body handling. --- ## Research — how other tools handle this We spawned parallel research on how similar developer tools handle sensitive-data redaction. Full notes kept in the PR discussion; the convergent findings: ### 1. Redact at the server/MCP boundary — unanimous Every closest analog does it at the MCP serialization layer, not in the UI and not in the model: - **Proxyman MCP** — *"Sensitive data (auth tokens, passwords, API keys) is automatically redacted in responses"* ([docs](https://docs.proxyman.com/mcp)) - **Sentry MCP** — inherits Sentry's server-side scrubber - **GitHub MCP** — scans inputs for secrets and blocks by default ([changelog](https://github.blog/changelog/2025-08-13-github-mcp-server-secret-scanning-push-protection-and-more/)) - **Postman Repro** — case-insensitive default-key redaction - **mitmproxy `FilteredDumper` pattern** — redact at display/egress, not on the wire **OWASP MCP Top 10 — MCP01:2025** explicitly mandates: *"redact or sanitize inputs and outputs before logging… redact or mask secrets before writing to logs or telemetry."* ([link](https://owasp.org/www-project-mcp-top-10/2025/MCP01-2025-Token-Mismanagement-and-Secret-Exposure)) ### 2. No `sensitive` / `secretHint` annotation exists in the MCP spec today The 2025-03-26 spec adds `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` — but the maintainers are explicit: *"clients MUST NOT rely solely on these for security decisions."* ([MCP blog](https://blog.modelcontextprotocol.io/posts/2026-03-16-tool-annotations/)) Treat server-side redaction as the hard boundary; don't wait for an annotation. ### 3. The de-facto default denylist Union across **Sentry**, **Bugsnag**, **google/har-sanitizer**, **Postman**, **Chrome DevTools sanitized HAR**, **Presidio**: - Headers: `Authorization`, `Cookie`, `Set-Cookie`, `Proxy-Authorization`, `X-Api-Key`, `X-CSRF-Token`, `X-XSRF-Token`, `X-Forwarded-For` - Keys: `password`/`passwd`/`pwd`, `secret`, `token`, `bearer`, `jwt`, `auth`, `authorization`, `api_key`/`apikey`, `credentials`, `session`/`sessionid`, `csrf`/`xsrf`, `access_token`, `refresh_token`, `id_token`, `client_secret`, `private_key` - Value patterns: AWS (`AKIA…`), Google (`AIza…`), JWT (`eyJ…`), Stripe, GitHub PATs (all prefixes), PEM private key blocks, Anthropic (`sk-ant-…`) This PR brings our defaults in line with that union. ### 4. Tool-by-tool highlights | Tool | Redaction approach | What we took / avoided | |---|---|---| | **Charles Proxy** | None built-in; user-written Rewrite rules only | Avoid its "bring your own regex" UX — ship opinionated defaults | | **Wireshark** | `editcap` + third-party TraceWrangler; fail-closed pattern | Noted `strictMode` allowlist as future work | | **Postman** | "Secret" variable type masks UI only; still exfiltrated in analytics URLs — cautionary tale | Redact the fully-rendered payload at MCP boundary, not at display | | **mitmproxy / Proxyman** | `modify_headers`, Python addons; Proxyman MCP auto-redacts but rules are opaque/non-tunable | Keep user-tunable config; don't ship an opaque rule set | | **Chrome DevTools** | `Export HAR (sanitized)` strips `Authorization`, `Cookie`, `Set-Cookie` only (Chrome 130, Oct 2024) | That's the floor. We already go beyond. | | **google/har-sanitizer** | Public [wordlist](https://github.com/google/har-sanitizer/blob/master/harsanitizer/static/wordlist.json) — `state`, `token`, `access_token`, `client_secret`, `SAMLRequest`, etc. | Directly informed our expanded default key list | | **Cloudflare HAR sanitizer** | Conditional, not denylist — strips JWT signature but keeps claims for debugging | Filed as a future enhancement (partial/format-preserving redaction) | | **Sentry / Bugsnag / Datadog / LogRocket** | Opinionated server-side defaults + user-extendable via `beforeSend`-style hook; Datadog offers partial redaction & Luhn-validated card detection | Union of their default lists → our new defaults. Partial redaction & Luhn are follow-ups. | ### Key canonical incident **Okta support breach (Oct 2023)** — attacker stole HAR files from 134 customer support tickets; the HARs contained live session tokens that were used to hijack sessions at BeyondTrust, Cloudflare, and 1Password. The PR's default-on posture is the right response to this class of leak. --- ## What is intentionally NOT in this PR Tracked as follow-ups so the review stays focused: - **Substring matching on keys.** Sentry JS and Bugsnag match substrings; that catches `sessionToken`/`userPassword` automatically but false-positives on `author`/`authored_by` when `auth` is in the list. Would need a separate denylist/pattern split. - **Typed redaction markers** (`[REDACTED:jwt]`) and a `_redacted` summary sibling field. Useful for LLM reasoning and defensive-sandwich logging but changes the public output shape. - **Luhn-validated credit-card detection.** A bare 13–19 digit regex produces too many false positives on random IDs and unix timestamps; needs Luhn to be safe. - **Cookie-value parsing within the `Cookie` header.** Currently the whole header is blunt-redacted. Cloudflare's per-cookie approach (keep names, redact values) would preserve more debug info. - **Partial / format-preserving masking** (keep last 4 of card, keep JWT claims but strip signature) — the strongest idea from Cloudflare/Datadog, worth a dedicated PR. - **`strictMode` allowlist** (à la TraceWrangler's "drop unknown layers" / mitmproxy's `FilteredDumper`) — only forward known-safe headers, redact the rest. ## Test plan - [x] `yarn test` in `lib/reactotron-mcp` — 105 tests pass - [x] `yarn typecheck` clean - [x] `yarn build` succeeds - [ ] Reviewer sanity-check: no new default key is an obvious false-positive trigger for any team's app-specific field names - [ ] Reviewer sanity-check: form-encoded regex doesn't false-positive on real-world payloads in your apps 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🐛 Redaction gap: request bodies aren't redacted by keyWhat I saw (manual QA on iOS sim + Android emulator): Fired an API call with: fetch("https://jsonplaceholder.typicode.com/posts?api_key=X&page=2", {
method: "POST",
headers: { Authorization: "Bearer abc...", Cookie: "session=..." },
body: JSON.stringify({ password: "hunter2", api_key: "x", accessToken: "y",
note: "embedded sk-ABCDEFGHIJKLMNOPQRSTUVWX" }),
})Reading "request": {
"data": "{\"password\":\"hunter2\",\"api_key\":\"x\",\"accessToken\":\"y\",\"note\":\"embedded [REDACTED] ...\"}",
"headers": { "authorization": "[REDACTED]", "cookie": "[REDACTED]" },
"params": { "api_key": "[REDACTED]", "page": "2" }
}Headers, URL query params, and the Root cause: Who's affected: anyone using Suggested fix: in |
🐛 State path patterns silently fail when requesting a subtreeRepro:
Actual: { "username": "alice", "password": "[REDACTED]", "api_key": "[REDACTED]",
"tokens": { "access": "access-raw-value", "refresh": "refresh-raw-value" } }
Expected: both redacted. Confirmed the pattern matcher itself works — changing the pattern to Root cause: Why it matters: the settings-modal UI encourages absolute paths (the placeholder example is Suggested fix: thread the requested path through // tools.ts
const redactedState = applyRedaction(stateValue, server, serverRedactionConfig, clientId, args.path ?? "")// redaction.ts
export function applyRedaction(data, server, config, clientId, basePath = "")
// -> return redact(data, rules, basePath)Same treatment for any other path-scoped code paths that return subtrees. |
ℹ️ State path patterns have limited effective surfaceRelated to the state-path-pattern bug above: in the example app, Combined with the subtree bug, this means state-path patterns written as absolute paths only fire in a narrow window: the Not blocking this PR — flagging for context. |
|
One screen exercises every MCP redaction surface (network, Redux state, logging, AsyncStorage). Each button emits a sensitive payload; the block comment above each button documents the input and the expected MCP-side redaction so future verification doesn't need an external QA doc. Client-side mcpRedaction config sends all three optional relaxations so the two-key permission model can be driven from the desktop settings modal alone, without rebuilding the app. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Warning: This was vibe-coded, we'll want to verify the architecture is what we want and review thoroughly.
Description
Add a secure-by-default redaction system that filters sensitive data from all MCP resource and tool responses. Uses a two-key model where default rules apply automatically and disabling requires agreement from both the client app and the Reactotron desktop app.
Redaction engine:
Client apps can send additionalRules (always allowed) or removeRules/ disableRedaction (requires server permission via settings modal).
Desktop app: settings modal for editing rules and client permissions, redaction status indicator (shield icon) in footer, config persisted via electron-store.
44 tests (30 unit + 14 integration). Docs updated.
Please verify the following:
yarn build-and-test:localpassesREADME.md(or relevant documentation) has been updated with your changes