← Back to Research
Research · July 22, 2026

Bypassing go-ethereum's --http.vhosts host allowlist with an empty Host, forged by an HAProxy HTTP/2→1.1 downgrade

Simon Morley·7 min read

A go-ethereum node that gates its JSON-RPC by Host header — the --http.vhosts allowlist that ships as geth's DNS-rebinding protection — accepts a request whose Host is empty (HTTP 200) while correctly rejecting the same request carrying a concrete, disallowed hostname (HTTP 403). When such a node sits behind an HAProxy HTTP/2 front that downgrades to HTTP/1.1, an attacker can produce that empty Host on demand: an HTTP/2 request with an empty :authority pseudo-header is normalised by HAProxy into an HTTP/1.1 request with an empty Host, which slips past the allowlist and reaches the RPC — a bypass of the Host-based access control the operator is relying on.

Why this matters

--http.vhosts is go-ethereum's documented protection against DNS rebinding: it restricts which Host values may reach the HTTP JSON-RPC, so a node is not driven by a request carrying an attacker-chosen hostname. Operators also lean on it as a plain Host allowlist in front of an RPC that is otherwise unauthenticated. The bypass matters because it defeats that control on a specific but ordinary deployment shape — geth behind a TLS-terminating HAProxy that speaks HTTP/2 to clients and HTTP/1.1 to the backend — without the operator changing anything.

The caveat that bounds it, stated plainly: the empty :authority cannot come from a browser (browsers always populate :authority), so this is not the classic browser-driven rebinding. It needs an attacker with a raw HTTP/2 client that can reach the front. Within that model, the naïve assumption — "the allowlist rejects bad hostnames, so we're covered" — is wrong in a specific way: the allowlist rejects the bad hostname but waves through the absent one.

How the attack works

  • Entry point: the TLS HTTP/2 front (here HAProxy 3.3.6) terminating client connections in front of go-ethereum's HTTP JSON-RPC (--http) on :8545.
  • Preconditions: geth's HTTP RPC is reachable only through the proxy; geth runs a --http.vhosts allowlist that does not include the attacker's chosen hostname (so a direct, honestly-labelled attempt is refused); and the front performs an HTTP/2→HTTP/1.1 downgrade that maps the request's :authority onto the HTTP/1.1 Host.
  • Processing path: the attacker sends an HTTP/2 request with :authority set to the empty string alongside a separate host: evil.example header. HAProxy downgrades the stream to HTTP/1.1 and forwards an empty Host to geth, ignoring the supplied host: header. geth's virtual-host check (node/rpcstack.go, virtualHostHandler) compares the incoming Host against the --http.vhosts allowlist and treats the empty value as an implicitly trusted default rather than an explicit deny, so the request is served.
  • Boundary crossed: the Host-based access-control / anti-DNS-rebinding trust boundary. The allowlist comparator is handed an input it does not model — absent identity — and fails open.
  • Resulting behaviour: the control returns HTTP 200 for the empty-Host request, while the same request sent honestly as :authority=evil.example (a concrete disallowed host) returns HTTP 403. The attacker reaches the JSON-RPC endpoint the Host filter was meant to keep them off.

Affected systems

  • Severity: MEDIUM — a Host-allowlist / anti-DNS-rebinding access-control bypass; no funds, no consensus impact, no memory-safety issue. Reaching the RPC still exposes only whatever the enabled namespaces serve.
  • Chain: Ethereum.
  • Implementation making the trust decision: go-ethereum, tested at 1.17.4, relying on its --http.vhosts Host allowlist.
  • Required edge: an HTTP/2→HTTP/1.1 downgrading front that forwards an empty :authority as an empty Host — observed with HAProxy 3.3.6 (a TLS HTTP/2 front).
  • Not affected the same way: a front that refuses or normalises the empty :authority rather than forwarding it. Advisory NR-2026-054 scopes the bypass as HAProxy-specific and records that nginx GOAWAYs and Envoy rejects the empty :authority, so those fronts are not affected — a scoping claim from the finding, not reproduced in this bundle's capture (which contains only the HAProxy control/attack exchanges). geth bound to loopback with no proxy, or a deployment that does not rely on --http.vhosts for access control, is outside this bypass. This is a composition of a front-end downgrade quirk with a geth Host-check behaviour, not a version-specific geth regression; no affected version range is established beyond the tested 1.17.4.

