fix(security): redact the request path on the MCP and OAuth-callback log lines - #1354
Merged
Merged
Conversation
Deploying mcpproxy-docs with
|
| Latest commit: |
4365ff6
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://4e667b37.mcpproxy-docs.pages.dev |
| Branch Preview URL: | https://fix-redact-path-in-mcp-and-o.mcpproxy-docs.pages.dev |
Dumbris
added a commit
that referenced
this pull request
Sep 23, 2026
Cross-model review (zcode/GLM-5.3) on #1354: the comment pointed at StartCallbackServer, but the closure it replaced — and the ServeMux-avoidance rationale it refers to — live in StartCallbackServerOnHost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…log lines SEC-01 left two log sinks writing `r.URL.Path` with no redaction at all — not even the name/shape rules that predate PR #1350. internal/server/server.go — the MCP mux's logging wrapper wrote `zap.String("path", r.URL.Path)` three times per request (Debug on arrival, then Debug or Warn on completion), plus four more in the deadline helpers. `/mcp/` and `/mcp/p/` are registered as SUBTREE patterns, so every byte after the prefix is whatever the caller sent; `ExtractToken` accepts `?apikey=<KEY>` on this very endpoint, so the admin key in the wrong part of the URL is not a hypothetical shape; and r.URL.Path arrives percent-DECODED, so `GET /mcp/%3Fapikey%3D<KEY>` reaches the handler as the path `/mcp/?apikey=<KEY>`. The completion line fires at Warn on any >=400 response, which is the DEFAULT log level — no debug flag involved. internal/oauth/config.go — the loopback callback listener redacted its query (LogSafeCallbackQuery, #1158) but logged the path raw at Info, in both the listener handler and handleCallback. The callback path is normally a fixed operator-configured value, so this closes an inconsistency rather than a demonstrated leak; it is still a client-controlled sink, since the handler has no mux in front of it and every path reaches the log line. Both now go through oauth.LogSafeRequestPath, the renderer internal/httpapi's access log already uses. The MCP wrapper renders it ONCE for all three of its lines: zap evaluates a field eagerly, so an unrendered path would be paid for at every log level, and the renderer walks the path per segment. internal/oauth/logging.go — LogSafeRequestPath did not actually cover the shape above. logSafeURLComponent split only on '/', so the decoded segment `?apikey=<KEY>` was handed to the name rule as the parameter name "?apikey", which matches nothing, and a 64-char hex value is under the entropy detector's threshold: the live admin key survived into the field. A path segment carries `k=v` pairs after a '?' exactly as a fragment does, and logSafeFragment already allowed for that. logSafePathSegment does the same for the path. The two handler closures were lifted to methods (Server.mcpLoggingHandler, CallbackServer.handleRequest) with no behavior change, so the log fields are reachable from a test without binding a listener. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cross-model review (zcode/GLM-5.3) on #1354: the comment pointed at StartCallbackServer, but the closure it replaced — and the ServeMux-avoidance rationale it refers to — live in StartCallbackServerOnHost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dumbris
force-pushed
the
fix/redact-path-in-mcp-and-oauth-logs
branch
from
September 24, 2026 06:42
9c3f272 to
4365ff6
Compare
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Contributor
📦 Build ArtifactsWorkflow Run: View Run Available Artifacts
How to DownloadOption 1: GitHub Web UI (easiest)
Option 2: GitHub CLI gh run download 35965843219 --repo smart-mcp-proxy/mcpproxy-go
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
SEC-01 follow-up. PR #1350 routed every
pathlog field ininternal/httpapithroughoauth.LogSafeRequestPath. Two other sinks were still loggingr.URL.Pathwith no redaction at all — not even the name/shape rules that predate #1350. This routes them through the same renderer, and closes a hole in the renderer that made the most realistic attack shape a no-op.Can these sites actually carry a live credential?
internal/server/server.go— the MCP mux's logging wrapper: yes, conditionally./mcp/and/mcp/p/are registered as subtree patterns (mux.Handle("/mcp/", …)), so every byte after the prefix is caller-controlled.httpapi.ExtractToken— whichmcpAuthMiddlewarecalls on this endpoint — accepts?apikey=<KEY>, so the admin key in the URL is a supported shape here, not a hypothetical one.r.URL.Patharrives percent-decoded, soGET /mcp/%3Fapikey%3D<KEY>reaches the handler as the path/mcp/?apikey=<KEY>, and ServeMux still matches it against the/mcp/subtree.>=400response — the default log level. No debug flag involved.Agent tokens (
mcp_agt_…) and a decodedBearer <token>land in the same field.internal/oauth/config.go— the loopback callback listener: consistency, not a demonstrated leak.The callback path is normally a fixed, operator-configured value. It is still a client-controlled sink — there is deliberately no mux in front of the handler, so every path reaches the log line including the ones that fall through to the debug page, and the listener is a plain loopback HTTP server any local process can reach for the whole login window. Every other field on those two lines already goes through this package's redactors; the path was the one that did not.
A gap in
LogSafeRequestPathitselfWriting the test for the shape above turned up the fact that
LogSafeRequestPathdid not redact it:logSafeURLComponentsplit only on/, so the name rule received the segment?apikey=<KEY>and read the parameter name as"?apikey", which matches nothing; a 64-char hex value is under the entropy detector's 4.5 bits/char threshold, so no shape rule caught it either. A path segment carriesk=vpairs after a?exactly as a fragment does, andlogSafeFragmentalready allowed for that —logSafePathSegmentdoes the same for the path.Changes
internal/server/server.go— 7zap.String("path", r.URL.Path)→oauth.LogSafeRequestPath(…)(3 in the MCP logging wrapper, 4 in the deadline helpers). The wrapper renders it once for all three of its lines: zap evaluates a field eagerly, so it is paid for at every log level, and the renderer walks the path per segment.internal/oauth/config.go— 2 sites →LogSafeRequestPath(…).internal/oauth/logging.go—logSafePathSegment, splitting a segment on?.Server.mcpLoggingHandler,CallbackServer.handleRequest) with no behavior change, so the log fields are reachable from a test without binding a listener.Tests
internal/server/mcp_logging_path_redaction_test.goandinternal/oauth/callback_path_redaction_test.go, both written failing first, both using theassertNoRunoracle from #1158 (no 12-byte run of the credential may survive, so a half-mask cannot pass):…RedactsANamedCredentialInThePath/mcp/apikey=<KEY>…RedactsThePathOnTheWarnCompletionLine/mcp/p/apikey=<KEY>, 404 → Warn…RedactsABearerTokenInThePathBearer mcp_agt_……RedactsAnEncodedQueryInsideThePath/mcp/%3Fapikey%3D<KEY>TestCallbackListenerRedacts…/TestHandleCallbackRedacts…TestLogSafeRequestPathRedactsAnEncodedQueryInsideThePathAll 6 failed before the fix, for the right reason (
path=/mcp/apikey=4f3c…verbatim in the rendered line).Known follow-up (not in this PR)
LogSafeRequestPathstill cannot redact the admin key as a bare path segment (/mcp/<KEY>) — plain hex has no name to key on and is under the entropy threshold. That needs exact-value redaction of the live key, which is theknownSecretsvariadic +currentAdminAPIKey()work in flight on #1350's branch but not yet pushed. Once it lands,internal/serverthreads it by adding one argument:s.runtime.Config().APIKeyis available at all seven call sites. Deliberately not duplicated here to avoid conflicting with that work.Verification
go test -race ./internal/oauth/... ./internal/server/...(unit-tests.yml-skipregex) — passgo test -race -tags server …across the CI package set — passgofmtclean on every changed filegolangci-lint run --config .github/.golangci.yml ./...and--build-tags server ./...— no new issues (the 16/19 pre-existing hits are all in untouched files:internal/telemetry,internal/transport,internal/runtime,internal/upstream,tests/oauthserver)scripts/test-api-e2e.shnot run: it does a blanketpkill -f "mcpproxy.*serve"(line 80), which would kill the maintainer's running instance and any concurrent session's test instances. The diff is log-field rendering plus a behavior-preserving extraction, both covered above.🤖 Generated with Claude Code