You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(mcp): prove same-machine callers by peer address, not Origin
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.
Copy file name to clipboardExpand all lines: docs/content/1.guide/14.security.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -75,7 +75,7 @@ For your own auth UI, disable built-in handling with `otpParam: false`, then cal
75
75
76
76
-**Stay on loopback.** Bind to a routable address only intentionally, and require authentication when you do.
77
77
-**Keep `auth: false` local.** The hosted bridges (`devframeViteBridge`, `@devframes/next`'s handler) gate their side-car by default; opt out with an explicit `auth: false` only when the host framework owns the trust boundary another way.
78
-
-**The MCP route trusts same-machine callers, harden it when that's not your boundary.**The origin gate keeps browsers and remote hosts out (loopback-only, `Origin`-less rejected), so the `'auto'` default - which mounts the route once agent tools exist - and `mcp: true` are enough for a local dev tool. `Origin`proves nothing about *which*local process is calling, though, so when the route is reachable beyond loopback (a widened `allowedOrigins`, a hosted app) or exposes destructive tools, add an identity check with `mcp: { authorization }` (a bearer from an env var, or a callback), or turn the route off with `mcp: false`. See [MCP](/adapters/mcp).
78
+
-**The MCP route trusts same-machine callers, harden it when that's not your boundary.**Two gates enforce that default: an origin gate (loopback-only, `Origin`-less rejected) is browser DNS-rebinding hardening, and a peer-address gate rejects a non-loopback caller even with a forged loopback `Origin` (the socket address can't be forged the way a header can). So the `'auto'` default - which mounts the route once agent tools exist - and `mcp: true` are enough for a local dev tool. Neither gate proves *which*caller it is, though, so to intentionally reach the route beyond loopback (a widened `allowedOrigins`, a hosted app) or to expose destructive tools, add an identity check with `mcp: { authorization }` (a bearer from an env var, or a callback), which also lifts the loopback-peer restriction - or turn the route off with `mcp: false`. See [MCP](/adapters/mcp).
79
79
-**Treat tokens as secrets.** Never log the bearer token or the one-time code, or bake either into build output.
80
80
-**Authorize every handler.** Validate inputs, and mark state-changing functions `type: 'destructive'` so MCP and agent clients prompt before invoking them.
81
81
-**Origin-lock remote docks.** When a hub embeds a remote-UI dock, keep `originLock` on (the default) so its session token is only honored on a connection whose `Origin` matches the dock's own.
Copy file name to clipboardExpand all lines: docs/content/2.adapters/7.mcp.md
+4-2Lines changed: 4 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -35,9 +35,11 @@ The endpoint is **stateless**: it serves the [2026-07-28 revision](https://model
35
35
36
36
### Origin gate, and opt-in identity
37
37
38
-
The **origin gate** guards every request: `Origin` must be loopback (or allow-listed), and `Origin`-less requests are rejected (a disallowed origin gets `403`). This is DNS-rebinding hardening that keeps browsers and remote hosts out, and it trusts same-machine callers - the `'auto'` default and `mcp: true` both mount origin-only, all a local dev tool needs.
38
+
The **origin gate** guards every request: `Origin` must be loopback (or allow-listed), and `Origin`-less requests are rejected (a disallowed origin gets `403`). This is DNS-rebinding hardening that keeps a browser from reaching the route across origins. It is **not** a network-locality check: `Origin` is a request header, so a non-browser client (curl, a script) sends any value it likes.
39
39
40
-
`Origin` proves nothing about *who* is calling, though: a native process on the same box can send any `Origin`. When a same-machine process isn't your trust boundary (a LAN/tunnel origin, a shared/CI host, a destructive tool surface), layer on an **identity check** with `authorization`:
40
+
Same-machine locality is instead proven from the **connected peer**: on the origin-only default (the `'auto'` default and `mcp: true`, with no widened `allowedOrigins` and no identity check), a request whose peer address is not loopback gets `403` even with a loopback `Origin`. The peer address comes from the socket, not a header, so a remote client cannot forge it. This is what makes "trusts same-machine callers" hold, and it's all a local dev tool needs. (A host that can't resolve a peer address, such as a serverless route, keeps the origin-only behavior; harden it with `authorization`.)
41
+
42
+
`Origin` still proves nothing about *who* is calling, and the peer check only proves *where from*. When same-machine isn't your trust boundary (a LAN/tunnel origin, a shared/CI host, a destructive tool surface), layer on an **identity check** with `authorization` - which also lifts the loopback-peer restriction, so authenticated callers may be remote:
0 commit comments