Skip to content

fix(mcp): prove same-machine callers by peer address, not Origin - #390

Merged
antfu merged 1 commit into
mainfrom
fix/mcp-peer-locality
Sep 14, 2026
Merged

antfu merged 1 commit into
mainfrom
fix/mcp-peer-locality

Conversation

@antfubot

Copy link
Copy Markdown
Collaborator

What

The route-based MCP endpoint treated a caller-supplied loopback Origin header as proof of a same-machine caller. Origin is browser hardening (DNS-rebinding), not a network-locality check: a non-browser client sends any value it likes. So a dev server reachable beyond loopback (vite --host, a framework's equivalent, or the aggregate hub route) exposed the entire agent tool surface, with no authentication, to remote callers. Where the terminals plugin is mounted (e.g. Vite/Nuxt DevTools' built-in terminal) that surface includes a shell-spawn action, so this was an unauthenticated remote code execution path.

Why this fix

On the zero-config default (no widened allowedOrigins, no identity check) the endpoint's stated contract is "trust same-machine callers." That can only be enforced from the connected peer address, which a client cannot forge, not from a request header, which it can. This change makes the default contract actually hold, while keeping the local dev experience zero-config.

  • On the origin-only default, a request whose peer address is not loopback is rejected, even with a forged loopback Origin.
  • The peer address is read from the socket (getRequestIP, never X-Forwarded-For), so it can't be spoofed by a header.
  • Deliberate opt-outs are unchanged: set mcp: { authorization } to prove who is calling (which also lifts the loopback-peer restriction, so authenticated callers may be remote), or allowedOrigins: false when the host owns the boundary.
  • A host that cannot resolve a peer address (e.g. a serverless route) keeps the prior origin-only behaviour, so nothing regresses; those deployments should harden with authorization as the docs now state.

The existing origin and identity gates are untouched; this adds a locality gate between them, scoped strictly to the vulnerable default so no explicit configuration changes behaviour.

Verification

  • New regression tests drive the handler seam with an injected peer: a forged loopback Origin from a non-loopback peer is 403; a loopback peer (incl. IPv4-mapped ::ffff:127.0.0.1) is 200; an identity check lifts the restriction; allowedOrigins: false opts out; an unknown peer stays origin-only.
  • Reproduced the original RCE end to end against the built packages, then confirmed the same request from a non-loopback peer is now refused while a loopback peer still succeeds.
  • pnpm lint && pnpm knip && pnpm typecheck && pnpm build and the affected test suites pass; API snapshots updated for the new McpConnectionInfo export and isLoopbackAddress helper.

Scope / follow-up

This closes the reachable RCE at the transport boundary for every framework that mounts the route over the node/h3 path. A separate hardening worth considering: auto-mounted MCP currently sets authorization: false even when the hub's own auth is enabled, an inconsistent silent downgrade for the destructive tool surface. Left for a follow-up since it touches the shared auth handler.

Given the downstream reach (Vite DevTools, Nuxt DevTools v4), this is worth coordinating disclosure and a release with those maintainers.

This PR was created with the help of an agent.

The route-based MCP endpoint trusted a caller-supplied loopback Origin as
proof of a same-machine caller. A non-browser client forges the header, so
a network-reachable dev server (vite --host and friends) exposed the whole
agent tool surface, including the terminal spawn action, to unauthenticated
remote callers.

On the zero-config default (no widened allowedOrigins, no identity check),
require the connected peer address to be loopback. The address comes from
the socket via getRequestIP (never X-Forwarded-For), so a client cannot
forge it. Configuring authorization or allowedOrigins: false opts out; a
host that can't resolve a peer keeps the prior origin-only behavior.
@coldtea-pr-lens

coldtea-pr-lens Bot commented Sep 14, 2026

Copy link
Copy Markdown

◈ PR Lens

🟢 +0 new · 🟠 ~1 changed · 🔴 -0 removed · 1 flow · 7 files · commit 06cac68


Architecture

Architecture diagram for devframes/devframe at 06cac68

1 component touched across 2 lanes.

Open the interactive canvas


Data flow

Data flow diagram for devframes/devframe at 06cac68

Verifying caller locality for MCP requests

Open the interactive canvas


View

  • Architecture lens
  • Data flow lens
  • Expand every detail

Tip

Add .github/workflows/pr-lens.yml with coldteadotai/pr-lens/packages/action@v0 and your model provider's key as its api-key to run PR Lens from your own CI. Any /chat/completions endpoint works.

🪧 More tips
  • Run npx skills add coldteadotai/pr-lens, then tell your coding agent: "Diagram the change you just made with PR Lens and attach it to the pull request."
  • Run npx @coldtea/pr-lens-cli analyze --base origin/main on a branch, then npx @coldtea/pr-lens-cli render .pr-lens/graph.json. Same lenses, your own model key, before the pull request exists.
  • Untick Architecture lens or Data flow lens under View to hide a diagram, or tick Expand every detail to open every section. The comment redraws in a few seconds.
  • Click the link under each diagram to open it on a canvas you can zoom, pan and step through.
  • The diagrams are links. Click one to open it on the canvas, then press W or click play to walk through the change.
  • Open a diagram on the canvas, then press W or click play to walk through the change one step at a time.
  • The CLI's render reads .github/pr-lens.yml and applies your renames, exclusions and lane pins at draw time.
  • Set github.comment.collapsed: true in .github/pr-lens.yml to fold the comment behind one View architecture and data flow row. Drawing still runs on every push.
  • Push a commit and the comment redraws for the new head. A slow older run never overwrites a newer one.
  • Switch GitHub to dark mode and the diagrams follow. The moving dots are this pull request's data in motion.

Thanks for using PR Lens! It's built by Coldtea, free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

@vercel

vercel Bot commented Sep 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
devframe Ready Ready Preview Sep 14, 2026 8:48am UTC

@antfu
antfu merged commit 582920e into main Sep 14, 2026
14 checks passed
@antfu
antfu deleted the fix/mcp-peer-locality branch September 14, 2026 09:25
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