Host Header Poisoning
$_SERVER['HTTP_HOST'] and $_SERVER['SERVER_NAME'] are derived from the
HTTP Host header (and on some webserver configs from
X-Forwarded-Host). Both come from the client. Any place those values land
in a URL, redirect, email, cache key, or CORS origin is a vulnerability.
Why
The canonical exploit is password reset:
$reset_link = "https://" . $_SERVER['HTTP_HOST'] . "/reset?token=$token";
mail($user_email, "Reset your password", $reset_link);Attacker POST /forgot with Host: attacker.com, victim receives an
email with https://attacker.com/reset?token=…, clicks it, attacker
captures the token, takes over the account.
Other sinks of the same primitive:
- OAuth
redirect_uridefaults derived from Host - Absolute URLs in transactional emails (image embeds, “click here” CTAs, unsubscribe links)
- Cache keys → web cache poisoning (response with attacker host served to other users)
- CORS —
Access-Control-Allow-Origin:echoed back from request origin / host - Canonical / OG meta tags → SEO poisoning, social-share hijack
- Routing decisions for multi-tenant apps
Search patterns
rg -n 'HTTP_HOST|SERVER_NAME|HTTP_X_FORWARDED_HOST' --type=php
# Common framework helpers that read Host under the hood
rg -n '->getHost\(\)|->getSchemeAndHttpHost\(\)|url\(|route\(|asset\(' --type=php
# Mail and URL construction
rg -n 'mail\(|Mail::|->send\(' --type=phpTest inputs
Single Host swap:
GET /forgot HTTP/1.1
Host: attacker.comForwarded-Host header (works if the app trusts it):
GET /forgot HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.comTwo Host headers (some frontends pass the second through):
GET /forgot HTTP/1.1
Host: target.com
Host: attacker.comHost header injection (CRLF in Host):
Host: target.com\r\nX-Forwarded-Host: attacker.comSame-domain subdomain abuse:
Host: target.com.attacker.com— naive.target.comsubstring matchers miss thisHost: attacker.com#@target.com(URL parsers vary)Host: target.com:80@attacker.com(userinfo confusion)
Other relevant headers commonly trusted:
X-Forwarded-HostX-Original-URL/X-Rewrite-URL(Symfony, IIS rewrite)X-Forwarded-Server
Audit focus
For every read of HTTP_HOST / SERVER_NAME / HTTP_X_FORWARDED_HOST:
- Where does the value land?
- URL in an email → password reset takeover
- OAuth callback / SSO redirect → token theft
- Cache key → cache poisoning across users
- CORS response → cross-origin data leakage
- HTML
<a>href / OG tag → phishing
- Is the host validated against an allowlist?
- Is the value used pre-auth? Password reset, signup confirmation, MFA reset — these are the high-value pre-auth sinks.
- Webserver config — does Apache / Nginx pass through unexpected Host values, or does it reject them at the edge?
- Multi-tenant routing — if
Hostselects the tenant, is the value normalised before being used as a cache / session key?
Frameworks often expose Host through helper methods (request->getHost,
url(), route()) that look harmless. The footgun is exactly the
same — they read HTTP_HOST under the hood. Look at the framework’s
configuration: Symfony has trusted_hosts; Laravel does not (you must
validate manually).
Fix
Hardcode the canonical host in app config and reject unknown values at the boundary:
$allowed_hosts = ['target.com', 'www.target.com'];
$host = $_SERVER['HTTP_HOST'] ?? '';
if (!in_array($host, $allowed_hosts, true)) {
http_response_code(400);
exit;
}Better: don’t trust Host at all for URL construction. Use a configured
APP_URL env var:
$base = rtrim(getenv('APP_URL'), '/');
$reset_link = "$base/reset?token=$token";At the webserver:
server {
server_name target.com www.target.com;
if ($host !~* ^(target\.com|www\.target\.com)$) { return 444; }
…
}