Skip to content

fix(vmcp): reject Authorization and Cookie in passthroughHeaders, as documented - #6235

Open
SashaMIT wants to merge 2 commits into
stacklok:mainfrom
SashaMIT:fix/vmcp-passthrough-restricted-set
Open

fix(vmcp): reject Authorization and Cookie in passthroughHeaders, as documented#6235
SashaMIT wants to merge 2 commits into
stacklok:mainfrom
SashaMIT:fix/vmcp-passthrough-restricted-set

Conversation

@SashaMIT

@SashaMIT SashaMIT commented Aug 7, 2026

Copy link
Copy Markdown

Problem

validatePassthroughHeaders checks names against middleware.RestrictedHeaders only. That map's omission of Authorization is deliberate for its original consumer (the standalone header-forward middleware, where TestCreateHeaderForwardMiddleware_AuthorizationAllowed asserts it stays forwardable) — but for vMCP passthrough the documented contract is stricter:

virtualmcpserver-api.md: "restricted headers (Host, Authorization, X-Forwarded-*, hop-by-hop) are rejected at startup"

The validator contradicts the doc: passthroughHeaders: [authorization] starts cleanly, and the client's Authorization (or Cookie) header is then forwarded verbatim to every backend — leaking the caller's incoming credential to all upstream MCP servers. The sibling embeddingHeaders field already excludes authorization via CEL validation (config.go:976), so the codebase's own convention says these headers don't belong in operator-configured forward lists.

Fix

The vMCP validator rejects Authorization and Cookie at startup with the same error shape as the other restricted headers. The standalone middleware's behavior (and its test) is untouched — the stricter set lives in the vMCP validator where the documented contract applies.

Tests

Added "Authorization is restricted" (lowercase input, exercising canonicalization) and "Cookie is restricted" cases to TestValidator_ValidatePassthroughHeaders. Full pkg/vmcp/config suite passes.

Related: #6234 (non-JSON POST authz skip) — same seam family, independent fix.

Made with Cursor

Made with Cursor

shouldSkipInitialAuthorization skipped authorization for any POST whose
Content-Type is not application/json. Such requests are never parsed as
MCP, so message-level authorization cannot run on them - but the
transparent proxy still forwards the body verbatim, and MCP backends
parse JSON-RPC without checking Content-Type. A tools/call smuggled as
text/plain therefore reached the backend with no Cedar evaluation.

A non-JSON POST now falls through to the parsed-request check, which
rejects it with 400. The codebase already treats non-JSON smuggling as
in-scope: the batch guard runs independently of Content-Type for exactly
this reason.
…documented

The vMCP passthroughHeaders allowlist was validated only against
middleware.RestrictedHeaders, whose omission of Authorization is
deliberate for the standalone header-forward middleware (a test asserts
it stays forwardable there). For vMCP passthrough the documented
contract is stricter: virtualmcpserver-api.md promises Authorization is
rejected at startup, and the sibling embeddingHeaders field already
excludes it via CEL validation. Forwarding a caller's Authorization or
Cookie verbatim to every backend is a credential-leak footgun, not a
pass-through use case.

Reject both at startup with the same error shape as the other
restricted headers.

@jhrozek jhrozek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A couple of small nits from review, nothing blocking.

package authz

import (
"strings"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new "strings" import breaks alphabetical ordering in the stdlib block (it's placed before "bytes"). task lint will fail on this - running task lint-fix should reorder it automatically (it belongs between "os" and "testing").

// documented contract is stricter: Authorization and Cookie are rejected at
// startup because forwarding caller-supplied credentials verbatim to every
// backend is a credential-leak footgun, not a pass-through use case.
vmcpRestricted := map[string]bool{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor nit: vmcpRestricted is a local map literal rebuilt on every call to validatePassthroughHeaders. The sibling set it's OR'd against, middleware.RestrictedHeaders, is a package-level var. This only runs at startup/config-validation time so there's no real perf concern, but hoisting this to a package-level var (e.g. vmcpRestrictedHeaders) would match the existing pattern.

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