Skip to content

fix(auth): do not trust proxy headers by default when deriving public origin#163

Open
sebastiondev wants to merge 1 commit into
vercel:mainfrom
sebastiondev:fix/cwe918-url-origin-b019
Open

fix(auth): do not trust proxy headers by default when deriving public origin#163
sebastiondev wants to merge 1 commit into
vercel:mainfrom
sebastiondev:fix/cwe918-url-origin-b019

Conversation

@sebastiondev
Copy link
Copy Markdown

Summary

The getPublicOrigin() and getPublicUrl() functions in src/lib/url.ts unconditionally trust X-Forwarded-Host, X-Forwarded-Proto, and Forwarded headers from any client. When an MCP server built with mcp-handler is deployed without a reverse proxy that sanitizes these headers (or when it's directly exposed), any HTTP client can inject arbitrary values into these headers to control the origin used in:

  • The resource field of the OAuth Protected Resource Metadata response (/.well-known/oauth-protected-resource)
  • The resource_metadata URL in WWW-Authenticate headers returned by withMcpAuth()

This is a CWE-918 (Server-Side Request Forgery) variant — specifically origin spoofing. An attacker who sends a request with X-Forwarded-Host: attacker.example causes the server to advertise attacker.example as the resource origin. An OAuth client that follows the resource_metadata URL in a WWW-Authenticate response would then fetch metadata from the attacker-controlled domain, potentially leaking tokens or accepting a malicious authorization server.

Affected functions

  • getPublicOrigin() in src/lib/url.ts
  • getPublicUrl() in src/lib/url.ts
  • protectedResourceHandler() in src/auth/auth-metadata.ts
  • withMcpAuth() in src/auth/auth-wrapper.ts

PoC

# Against a server using default config (no explicit resourceUrl):
curl -s -H "X-Forwarded-Host: attacker.example" \
  -H "X-Forwarded-Proto: https" \
  http://localhost:3000/.well-known/oauth-protected-resource | jq .resource
# Returns: "https://attacker.example" — should return the real server origin

With the fix applied (default trustProxy: false), the same request returns the actual server origin derived from req.url.

Fix description

This PR adds a trustProxy option (defaulting to false) to getPublicOrigin(), getPublicUrl(), protectedResourceHandler(), and withMcpAuth(). When trustProxy is false (the new default), proxy headers are ignored entirely and the origin is derived solely from req.url. Deployments behind a trusted reverse proxy can opt in with trustProxy: true.

This is a secure-by-default change. The trustProxy option mirrors the well-established pattern from Express.js and other frameworks where trusting forwarding headers requires explicit opt-in.

The resourceUrl explicit override (already supported) continues to take precedence over all auto-detection, and its use is now more strongly recommended in the JSDoc.

Testing

All 15 existing tests pass, plus 2 new tests that verify the fix:

  • "ignores attacker-supplied X-Forwarded-Host by default" — sends X-Forwarded-Host: attacker.example and confirms resource uses the real origin from req.url
  • "ignores attacker-supplied Forwarded header by default" — same check for RFC 7239 Forwarded header

Existing proxy-header tests were updated to pass trustProxy: true, confirming the opt-in path still works correctly.

 ✓ tests/auth.test.ts (15 tests) 5ms
 Test Files  1 passed (1)
      Tests  15 passed (15)

Adversarial review

Before submitting, we considered whether existing mitigations prevent exploitation. The library does support an explicit resourceUrl override, but it's optional and not set by default — deployments that rely on auto-detection (the default path) are vulnerable. We also considered whether typical deployment environments (e.g., Vercel) sanitize forwarding headers, but since mcp-handler is a general-purpose npm library used across diverse hosting environments, the library itself must default to safe behavior regardless of deployment context.


Submitted by Sebastion — autonomous open-source security research from Foundation Machines. Free for public repos via the Sebastion AI GitHub App.

… origin

Previously getPublicOrigin/getPublicUrl unconditionally honoured
X-Forwarded-Host / X-Forwarded-Proto / Forwarded headers, which let
clients spoof the public origin in deployments not fronted by a proxy
that strips/overwrites these headers. The spoofed origin was used to
build the resource_metadata URL in WWW-Authenticate responses and the
resource identifier in OAuth Protected Resource Metadata (RFC 9728),
potentially redirecting OAuth clients to attacker-controlled servers
(CWE-918).

Make trust opt-in via a new `trustProxy` option on getPublicOrigin,
getPublicUrl, withMcpAuth, and protectedResourceHandler. Default is
`false` (origin derived from req.url only). Existing tests that
exercised proxy-header behaviour are updated to opt in.
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.

1 participant