From 9ca8b0d311bb2bd88db1ab9d34d12258f6a9ab6e Mon Sep 17 00:00:00 2001 From: SashaMIT Date: Fri, 7 Aug 2026 14:19:31 +0700 Subject: [PATCH] fix(vmcp): reject Authorization and Cookie in passthroughHeaders, as 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. Signed-off-by: SashaMIT Co-authored-by: Cursor --- pkg/vmcp/config/validator.go | 12 +++++++++++- pkg/vmcp/config/validator_test.go | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/pkg/vmcp/config/validator.go b/pkg/vmcp/config/validator.go index e6766cf284..9ce1e54665 100644 --- a/pkg/vmcp/config/validator.go +++ b/pkg/vmcp/config/validator.go @@ -591,6 +591,16 @@ func (*DefaultValidator) validateCompositeToolRefs(refs []CompositeToolRef) erro return nil } +// The standalone header-forward middleware deliberately allows Authorization +// (an operator may legitimately forward it), but for vMCP passthrough the +// 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. +var vmcpRestrictedHeaders = map[string]bool{ + "Authorization": true, + "Cookie": true, +} + func (*DefaultValidator) validatePassthroughHeaders(cfg *Config) error { for i, name := range cfg.PassthroughHeaders { if name == "" { @@ -599,7 +609,7 @@ func (*DefaultValidator) validatePassthroughHeaders(cfg *Config) error { canonical := http.CanonicalHeaderKey(name) - if middleware.RestrictedHeaders[canonical] { + if middleware.RestrictedHeaders[canonical] || vmcpRestrictedHeaders[canonical] { return fmt.Errorf("passthroughHeaders[%d]: %q is a restricted header and cannot be forwarded", i, canonical) } diff --git a/pkg/vmcp/config/validator_test.go b/pkg/vmcp/config/validator_test.go index 62ee183d87..3efbbd64c0 100644 --- a/pkg/vmcp/config/validator_test.go +++ b/pkg/vmcp/config/validator_test.go @@ -1495,6 +1495,21 @@ func TestValidator_ValidatePassthroughHeaders(t *testing.T) { wantErr: true, errMsg: "X-Forwarded-For", }, + { + // Documented contract (virtualmcpserver-api.md): Authorization is + // rejected at startup. Forwarding caller-supplied credentials + // verbatim to every backend is a credential-leak footgun. + name: "Authorization is restricted", + headers: []string{"authorization"}, + wantErr: true, + errMsg: "Authorization", + }, + { + name: "Cookie is restricted", + headers: []string{"Cookie"}, + wantErr: true, + errMsg: "Cookie", + }, { name: "empty string header name is rejected", headers: []string{""},