Skip to content

fix(security): redact the request path on the MCP and OAuth-callback log lines - #1354

Merged
Dumbris merged 2 commits into
mainfrom
fix/redact-path-in-mcp-and-oauth-logs
Sep 24, 2026
Merged

Dumbris merged 2 commits into
mainfrom
fix/redact-path-in-mcp-and-oauth-logs

Conversation

@Dumbris

@Dumbris Dumbris commented Sep 23, 2026

Copy link
Copy Markdown
Member

What

SEC-01 follow-up. PR #1350 routed every path log field in internal/httpapi through oauth.LogSafeRequestPath. Two other sinks were still logging r.URL.Path with 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.

Based on fix/redact-apikey-from-logs (#1350), not main. oauth.LogSafeRequestPath does not exist on main, so this cannot target it. Merge #1350 first; the base will retarget automatically.

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 — which mcpAuthMiddleware calls on this endpoint — accepts ?apikey=<KEY>, so the admin key in the URL is a supported shape here, not a hypothetical one.
  • r.URL.Path arrives percent-decoded, so GET /mcp/%3Fapikey%3D<KEY> reaches the handler as the path /mcp/?apikey=<KEY>, and ServeMux still matches it against the /mcp/ subtree.
  • The completion line is logged at Warn on any >=400 response — the default log level. No debug flag involved.

Agent tokens (mcp_agt_…) and a decoded Bearer <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 LogSafeRequestPath itself

Writing the test for the shape above turned up the fact that LogSafeRequestPath did not redact it:

LogSafeRequestPath("/mcp/?apikey=4f3c…3c2b")
  → "/mcp/?apikey=4f3c…3c2b"      // unchanged

logSafeURLComponent split 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 carries k=v pairs after a ? exactly as a fragment does, and logSafeFragment already allowed for that — logSafePathSegment does the same for the path.

Changes

  • internal/server/server.go — 7 zap.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.gologSafePathSegment, splitting a segment on ?.
  • Both handler closures lifted to methods (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.go and internal/oauth/callback_path_redaction_test.go, both written failing first, both using the assertNoRun oracle from #1158 (no 12-byte run of the credential may survive, so a half-mask cannot pass):

Test Shape
…RedactsANamedCredentialInThePath /mcp/apikey=<KEY>
…RedactsThePathOnTheWarnCompletionLine /mcp/p/apikey=<KEY>, 404 → Warn
…RedactsABearerTokenInThePath decoded Bearer mcp_agt_…
…RedactsAnEncodedQueryInsideThePath /mcp/%3Fapikey%3D<KEY>
TestCallbackListenerRedacts… / TestHandleCallbackRedacts… same three shapes on both OAuth log lines
TestLogSafeRequestPathRedactsAnEncodedQueryInsideThePath the renderer gap, directly

All 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)

LogSafeRequestPath still 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 the knownSecrets variadic + currentAdminAPIKey() work in flight on #1350's branch but not yet pushed. Once it lands, internal/server threads it by adding one argument: s.runtime.Config().APIKey is 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 -skip regex) — pass
  • go test -race -tags server … across the CI package set — pass
  • gofmt clean on every changed file
  • golangci-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.sh not run: it does a blanket pkill -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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 23, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

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

View logs

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>
@Dumbris
Dumbris changed the base branch from fix/redact-apikey-from-logs to main September 24, 2026 04:54
Dumbris and others added 2 commits September 24, 2026 09:42
…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
Dumbris force-pushed the fix/redact-path-in-mcp-and-oauth-logs branch from 9c3f272 to 4365ff6 Compare September 24, 2026 06:42
@codecov-commenter

codecov-commenter commented Sep 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 94.66667% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/oauth/config.go 92.30% 1 Missing and 1 partial ⚠️
internal/server/server.go 95.12% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: fix/redact-path-in-mcp-and-oauth-logs

Available Artifacts

  • archive-darwin-amd64 (30 MB)
  • archive-darwin-arm64 (27 MB)
  • archive-linux-amd64 (18 MB)
  • archive-linux-arm64 (16 MB)
  • archive-windows-amd64 (30 MB)
  • archive-windows-arm64 (26 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (24 MB)
  • installer-dmg-darwin-arm64 (22 MB)
  • smart-mcp-proxymcpproxy-goWNBESS.dockerbuild (0 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 35965843219 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@Dumbris
Dumbris merged commit 60e09a4 into main Sep 24, 2026
42 checks passed
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.

2 participants