hunting-host-header-and-url-parsing-trust · git:20260903.6dac719 · 2026-09-03 · sha256 b46af7f9063cdc0b
hunting-host-header-and-url-parsing-trust git:20260903.6dac719A
Immutable. This exact content is served forever at /api/v1/blob/b46af7f9063cdc0b.
--- name: hunting-host-header-and-url-parsing-trust description: >- Hunt trust placed in the Host or a forwarded host header and in inconsistently parsed URLs, where the app builds absolute links, keys a cache, routes a request, or matches an allowlist from a header or a parsed URL an attacker can influence or that two components parse differently. Use when a service derives its own external URL from the request, when a cache or router keys on the host, or when a security decision parses a URL. Covers password-reset and verification-link poisoning, cache poisoning, routing to internal virtual hosts, and redirect or fetch allowlist bypass through parser differentials. The attacker-influenced host or ambiguous URL is the source, the link builder, cache key, router, or allowlist parse is the sink, and trusting a host or a parse the attacker controls is the bug. license: MIT --- # Hunting host header and URL parsing trust: when the app believes the request about its own name An application often needs to know its own address, to put a link in an email or to decide where a request belongs. The easy way is to read it from the incoming request: the Host header, or a forwarded-host header a proxy added. But those are attacker-controlled, and a server that builds a password-reset link from the Host sends the victim a link pointing at the attacker's domain. A shared cache that keys on a header the origin reflects serves the poisoned response to everyone. A router that trusts a forwarded host reaches an internal virtual host. The related trap is URL parsing: when the component that checks a URL and the component that uses it disagree about where the host ends, an allowlist passes a URL that resolves somewhere else. The bug is trusting the request's account of the host, or one parser's reading of a URL, as if it were authoritative. You find it by tracing where self-URLs, cache keys, routes, and allowlists get their host. ## When to use - The app builds absolute URLs (reset, verification, invite links) from the request's Host or forwarded host. - A shared cache or a router keys on, or routes by, the Host or a forwarded-host header. - A redirect or fetch allowlist parses a URL, and different components may parse that URL differently. ## Scope check Test host and URL-parsing trust only against systems you own or are authorized to assess, on non-production infrastructure, using a benign host you control and benign markers rather than poisoning a live cache or sending real reset links to third parties. A confirmed cache-poisoning or reset-link case reaches other users, so keep every probe in scope and prefer an isolated instance. If you can't name the authorization, stop. ## The loop 1. **Establish where the host actually comes from first.** For each place the app names itself, keys a cache, routes, or matches an allowlist, determine whether it uses a fixed configured base URL and a trusted parser, or derives the host from the incoming Host or forwarded header and parses the URL ad hoc. This is the false-positive killer: a service that builds links from a configured base and routes on a trusted value cannot be poisoned through the header, while one that reflects the request host can. Name the source of the host before crafting a request. 2. **Map every host consumer.** Trace the Host and any forwarded-host header, and every parsed URL, into absolute-link builders, cache keys, routing decisions, and redirect or fetch allowlist checks. Each consumer that takes the host from the request or a differently-parseable URL is a candidate sink. 3. **Separate header trust from parser differentials.** One class is trusting the header value: the app copies the Host or forwarded host into a link, a cache key, or a route. The other is a URL that the checking parser and the using parser read differently, so the host the allowlist approves is not the host the fetch or redirect reaches. Decide which class each sink belongs to. 4. **Follow the consumer to its impact.** A poisoned self-URL in a reset or verification email points the victim at the attacker; a header reflected into a cached response poisons every client on that key; a trusted forwarded host routes to an internal virtual host the client should not reach; a parser differential passes an allowlist and resolves to an internal or attacker address. Identify which impact the sink yields. 5. **Check the defense that actually holds.** The reliable controls are building external URLs from a fixed configured base rather than the request, validating the Host against an allowlist of known hostnames, trusting forwarded headers only from a known proxy and stripping client-supplied ones at the edge, keying caches on the values that actually vary the response, and parsing URLs once with a single strict parser whose result both the check and the use share. Determine which stands at each sink. 6. **Confirm and record.** Confirm by sending a request with a crafted host or ambiguous URL and observing the poisoned link in the outbound message, the reflected host in a cached response, the internal route, or the allowlist passing a URL that resolves elsewhere, on an isolated instance with benign markers. Kill the lead if the sink uses a configured base and a validated host, if forwarded headers come only from a trusted proxy, or if one strict parser governs both check and use. Record the header or URL, the sink, the class, and the impact, or set a `kill_reason`. ## Where host and parsing trust leaks - **Self-URLs built from the request are poisonable.** A reset or verification link built from the Host or a forwarded header points wherever the attacker sets that header, delivering an attacker link in a trusted email. - **Caches key on the wrong thing.** A shared cache that keys on a header the origin reflects into the body stores the attacker's value and serves it to everyone on that key. - **Forwarded headers are client input.** Unless stripped at a trusted edge, a client can supply a forwarded-host that the app treats as authoritative for routing or link building. - **Parser differentials split check from use.** When the allowlist parser and the fetching or redirecting parser disagree about the host, the approved host and the reached host differ; the URL must be parsed once. - **Routing on host reaches internal vhosts.** A router that selects a backend by host can be steered to an internal virtual host by a crafted Host, exposing services meant to be unreachable. ## Worked example (a confirm and a kill) > **Confirm.** A password-reset flow builds the reset link from the incoming Host header. A request with the > Host set to a domain the tester controls causes the reset email to contain a link to that domain carrying > the victim's reset token, on an isolated instance with a test mailbox. **Confirmed** password-reset > poisoning through host-header trust, `high`, remediation = build the reset URL from a fixed configured base > URL, validate the Host against an allowlist of known hostnames, and strip client-supplied forwarded-host > headers at a trusted edge. > > **Kill.** The same flow builds the reset link from a configured base URL, validates the incoming Host > against an allowlist and rejects unknown hosts, trusts a forwarded host only from the known proxy, and the > redirect allowlist parses each URL once with a strict parser shared by check and fetch. A crafted Host is > rejected and the email link always points at the configured domain. **Killed**, `kill_reason` = "external > URLs come from a configured base with a validated Host, forwarded headers are trusted only from the edge > proxy, and one strict parser governs the allowlist; the attacker cannot influence the host the app uses." ## Rationalizations to reject - *"The Host header is set by the browser."* -> A client, not a browser, sends the request, and any value can be placed in the Host or a forwarded header; treat it as untrusted input, not a fact about your deployment. - *"The proxy sets the forwarded host."* -> Only if the edge strips the client's version first; otherwise a client-supplied forwarded header reaches the app and is trusted. Confirm the edge overwrites it. - *"Our URL parser is standard."* -> The question is whether the same parser governs both the check and the use; two standard parsers can still disagree on userinfo, backslashes, or empty hosts, splitting the two. - *"The cache only keys on the path."* -> If the origin reflects a header into the body but the cache does not key on it, that header poisons every response on the path; align the key with what varies the response. - *"Internal vhosts are not routable."* -> If routing selects the backend by host, a crafted Host can select an internal one; confirm the router does not trust the request's host for backend selection. ## Executing this in practice You need every builder of a self-URL, every cache key and routing decision, and every allowlist parse, with the source of the host each uses and whether one strict parser governs both check and use. For each, decide whether the attacker can set the host through a header or split the parse through an ambiguous URL, and what the consumer then does. Reading where the host and the parse come from settles most leads; a crafted host or ambiguous URL producing a poisoned link, a reflected cache entry, an internal route, or an allowlist bypass on an isolated instance settles the rest. ## Related - `auditing-open-redirect-and-forced-navigation` - the redirect allowlist that a parser differential defeats is the sink that skill audits, so URL-parsing trust and open redirect meet on the target check. - `testing-web-cache-attacks` - a header reflected into a cached response is the poisoning primitive that skill treats, and the cache-key analysis is shared. - `hunting-unicode-normalization-and-canonicalization-bypass` - encoded or ambiguous hosts that split check from use are a canonicalization failure that skill analyzes directly. - `hunting-dns-rebinding-and-ssrf-pivots` - a fetch allowlist beaten by a parser differential feeds the server-side request forgery reach that skill covers. - [FINDING-SCHEMA.md](../../FINDING-SCHEMA.md) - source = the attacker-influenced host or ambiguous URL, sink = the link builder, cache key, router, or allowlist parse, evidence = the poisoned link, reflected cache entry, internal route, or allowlist bypass observed on an isolated instance.