fix(security): remove loopback-trust auth bypass#77
Merged
Conversation
isAuthorized()/isWsAuthorized() short-circuited to true for any request whose req.socket.remoteAddress looked like 127.0.0.1/::1 — meant to let local CLI calls skip auth. But tailscale serve (llmux's own documented remote-access path) proxies connections locally, so every tailnet visitor's request arrives at the Node process looking exactly like a trusted local one. Confirmed live and exploitable: unauthenticated GET /api/sessions (full cwd/env/initPrompts) and GET /api/settings (the entire daemon config yaml) returned real data over the daemon's own Tailscale-fronted URL with zero credentials. POST /api/sessions (spawn a session) and the WS attach route sat behind the identical bypass. Fix: drop the remoteAddress special-case entirely. Auth is now always a real v1 SAS token or v2 session/bearer, exactly like the v2 routes (/account, /admin/users) already required — an IP-based local check cannot be made safe once anything can proxy to the daemon over loopback, so the fix is to stop trusting address at all rather than trying to detect the proxy case. Default local CLI usage (no --server flag) is unaffected — per the auth recon, it calls daemon handlers in-process directly and never makes an HTTP request, so it never went through this gate either way. Also fixed a latent self-lockout: the HTML-route failure path served gatePage(), a v1-only SAS-token-entry form. Since v1 tokens can no longer be minted (tokens unified to v2 in 0.37.0), an admin with only a v2 account (the normal case now) had no way through that form at all. Redirect to /login?returnTo=<path> instead, the same mechanism v2-routes.ts already uses for /account and /admin/users. gatePage() itself is kept (still used by the legacy ?token= deep-link for anyone who still holds an old v1 SAS token); sendGate(), its only other caller, is removed as dead code. Verified end-to-end against the running daemon: GET / now 302s to /login?returnTo=%2F instead of 200; GET /api/sessions, /api/settings, /api/logs, /api/agents now 401 instead of 200 with real data; /account, /admin/users, /login unaffected; /health, /api/version (deliberately public) unaffected. With a freshly-minted v2 bearer token, all of the above return 200 with correct data again — legitimate access is not broken, only the unauthenticated bypass is closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Au8T9RPCb6wbgiEecQrBfZ
This was referenced Jul 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Security fix.
isAuthorized()/isWsAuthorized()indaemon/web/server.tstreated any request whoseremoteAddresslooked like loopback (127.0.0.1/::1) as fully authenticated — meant as a convenience for local CLI calls. Buttailscale serve, llmux's own documented remote-access path, proxies connections to the daemon locally, so every tailnet visitor's request arrives looking exactly like a trusted local one.Confirmed live and exploitable against the running daemon's own Tailscale-fronted URL, zero credentials:
GET /api/sessions→ full session list (cwd,env,initPrompts)GET /api/settings→ the entire daemon config yamlPOST /api/sessions(spawn) and the WS attach route sit behind the identical bypass (code-path confirmed, not live-fired to avoid side effects)Fix
Drop the
remoteAddressspecial-case entirely. Auth is now always a real v1 SAS token or v2 session/bearer — exactly like/accountand/admin/usersalready required. There's no way to make an IP-based "is this really local" check safe once anything can proxy over loopback, so the fix is to stop trusting address at all.Default local CLI usage is unaffected — it calls daemon handlers in-process directly (no HTTP round-trip), so it never went through this gate either way.
Also fixed a latent self-lockout: the HTML-route failure path served a v1-only SAS-token-entry form (
gatePage()). Since v1 tokens can no longer be minted (unified to v2 in 0.37.0), an admin with only a v2 account — the normal case now — had no way through it. Redirects to/login?returnTo=<path>instead, mirroringv2-routes.ts's existing pattern.gatePage()itself stays (still serves the legacy?token=deep-link); its only other caller,sendGate(), is removed as dead code.Test plan
npm run typecheckcleannpm run buildcleanGET /unauthenticated →302 /login?returnTo=%2F(was200, full page)GET /api/sessions,/api/settings,/api/logs,/api/agentsunauthenticated →401(was200with real data)/account,/admin/users,/loginunaffected (still302/200as before)/health,/api/version(deliberately public) unaffected200with correct data again — legitimate access unbroken🤖 Generated with Claude Code
https://claude.ai/code/session_01Au8T9RPCb6wbgiEecQrBfZ