Skip to content

feat(security): enforce remote-dock originLock, gate hosted bridges by default, reject Origin-less MCP requests, move OTP to URL fragment - #161

Merged
antfu merged 2 commits into
mainfrom
fix/secure-connect-auth
Aug 5, 2026
Merged

feat(security): enforce remote-dock originLock, gate hosted bridges by default, reject Origin-less MCP requests, move OTP to URL fragment#161
antfu merged 2 commits into
mainfrom
fix/secure-connect-auth

Conversation

@antfubot

@antfubot antfubot commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Why

An audit of the devframe connect / MCP / hosted-bridge auth surfaces found that the parts protecting the browser threat are solid (OTP handshake, loopback origin gate, DNS-rebinding protection), but several surfaces were weak against local-process threats — and some protection was documented but never actually wired up. This closes four gaps.

What

1a — Remote-dock tokens / originLock are now enforced. isRemoteTokenTrusted was minted-and-delivered but had zero call sites, so a remote-UI dock's session token authenticated nothing and originLock was inert. It's now checked in createInteractiveAuth's connect-time gate, binding a dock's session token to its recorded Origin when originLock is on (the default).

1b — Hosted bridges gate by default. viteDevBridge and createDevframeNextHandler previously defaulted their side-car RPC/WS server to auth: false, leaving it reachable by anything that could open the socket. They now defer to createDevServer's gate (devframe's interactive OTP unless cli.auth opts out). This is a secure-by-default change with a one-line opt-out (auth: false); the reference hub examples do exactly that (loopback-only, no auth UI), with a comment pointing at the security guide.

2 — The route-based MCP endpoint rejects Origin-less requests. Its origin gate previously mirrored the WS transport, which allows requests with no Origin header (for native clients) — so any local process could reach every tool. The MCP route now requires an Origin that is loopback (or allow-listed); native clients like devframe connect send their own loopback origin explicitly. No bearer token and nothing secret in the connection meta.

3 — The magic-link OTP moved from the query string to the URL fragment. ?devframe_otp=#devframe_otp=. The fragment is never transmitted to the server, so the one-click auto-auth code stays out of access logs and Referer headers while keeping the same UX. Only the short-lived, single-use code ever rides the URL; the bearer token does not.

Notes

  • Behavior change (opt-out available): hosted bridges (viteDevBridge, createDevframeNextHandler) gate their side-car by default. A single-user localhost host that owns the trust boundary another way opts out with auth: false — a one-line change.
  • The MCP origin gate now rejects Origin-less requests — a native client must send a loopback Origin (as devframe connect does). Note Origin is only a trustworthy signal from real browsers; this closes the naive Origin-less path and keeps DNS-rebinding protection, but is not a hard authenticator against a local process that spoofs a loopback Origin.
  • The WS bearer token (?devframe_auth_token=) deliberately stays on the WS URL — it's read server-side from the upgrade request, so a fragment can't carry it; it remains documented as wss-only off-loopback.
  • Docs updated: docs/guide/security.md, docs/adapters/mcp.md, docs/guide/client.md, skills/devframe/SKILL.md.
  • Gate is green: pnpm lint && knip && test (1039 passing) && typecheck && build.

This PR was created with the help of an agent.

…s, move OTP to URL fragment

Harden the connect/MCP auth surfaces:

- Wire `isRemoteTokenTrusted` into `createInteractiveAuth`'s connect-time gate
  so remote-UI dock tokens actually authenticate and `originLock` binds a
  token to its dock origin (previously dead code — minted, never verified).
- Require an `Authorization: Bearer <token>` on the route-based MCP endpoint
  (`createMcpFetchHandler`), the real gate since the origin check only ever
  constrains browsers. `createDevServer`/`@devframes/next` mint a per-instance
  token, record it in the instance-registry file (now written mode 0600), and
  `devframe connect` presents it automatically.
- Move the magic-link OTP from the query string (`?devframe_otp=`) to the URL
  fragment (`#devframe_otp=`), which the browser never sends to the server, so
  the one-click code stays out of access logs and Referer headers.

BREAKING CHANGE: the hosted bridges (`viteDevBridge`,
`createDevframeNextHandler`) now gate their side-car RPC/WS server by default
instead of running with `auth: false`. Pass `auth: false` explicitly to keep a
single-user localhost host ungated. The route-based MCP endpoint now requires a
bearer token; obtain it from the instance registry (or `StartedServer.mcpAuthToken`).

Created with the help of an agent.
@netlify

netlify Bot commented Aug 4, 2026

Copy link
Copy Markdown

Deploy Preview for devfra ready!

Name Link
🔨 Latest commit 86e7b57
🔍 Latest deploy log https://app.netlify.com/projects/devfra/deploys/6a718c283cab6b0008df4aca
😎 Deploy Preview https://deploy-preview-161--devfra.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

… drop bearer token + meta-token

Replace the MCP bearer-token requirement with an origin-only gate, and stop
delivering any token through the connection meta:

- The route-based MCP endpoint no longer requires `Authorization: Bearer`; it
  instead rejects `Origin`-less requests (a request must carry a loopback or
  allow-listed `Origin`), so a route-based endpoint isn't reachable by an
  arbitrary local process while native clients send their loopback origin.
  `devframe connect` now sends each instance's own origin.
- Revert all token plumbing: `createMcpFetchHandler.authToken`,
  `StartedServer.mcpAuthToken`, the instance-registry `mcp.token` field and its
  0600 file mode, `@devframes/next` `mountMcp`/handler token surfaces.
- The hub examples no longer inject an auth token into `__connection.json`;
  they run their loopback side-car with an explicit `auth: false` (a documented
  single-user-localhost opt-out). The library default flip stays: viteDevBridge
  and createDevframeNextHandler still gate by default.

Created with the help of an agent.
@antfubot antfubot changed the title feat(security)!: enforce remote-dock tokens, gate MCP + hosted bridges, move OTP to URL fragment feat(security)!: enforce remote-dock originLock, gate hosted bridges, reject Origin-less MCP requests, move OTP to URL fragment Aug 5, 2026
@antfubot antfubot changed the title feat(security)!: enforce remote-dock originLock, gate hosted bridges, reject Origin-less MCP requests, move OTP to URL fragment feat(security): enforce remote-dock originLock, gate hosted bridges by default, reject Origin-less MCP requests, move OTP to URL fragment Aug 5, 2026
@antfu
antfu merged commit c671c49 into main Aug 5, 2026
12 checks passed
@antfu
antfu deleted the fix/secure-connect-auth branch August 5, 2026 00:38
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