Validate the Host header on the dashboard's loopback Telefunc mount - #1387
Merged
Conversation
The mount's only guard was the Origin check, which passes a request whose Origin matches the server — including one from a page whose DNS re-answers as 127.0.0.1, since the browser then treats the daemon as that page's own origin. Host still carries the name the browser was asked for, so check it: on a loopback bind, accept only a loopback Host (or the bound address), and reject an absent one. A --host bind (#1051) is reached by a hostname the daemon cannot predict, so there is nothing to check against; it gates behind the shared token instead. The relay serves a public domain and passes no bind host, so it is unchanged. isLoopbackHost moves to its own leaf module so the mount and the daemon share one definition — daemon.ts imports the dashboard, so the mount cannot import back out of it — and is re-exported from daemon.ts for its existing callers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
suleimansh
marked this pull request as ready for review
July 30, 2026 15:53
suleimansh
added a commit
that referenced
this pull request
Jul 30, 2026
The Host-guard security fix merged with no changeset, so the changelog would never mention it (fix owed from #1387). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
The Telefunc mount's only guard was the Origin check, which passes any request whose
Originmatches the server'sHost.Hostitself was never validated — anywhere in the daemon; it was read only to compare againstOrigin.That leaves a gap for DNS rebinding: a page whose DNS re-answers as
127.0.0.1is same-origin with the daemon as far as the browser is concerned, so itsfetch()carries a matchingOriginand takes the passing branch. Every RPC behind the mount is reachable that way,sendStartincluded.Hoststill carries the name the browser was asked for, so that is what the new check reads.What changed
isExpectedHostintelefunc-serve.ts, enforced at the mount alongside the Origin check: on a loopback bind, only a loopbackHost— or the bound address itself — passes. An absentHostis rejected while enforcing; HTTP/1.1 requires it and every browser sends it.server.tspasses the address it bound to, which is what enables the check.isLoopbackHostmoves to a new leaf module,loopback-host.ts, and is re-exported fromdaemon.tsfor its existing callers. The mount cannot import fromdaemon.ts(that imports the dashboard, so it would be a cycle), and the daemon's "does this bind need a token" test and the mount's "is this Host rebound" test must not drift apart.Scope
Enforcement is limited to loopback binds. A
--hostbind (#1051) is reached by a hostname the daemon cannot predict, so there is no allowlist to build; that path gates behind the shared daemon token instead. The relay serves a public domain and passes no bind host, so it is unaffected.isSameOriginRequestkeeps its existing contract — an absentOriginstill passes, for curl and the test suite — so the Host check lives in its own function and the existing assertion for it is unchanged.Testing
/_telefuncwith a reboundHost(and the matchingOrigina rebound browser would send) gets a 403, while the identical request carrying the realHostdoes not.Host, a non-loopback bind, no bind host, and the bracketed IPv6 form[::1]:4200that a naive port split would mangle.the-frameworksuite: 1629 tests, 0 failures.turbo run typecheck: 22/22 green.🤖 Generated with Claude Code