Lion Cipher Lab
Exploit and fix Host header injection (password-reset poisoning and web cache poisoning) in your own lab
- AppSec / DevSecOps
- Intermediate
- ~50 min
- Free · no account required
Why it matters
Almost every web app has to answer the question 'what is my own address?' — to build the link in a password-reset email, to set an absolute redirect, to load its own scripts and images. The lazy answer is to read it from the incoming request's HOST header, because that header names the site the browser asked for (Host: yourbank.com). The catastrophic detail is that the Host header is not trustworthy: it is just a line the CLIENT puts in the request, and a client can put anything there. An attacker sending requests by hand — with curl, a proxy, or a script — sets Host to whatever they like. So…
What you'll learn
- explaining HOST HEADER INJECTION (HTTP Host header attacks): the HTTP Host request header is attacker-controlled — it is just a header the CLIENT sends — yet apps routinely trust it to build absolute URLs and route requests, so trusting it lets an attacker choose the host the app 'thinks' it is
- building a tiny dependency-free Node app whose /forgot-password endpoint constructs a password-reset link from req.headers.host (honouring X-Forwarded-Host) and 'emails' it by printing it
- PASSWORD-RESET POISONING: sending the reset request with a spoofed Host / X-Forwarded-Host header so the generated reset link points at the attacker's domain — when the victim clicks it, the secret reset token is delivered to the attacker and the account is taken over
- WEB CACHE POISONING: reflecting an unkeyed Host / X-Forwarded-Host value into a response (an absolute resource URL) and reasoning about how a shared cache then serves that one poisoned response to every other user — amplifying a single request into a site-wide attack
- seeing WHY the naive fixes fail: trusting X-Forwarded-Host 'because the proxy sets it' (the client can send it directly to the app); an includes('yourdomain.com') substring check (beaten by yourdomain.com.evil.example and evil.example/?x=yourdomain.com); and a too-loose subdomain/port rule (endsWith('.yourdomain.com') waves through any attacker-controlled subdomain you never provisioned)
- fixing it the durable way: NEVER derive a security-sensitive absolute URL from the request Host — pin a CANONICAL base URL / origin from server config; if you must validate, match against an EXACT allowlist of permitted hosts and return 400 otherwise; drop or ignore X-Forwarded-Host unless it comes from a trusted proxy you control; and for reset links, send a relative link or a hardcoded origin
- internalising the judgment: this bites multi-tenant apps, anything behind a proxy, and anything that emails links; a substring or suffix check can always be tricked by a hostname the attacker registers; the only reliable base is one YOU configure, never one the request tells you
Steps in this lab
- The vocabulary: the Host header is client input, not server truth
- Set up: confirm Node, make a folder
- Build the VULNERABLE app (it builds the reset link from the Host header)
- Exploit it (1): password-reset poisoning via the Host header
- Exploit it (2): the same bug via X-Forwarded-Host, and web cache poisoning
- Why the tempting fixes fail — feel the bypasses yourself
- Fix it right — a canonical base URL, an exact allowlist, and drop X-Forwarded-Host
- What you did — and the reflex to keep