Skip to content

fix(security): validate Host header to defeat DNS rebinding (daily review 2026-07-31) - #56

Open
nithiink wants to merge 1 commit into
mainfrom
security-fixes-2026-07-31
Open

fix(security): validate Host header to defeat DNS rebinding (daily review 2026-07-31)#56
nithiink wants to merge 1 commit into
mainfrom
security-fixes-2026-07-31

Conversation

@nithiink

Copy link
Copy Markdown
Owner

Fixes the single confirmed finding from the daily security review. Fixes #55.

Finding addressed

M1 (Medium) — DNS-rebinding bypass of loopback-only auth (missing Host-header validation)

In the default tokenless local mode, _access_ok trusts any client whose socket peer is loopback, and require_auth only consults the Origin allowlist when an Origin header is present (main.py:212-214). Same-origin GET/EventSource requests carry no Origin, and no Host-header allowlist existed anywhere, so a DNS-rebinding page (evil.com → 127.0.0.1) could reach the backend over loopback and read /debug/stream + /debug/recent, leaking session transcripts, tool-call arguments, and file contents surfaced in tool results.

Fix (minimal, targeted)

Change File Maps to
Add host_allowed() — allowlists loopback + private-LAN Host headers (any port), plus exact VC_ALLOWED_HOSTS; regex overridable via VC_ALLOWED_HOST_REGEX; rejects a missing/empty Host backend/config.py M1
Enforce host_allowed() in require_auth, unconditionally (not gated on the Origin header, since the rebinding vector is Origin-less) backend/main.py (require_auth) M1
Enforce host_allowed() in _ws_access_ok (WebSocket handshake, close code 4403) backend/main.py (_ws_access_ok) M1
Regression tests: loopback/LAN allowed, rebound public names + link-local + suffix-injection rejected, missing Host rejected backend/tests/test_security_hardening.py M1

Why it holds: the browser sets the Host header from the connection's name and page JS cannot override it (Host is a forbidden header), so a rebound public name like evil.com:8000 is rejected while genuine localhost / 127.0.0.1 / private-LAN access passes. This is the standard DNS-rebinding defense and does not rely on the Origin header being present.

No legitimate flow is affected: the Next /api/* proxy calls the backend at localhost:8000; native/plugin clients connect to localhost:8000; phone/LAN in network mode connects to the private-LAN IP — all covered by the allowlist. Custom DNS hostnames get the VC_ALLOWED_HOSTS escape hatch, mirroring the existing VC_ALLOWED_ORIGINS design.

Verification

  • host_allowed() logic validated against 16 host cases (loopback/LAN → allow; public/link-local/suffix-injection/empty → reject) — all pass.
  • Added HostHeaderAllowlist unit tests in the style of the existing suite.
  • Edited modules byte-compile cleanly. (Full unittest discovery in this environment is blocked by an unrelated missing dependency, claude_agent_sdk, which is pre-existing and not touched here; the new tests import only config.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01GmQcs7se3WnceoJYXwjnvY


Generated by Claude Code

In tokenless local mode the backend trusts any loopback socket peer, and
require_auth only consults the Origin allowlist when an Origin header is
present. Same-origin GET/EventSource requests carry no Origin, and no Host
allowlist existed, so a DNS-rebinding page (evil.com -> 127.0.0.1) could
read /debug/stream and /debug/recent — leaking session transcripts, tool
arguments, and file contents.

Add config.host_allowed() (loopback + private-LAN + VC_ALLOWED_HOSTS,
override via VC_ALLOWED_HOST_REGEX) and enforce it in require_auth and
_ws_access_ok, unconditionally (independent of the Origin header). The
browser sets Host from the connection name and page JS cannot forge it, so
a rebound public name is rejected while genuine localhost/LAN access passes.

Refs #55

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmQcs7se3WnceoJYXwjnvY
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.

Daily security review — 2026-07-31

2 participants