Repository navigation
Harden build-mode DevTools auth: require a capability token instead of disabling client auth #552
Description
Summary
In build mode, Vite DevTools disables its client-auth gate entirely, so every accepted WebSocket client is marked trusted with no OTP or token. This is defense-in-depth hardening: I'd like build mode to require a per-process capability token instead of trusting all comers.
Context: the primary cross-origin bypass in this area — devframe's loopback WS Origin classifier accepting attacker-controlled DNS names beginning with 127. (e.g. 127.attacker.example) — has a fix in devframe (devframes/devframe#319). Once DevTools resolves to a patched devframe, the public-website exploit path is closed. This issue is about the second link in that chain, which lives here in DevTools.
Where
packages/core/src/node/auth-handler.ts—isClientAuthDisabled(context)returnstrueforcontext.mode === 'build'(alongside the explicitclientAuth === falseopt-out and theVITE_DEVTOOLS_DISABLE_CLIENT_AUTHenv escape hatch).packages/core/src/node/server.ts(~L57/L80) —createDevToolsHubthen passesauth: authDisabled ? false : getAuthHandler(context)toinitHub.auth: falseengages devframe's auto-trust shim, so any accepted connection is trusted.packages/core/src/node/context.ts(~L80) — the same guard skips registering the interactive-auth RPC functions; both call sites must stay in agreement.
Residual risk (after the devframe origin fix)
The public-web chain is closed, but build mode still auto-trusts:
- any browser page served from a loopback origin — cross-port
localhost/*.localhost, e.g. another local dev tool the developer happens to have open; and - any
Origin-less local process (the WS transport allows those by design).
A trusted client can then drive the built-in terminals RPC (devframes:plugin:terminals:spawn → default shell, then :write), i.e. command execution as the Vite/Node user. So the build-mode "disable auth" posture is the remaining weak point once the origin classifier is fixed.
Proposed hardening
In the implicit mode === 'build' branch, keep the zero-prompt UX but stop trusting arbitrary clients — require a per-process, unguessable capability token that only a client able to read the locally-served connection metadata can present:
- Generate a random per-process token.
- Install the real auth handler with that token in
clientAuthTokens(instead ofauth: false), and register the interactive-auth RPC functions for this branch too. Don't print the OTP banner — trust comes purely from the token. - Inject the same token into the emitted connection metadata's
authTokenso the locally-served build viewer picks it up automatically (a same-origin viewer reads__connection.json; a cross-origin page can't, so it never learns the token).
The primitives already exist in devframe: createInteractiveAuth({ clientAuthTokens }) treats those tokens as always-trusted, and the browser client auto-sends ConnectionMeta.authToken in its anonymous:devframe:auth handshake.
The explicit clientAuth: false and VITE_DEVTOOLS_DISABLE_CLIENT_AUTH=true should keep fully disabling the gate — those are deliberate user choices; only the implicit build-mode branch should change.
References
- devframe origin-classifier fix: fix(devframe): reject non-loopback DNS origins beginning with 127. devframes/devframe#319
- CWE-346 (Origin Validation Error), CWE-1385 (Missing Origin Validation in WebSockets)
Filed with the help of an agent.
Activity