Evidence

  • Disclosed by NullRabbit: advisory NR-2026-054 ("h2→h1 empty-:authority downgrade bypasses geth's --http.vhosts Host allowlist", published 2026-07-15) is the canonical record; it cites geth's virtualHostHandler (node/rpcstack.go) and the HAProxy downgrade, and carries the measurements below. No CVE is assigned.
  • Publicly documented: go-ethereum documents --http.vhosts as the virtual-host / DNS-rebinding protection for its HTTP JSON-RPC server. What is exercised here is how that check treats an empty Host — an interaction, not a documented feature.
  • Independently reproduced by NullRabbit (lab): a reproducer (primitive eth_geth_h2h1_empty_authority_vhost_bypass, family auth_bypass, source_class: original) runs a self-owned localnet — real go-ethereum 1.17.4 behind a real HAProxy 3.3.6 TLS HTTP/2 front — and drives the empty-:authority request against it. The captured packet trace ships in the public dataset NullRabbit/nr-bundles-public (corpus crp_3083e2525b7743c4).
  • Observed under controlled conditions: the control request (:authority=evil.example, a disallowed host) returns HTTP 403 ("invalid host specified"); the attack request (empty :authority + host: evil.example) returns HTTP 200. The advisory records the fuller behaviour that isolates the defect — Host: localhost → 200, Host: 127.0.0.1 → 200, an empty Host → 200, Host: evil.example → 403, and no Host at all → 400 — so it is the empty-but-present Host, specifically, that is treated as allowed. That differential, captured in the bundle's pcap, is the entire result — a binary allowlist bypass.
  • Measured: not applicable — this is an access-control bypass, not an amplification or resource-exhaustion finding, so there is no throughput or resource multiplier to report.

Operator actions

  • Do not treat --http.vhosts as the only Host gate behind a downgrading proxy. Enforce the Host / :authority policy at the edge as well: configure the HAProxy front to reject requests with an empty or missing :authority/Host rather than forwarding them, so the empty value never reaches geth.
  • Prefer a front that rejects the malformed request outright. Fronts that GOAWAY or 400 an empty :authority do not produce the bypassing input in the first place; the advisory scopes nginx and Envoy fronts as unaffected for this reason.
  • Keep the RPC unreachable except through the intended path — bind --http to loopback or an internal interface and require the proxy, and enable only the namespaces an application needs, so a Host-filter bypass yields the least surface.
  • Exposure check: if you run geth behind an HTTP/2-terminating proxy and rely on --http.vhosts (or a proxy Host allowlist) for access control, test how the stack handles an empty :authority/Host — send one through the front and confirm it is rejected, not answered with 200. These are configuration-level mitigations; no vendor patch is asserted here.

Canonical references

Related attacks

This instance was found on the same HTTP/2 downgrade-differential rig as a companion go-ethereum finding, eth_geth_h2c_multiplex_connlimit_bypass — an HTTP/2 cleartext (h2c) stream-multiplexing bypass of a per-connection rate limit. Both exploit a disagreement between how an edge and go-ethereum interpret the same HTTP/2 request: one over the Host/:authority identity, the other over connection-vs-stream accounting. NRDAX groups this instance under the chain-agnostic technique NRDAX-T0110 (HTTP Header Spoofing Trust Bypass), whose registry entry also catalogues known public header-trust CVEs in other software as coverage references; those are separate implementations and are not asserted to be the same defect. No relationship is claimed beyond the shared rig, the auth_bypass family, and that technique id.

Track attacks affecting go-ethereum and Ethereum RPC infrastructure.

Analysis plus reproducer. No weaponised proof-of-concept code.

researchethereumgo-ethereumhaproxyhttp2auth_bypassrpcinfrastructure-security
Publication policy:Why we publish these findings →

Related Posts

One prior-knowledge h2c connection multiplexes N eth_getLogs past an L4 per-connection cap on go-ethereum's JSON-RPC port

·7 min read

go-ethereum's JSON-RPC port terminates cleartext HTTP/2 (h2c) by prior knowledge, so one TCP connection can multiplex N eth_getLogs streams past an L4 edge that only caps connections per IP (nginx stream limit_conn) — measured 20/20 streams and ~39 MB pulled through a single connection the edge counts as one, an amplification-multiplexing bypass of a connection cap, not of an L7 request rate limit.

researchethereumgo-ethereum+5 more