Skip to content

diag: refuse pprof requests DNS rebinding can forge - #299

Merged
dinstein merged 3 commits into
mainfrom
fix/diag-rebinding-defense
Aug 10, 2026
Merged

diag: refuse pprof requests DNS rebinding can forge#299
dinstein merged 3 commits into
mainfrom
fix/diag-rebinding-defense

Conversation

@dinstein

Copy link
Copy Markdown
Owner

Loopback binding stops the network from reaching internal/diag's pprof endpoint, but not a local
browser under DNS rebinding: a page served from evil.example:PORT whose name is rebound to
127.0.0.1 is same-origin from the browser's point of view and can read /debug/pprof/heap, which
carries downstream credentials and tool payloads. internal/httpbridge/ingress.go's checkOrigin
already documents and defends against this exact attack one package over; diag.Serve's mux had no
equivalent.

  • 1. fix(diag): add a request-level guard (any Origin refused, Host must prove loopback) plus tests
  • 2. test(daemon): matching test for the daemon's fatal refusal of a non-loopback AGENTHUB_PPROF_ADDR
  • 3. docs(diag): document the request-level defense in the package doc, foundation.md, and security.md

Verification

make ci (build + test + lint) is green, including internal/diag, internal/daemon, and the
full suite. test/e2e's docker-runtime tests were not touched by this change and were not run in
this session's flaky window.

@dinstein
dinstein force-pushed the fix/diag-rebinding-defense branch from f89db62 to 48e8ee0 Compare August 10, 2026 03:59
@dinstein
dinstein marked this pull request as ready for review August 10, 2026 03:59
dinstein and others added 3 commits August 10, 2026 12:06
diag.Serve bound loopback-only and refused a non-loopback ADDRESS, but the
pprof mux had no request-level check. Loopback binding stops the network,
not a local browser under DNS rebinding: a page served from
evil.example:PORT whose name is rebound to 127.0.0.1 reads as same-origin
to the browser and can read /debug/pprof/heap, which carries downstream
credentials and tool payloads — a worse payload than the httpbridge attack
this mirrors, whose checkOrigin (internal/httpbridge/ingress.go) already
documents the mechanism.

requestGuard wraps the mux and fails closed on two counts: any Origin
header refuses the request outright (there is no browser client here, so
unlike the bridge no Origin is ever legitimate), and the Host header must
independently prove loopback via netguard.AddrIsLoopback, since under
rebinding Host carries the attacker's chosen name. AddrIsLoopback already
handles both a bare host and a host:port, so no normalization was needed.

Tests cover an Origin equal to Host (the exact rebound shape), a
non-loopback Host, and both curl-shaped forms (with and without a port).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
internal/gateway already had TestRunRefusesNonLoopbackProfilingAddr proving
the wiring reaches Run rather than stopping inside internal/diag; the
daemon's matching refusal (daemon.go, profiling armed before the slow part
of startup) had no such test even though the reasoning is identical.

TestDaemonRefusesNonLoopbackProfilingAddr calls daemon.Run directly with
AGENTHUB_PPROF_ADDR set to a non-loopback address and asserts the error is
diag.ErrNotLoopback before OnReady would ever fire. The existing
testResolver helper made this cheap — no new harness needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
foundation.md's pprof section and security.md's AddrIsLoopback section both
described only the bind-time loopback check, which the previous commit
extended with a request-level guard. Both now name requestGuard, its
fail-closed direction (any Origin refused, Host must independently prove
loopback), and point at internal/httpbridge/ingress.go's checkOrigin for
the rebinding mechanism rather than re-deriving it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dinstein
dinstein force-pushed the fix/diag-rebinding-defense branch from 48e8ee0 to 302aee0 Compare August 10, 2026 04:06
@dinstein
dinstein merged commit 302aee0 into main Aug 10, 2026
3 checks passed
@dinstein
dinstein deleted the fix/diag-rebinding-defense branch August 10, 2026 04:11
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