Skip to content

fix(logs): mask JWTs before the high-entropy pattern can split them - #1760

Merged
Dumbris merged 1 commit into
smart-mcp-proxy:mainfrom
rudycelekli:fix/log-sanitizer-jwt-order-20261023-01
Oct 25, 2026
Merged

Dumbris merged 1 commit into
smart-mcp-proxy:mainfrom
rudycelekli:fix/log-sanitizer-jwt-order-20261023-01

Conversation

@rudycelekli

Copy link
Copy Markdown
Contributor

Bug

SecretSanitizer applies its patterns in registration order, and high_entropy is registered before jwt. high_entropy matches after =, : or a quote and stops at the first ., so for a JWT in any of those contexts it masks only the header (eyJ***J9). The jwt regex then no longer matches the rewritten string, so the payload and signature are written to the log in clear text. A standard header ({"alg":"HS256","typ":"JWT"}) is trivially guessable, so in practice the whole token is recoverable.

Trigger

Any log line or field containing:

{"id_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}

On main this is logged as:

{"id_token":"eyJ***J9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}

token=<jwt> and id_token: <jwt> leak the same way. A bare JWT (got <jwt>) is masked correctly, because high_entropy needs the =/:/quote context.

Fix

Register the jwt pattern before high_entropy. The JWT is then masked first (<header>.***.<last4>). Any remaining high-entropy header after a quote is still masked by high_entropy, as before. No pattern changes.

Test

TestSanitizer_JWTInKeyValueAndQuotedContexts checks that neither the payload nor the signature survives in the JSON, token=, id_token: and bare forms.

Commands run

  • CGO_ENABLED=0 go test -count=1 ./internal/logs: ok
  • With the fix reverted (git stash of sanitizer.go), the new test fails on 3 of 4 inputs with JWT payload/signature leaked. The bare form passes in both cases.
  • gofmt -l on the edited files: no output

@codecov-commenter

codecov-commenter commented Oct 24, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 75.00000% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/logs/sanitizer.go 75.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@Dumbris Dumbris left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified: the new test fails on main (payload and signature of a JWT after =, : or a quote were logged in clear) and passes here; race tests and CI lint for internal/logs are clean. Also checked combined with #1761 and #1762, and cross-reviewed. Masking the header twice once high_entropy runs afterwards is harmless.

@Dumbris
Dumbris merged commit 970eefb into smart-mcp-proxy:main Oct 25, 2026
46 checks passed
@Dumbris

Dumbris commented Oct 25, 2026

Copy link
Copy Markdown
Member

Merged. Thank you, @rudycelekli! A JWT logged after =, : or inside quotes (an id_token in a JSON body, a token= query) had only its header masked, so the full token was recoverable from the logs. It is now masked whole.

Contributions like this are what keep MCPProxy moving, and they matter a lot to the project.

One small ask, only if you feel like it: a short public note about your change (a tweet, a LinkedIn post, a line in a community you are part of) helps more people discover MCPProxy. No pressure at all, and I would be glad to repost it.

Dumbris pushed a commit that referenced this pull request Oct 25, 2026
## Bug

The log sanitizer's `jwt` regex accepts any non-empty third segment
(`[A-Za-z0-9\-_]+`), but its mask function always takes the last four
characters with `parts[2][len(parts[2])-4:]`. A JWT-shaped string with a
1–3 character signature panics with `slice bounds out of range` inside
`SecretSanitizer.Write`, which zap does not recover. The process logging
the line crashes.

## Trigger

Any log message or field containing, for example:

```
eyJhbGciOiJub25lIn0.eyJzdWIiOiIxIn0.x
```

This can come from upstream tool output, an error body echoed into a
log, or a test or unsigned token. The result is `panic: runtime error:
slice bounds out of range [-3:]`.

## Fix

When the signature is shorter than four characters, mask it fully
(`<header>.***.***`) instead of slicing.

This touches the same `jwt` block that #1760 moves. Whichever lands
second needs a trivial rebase.

## Test

`TestSanitizer_JWTShortSignatureDoesNotPanic` runs signatures of length
1, 2 and 3 through `sanitizeString`, recovers any panic, and checks that
the payload is masked.

## Commands run

- `CGO_ENABLED=0 go test -count=1 ./internal/logs`: ok
- Without the fix, the new test fails on all three inputs with
`panicked: runtime error: slice bounds out of range [-3:]`, `[-2:]` and
`[-1:]`.
- `gofmt -l` on the edited files: no output
Dumbris added a commit that referenced this pull request Oct 29, 2026
## Summary

Fixes two secret leaks in `internal/logs/sanitizer.go` that were already
on `main`.

**1. Bearer suffix leak (pattern order).** The patterns run one after
another over the rewritten string, and every mask inserts `***`, which
no pattern's character class accepts. `github_token`, `openai_key` and
`anthropic_key` ran before `bearer_token`. So when a Bearer credential
started with a known key prefix, the key pattern masked it first, and
bearer then stopped at the inserted `***`:

```
before: Bearer sk-AbCdEf1234567890GhIjKlMnOpQrStUvWxYz0123456789.EXTRA_SECRET
     -> Bearer sk-A***Ab***89.EXTRA_SECRET
after:  -> Bearer sk-A***ET
```

The same thing could happen inside a JWT: a key-shaped run after a `-`
in a segment got masked before `jwt` ran, and the payload and signature
were logged. The fix runs `bearer_token`, then `jwt`, before the
key-specific patterns. A comment above the patterns now states the
ordering rule.

**2. Unsigned JWT (`alg: none`).**
`eyJhbGciOiJub25lIn0.eyJzdWIiOiIxIn0.` has an empty third segment, and
the trailing `\b` could never match after a `.`, so the payload was
logged in clear. The third segment is now `[A-Za-z0-9\-_]*` and the
trailing `\b` is gone. The output is `header.***.***`.

Existing mask formats are unchanged. That includes the tests from #1760,
#1761 and #1762. A credential after `Bearer` now gets the Bearer mask
instead of the key mask.

## Testing

- New `internal/logs/sanitizer_pattern_order_test.go`: Bearer +
sk-/sk-proj-/ghp_/sk-ant- suffix, a JWT with a key-shaped run inside it,
and an unsigned JWT in four contexts. All of these failed before the
fix.
- `go test -race ./internal/logs/` passes.
- golangci-lint v2 (`.github/.golangci.yml`) reports 0 issues, both bare
and with `--build-tags server`.
- Cross-review (opencode gpt-6.1-sol, high): CLEAN.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.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.

3 participants