Commit a47a71f
feat(reactotron-mcp): expand redaction defaults and add form-urlencoded 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>1 parent 5270b84 commit a47a71f
3 files changed
Lines changed: 221 additions & 9 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | | - | |
61 | | - | |
62 | | - | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
63 | 63 | | |
| 64 | + | |
64 | 65 | | |
65 | 66 | | |
66 | 67 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
| 10 | + | |
9 | 11 | | |
10 | 12 | | |
11 | | - | |
12 | | - | |
13 | | - | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
14 | 24 | | |
15 | 25 | | |
16 | 26 | | |
| 27 | + | |
17 | 28 | | |
| 29 | + | |
18 | 30 | | |
19 | | - | |
20 | | - | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
21 | 38 | | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
22 | 47 | | |
23 | 48 | | |
24 | 49 | | |
| |||
222 | 247 | | |
223 | 248 | | |
224 | 249 | | |
225 | | - | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
226 | 255 | | |
227 | 256 | | |
228 | 257 | | |
| |||
238 | 267 | | |
239 | 268 | | |
240 | 269 | | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
241 | 294 | | |
242 | 295 | | |
243 | 296 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
319 | 319 | | |
320 | 320 | | |
321 | 321 | | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
322 | 333 | | |
323 | 334 | | |
324 | 335 | | |
325 | 336 | | |
326 | 337 | | |
327 | 338 | | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
328 | 368 | | |
329 | 369 | | |
330 | 370 | | |
| |||
348 | 388 | | |
349 | 389 | | |
350 | 390 | | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
351 | 509 | | |
0 commit comments