Skip to content

Validate the Host header on the dashboard's loopback Telefunc mount - #1387

Merged
suleimansh merged 1 commit into
mainfrom
worktree-dns-rebinding-host-guard
Jul 30, 2026
Merged

Validate the Host header on the dashboard's loopback Telefunc mount#1387
suleimansh merged 1 commit into
mainfrom
worktree-dns-rebinding-host-guard

Conversation

@suleimansh

Copy link
Copy Markdown
Member

The Telefunc mount's only guard was the Origin check, which passes any request whose Origin matches the server's Host. Host itself was never validated — anywhere in the daemon; it was read only to compare against Origin.

That leaves a gap for DNS rebinding: a page whose DNS re-answers as 127.0.0.1 is same-origin with the daemon as far as the browser is concerned, so its fetch() carries a matching Origin and takes the passing branch. Every RPC behind the mount is reachable that way, sendStart included.

Host still carries the name the browser was asked for, so that is what the new check reads.

What changed

  • isExpectedHost in telefunc-serve.ts, enforced at the mount alongside the Origin check: on a loopback bind, only a loopback Host — or the bound address itself — passes. An absent Host is rejected while enforcing; HTTP/1.1 requires it and every browser sends it.
  • server.ts passes the address it bound to, which is what enables the check.
  • isLoopbackHost moves to a new leaf module, loopback-host.ts, and is re-exported from daemon.ts for its existing callers. The mount cannot import from daemon.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.
  • Specs updated for both touched modules; a new spec for the leaf module.

Scope

Enforcement is limited to loopback binds. A --host bind (#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.

isSameOriginRequest keeps its existing contract — an absent Origin still 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

  • End-to-end: a POST to /_telefunc with a rebound Host (and the matching Origin a rebound browser would send) gets a 403, while the identical request carrying the real Host does not.
  • Unit: loopback spellings, the bound address, an absent Host, a non-loopback bind, no bind host, and the bracketed IPv6 form [::1]:4200 that a naive port split would mangle.
  • Full the-framework suite: 1629 tests, 0 failures. turbo run typecheck: 22/22 green.

🤖 Generated with Claude Code

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
suleimansh marked this pull request as ready for review July 30, 2026 15:53
@suleimansh
suleimansh merged commit 25e1c31 into main Jul 30, 2026
1 check failed
@suleimansh
suleimansh deleted the worktree-dns-rebinding-host-guard branch 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>
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.

1 participant