Attacker-influenced HTTP headers (Host, Location, Referer) are used to construct a redirect response without destination validation.
TL;DR
Location constructionHost: evil.com → reset email contains https://evil.com/reset?token=TOKENX-Forwarded-Host: evil.com + uncached host in cache key → all users served attacker JSHost headerHeader-based open redirect exploits the common practice of using HTTP request headers — particularly Host and X-Forwarded-Host — to construct absolute URLs in responses. When an application builds a redirect URL as https:// + request.headers["host"] + /reset?token=XYZ, an attacker who can control the Host header controls the redirect destination.
The vulnerability falls under CWE-601 and OWASP A01:2021 (Broken Access Control). It differs from parameter-based open redirect in that the malicious input arrives in a header rather than a query parameter — it is invisible in the URL, and many URL-focused security controls and WAF rules miss it entirely. The most impactful exploitation path is password reset poisoning — an attacker uses the Host header injection to poison reset links sent to victim email addresses, then intercepts the reset token to achieve account takeover.
Web cache poisoning is a related but distinct impact: if the response containing the attacker-controlled URL is cached and served to other users, the attack scales from targeting individuals to compromising all users who receive the cached response.
Password reset poisoning via Host header is the highest-impact exploitation path because it converts a one-time network interaction into persistent account takeover:
Host header — e.g., https://[Host]/reset?token=[TOKEN].Host: evil.com (or X-Forwarded-Host: evil.com). No authentication required — the reset endpoint is public.https://evil.com/reset?token=VALID_TOKEN.evil.com. The attacker logs the token.POST /reset?token=VALID_TOKEN&new_password=attacker_pw to the legitimate server. Account takeover complete.The attack is fully passive from the victim's perspective: they receive a legitimate-looking reset email from the correct sender address, click a link, and their account is taken over — even if they never enter any credentials on the attacker's site.
Web cache poisoning via X-Forwarded-Host scales the attack from targeting one user to compromising all users of a cached resource:
X-Forwarded-Host (canonical links, JavaScript sources, API endpoints, or redirect URLs).X-Forwarded-Host: evil.com and verifies the response includes evil.com in a URL.X-Forwarded-Host in the cache key (common in misconfigured CDN setups), the poisoned response may be cached.evil.com — attacker-controlled code runs in the context of the legitimate domain.The blast radius depends on the cache TTL and traffic volume. A poisoned response with max-age=3600 served to a high-traffic path can affect thousands of users before the cache expires.
Beyond Host and X-Forwarded-Host, several other headers can cause redirect injection depending on the framework:
Forwarded: host=evil.com (RFC 7239) — modern proxy-forwarding header, some frameworks prefer it over X-Forwarded-HostX-Original-URL: /admin — Symfony and some Django configurations use this to override the request URL path; combined with a redirect it can produce attacker-controlled Location valuesX-Rewrite-URL — older IIS and Apache mod_rewrite configurations; similar impact to X-Original-URLX-Forwarded-Server — less common, but some Java application servers use it for URL constructionFront-End-HTTPS: on — Microsoft header for scheme detection; if the application uses this to determine http:// vs https:// in URL construction, it can be abused to generate mixed-content or redirect to HTTPThe vulnerable server-side code pattern:
# VULNERABLE Flask — constructs reset URL from Host header
from flask import request
@app.route("/forgot-password", methods=["POST"])
def forgot_password():
email = request.form["email"]
token = generate_reset_token(email)
# VULNERABLE: uses request.host (attacker-controlled)
reset_url = f"https://{request.host}/reset?token={token}"
send_email(email, f"Click here to reset: {reset_url}")
return "Email sent", 200# Attacker's malicious request
POST /forgot-password HTTP/1.1
Host: evil.com
Content-Type: application/x-www-form-urlencoded
email=victim@target.com
# Victim receives email:
# "Click here to reset: https://evil.com/reset?token=VALID_TOKEN_HERE"# VULNERABLE Node.js/Express — uses req.hostname
app.post("/forgot-password", async (req, res) => {
const token = await createResetToken(req.body.email);
const resetUrl = `https://${req.hostname}/reset/${token}`; // VULNERABLE
await sendResetEmail(req.body.email, resetUrl);
res.json({ success: true });
});| Technique | Header Used | Mechanism | Impact |
|---|---|---|---|
| Password reset poisoning | Host or X-Forwarded-Host | Reset link contains attacker domain | Account takeover |
| Web cache poisoning | X-Forwarded-Host | Response with attacker domain cached for other users | Mass XSS/redirection |
| SSRF via Host | Host: 169.254.169.254 | Internal routing to metadata service | Cloud credential theft |
| API redirect poisoning | Host | API response redirect points to attacker | Token theft via API client |
| OAuth redirect injection | X-Forwarded-Host | OAuth redirect URL construction uses X-FH | Authorization code theft |
# Attacker sends (assumes X-Forwarded-Host not in cache key):
GET /home HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com
# Vulnerable response (cached by CDN/Varnish):
HTTP/1.1 200 OK
Cache-Control: max-age=3600
Content-Type: text/html
<script src="https://evil.com/app.js"></script>
<link rel="canonical" href="https://evil.com/home">If the cache key includes only Host (not X-Forwarded-Host), this poisoned response is served to all subsequent users requesting /home from target.com. Their browsers load evil.com/app.js — attacker-controlled JavaScript executing in the context of target.com.
CVE-2024-46452 — OpenCart-like Platform (CVSS 8.1)
This CMS platform used the HTTP Host header directly in password reset email construction. Attackers submitted password reset requests for victim accounts with Host: evil.com, causing the reset email to contain https://evil.com/index.php?route=account/reset&code=TOKEN. When victims clicked the link, their reset tokens were logged on evil.com. The attacker then used the token to set a new password and take over the account. The CVSS 8.1 rating (High) reflects the full account takeover impact.
CVE-2024-40686 — IBM SmartCloud Analytics (CVSS 6.5)
IBM SmartCloud Analytics was vulnerable to Host header injection causing cache and session-level redirect manipulation. The application used the Host header to construct URLs in API responses and redirects. Attackers could manipulate cached content and session redirects. Fixed in the IBM security advisory by hardcoding the application base URL from configuration rather than HTTP headers.
PortSwigger Research — Password Reset Poisoning (Canonical)
PortSwigger's research on Host header attacks identifies password reset poisoning as the most commonly exploited Host header vulnerability in real-world applications. The pattern appears across PHP frameworks (where $_SERVER['HTTP_HOST'] is attacker-controlled), Django (without ALLOWED_HOSTS configured), Ruby on Rails (without config.hosts), and Express.js (where req.hostname trusts proxy headers without trusted proxy configuration).
X-Forwarded-Host: YOUR_COLLABORATOR_URL. Submit a password reset for an account you control.# Test Host override
GET / HTTP/1.1
Host: canary.oast.fun
X-Forwarded-Host: canary.oast.fun
X-Original-URL: /# Burp Param Miner — Host header fuzzing
# Enable via Burp Extensions → Param Miner → "Guess headers" on the target
# nuclei Host header injection templates
nuclei -u https://target.com -t http/misconfiguration/host-header-injection.yaml
# httpx Host header check
echo "https://target.com" | httpx -H "Host: canary.oast.fun" -silent -match-string "canary.oast.fun"
# Manual curl test for X-Forwarded-Host reflection
curl -sI -H "X-Forwarded-Host: canary.oast.fun" https://target.com/forgot-password | grep -i "location\|canary"BreachVex detects Host header injection by sending password reset requests with a unique out-of-band canary host in X-Forwarded-Host and monitoring for DNS/HTTP callbacks. A confirmed finding requires the server to either include the canary in a redirect Location header, or for the out-of-band canary to receive an HTTP request originating from the server (email click simulation not required — an out-of-band callback is sufficient for confirmation).
# Flask — GOOD
import os
BASE_URL = os.environ["APP_BASE_URL"] # e.g., "https://app.example.com"
@app.route("/forgot-password", methods=["POST"])
def forgot_password():
email = request.form["email"]
token = generate_reset_token(email)
# GOOD: base URL from environment, never from request headers
reset_url = f"{BASE_URL}/reset?token={token}"
send_email(email, f"Click here to reset: {reset_url}")
return "Email sent", 200// Express.js — GOOD
const BASE_URL = process.env.APP_BASE_URL; // https://app.example.com
app.post("/forgot-password", async (req, res) => {
const token = await createResetToken(req.body.email);
// GOOD: hardcoded base URL — never use req.hostname or req.headers.host
const resetUrl = `${BASE_URL}/reset/${token}`;
await sendResetEmail(req.body.email, resetUrl);
res.json({ success: true });
});# settings.py
ALLOWED_HOSTS = ["app.example.com", "www.example.com"]
# Django rejects requests where Host is not in this list with 400 Bad Request
# This prevents Host header injection at the framework levelserver {
listen 443 ssl;
server_name app.example.com;
location / {
# Override X-Forwarded-Host with the validated server name
# This prevents clients from injecting arbitrary X-Forwarded-Host values
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host; # Override with validated value
proxy_pass http://app_backend;
}
}Express.js's req.hostname trusts the X-Forwarded-Host header when app.set("trust proxy", true) is configured — and trusts any proxy in that case. For production deployments, set app.set("trust proxy", "loopback, 10.0.0.0/8") to only trust known proxy IP ranges. Never set trust proxy to true in production.
Header-based open redirect occurs when an application constructs a redirect URL using attacker-controlled HTTP headers — primarily Host or X-Forwarded-Host — rather than a hardcoded base URL. The application reads the Host header from the request and uses it to build an absolute URL in the Location response, allowing an attacker to direct users to an attacker-controlled domain by setting a custom Host or X-Forwarded-Host header.
Many frameworks construct absolute URLs using request.headers.host or similar: Location: https://{HOST}/reset?token=XYZ. If an attacker sends Host: evil.com, the Location header becomes Location: https://evil.com/reset?token=XYZ. The password reset link in the subsequent email contains evil.com — when the victim clicks it, the token is delivered to the attacker's server.
Password reset poisoning is the most impactful Host header attack. The application generates a reset link using the Host header: https://HOST/reset?token=TOKEN. An attacker submits a password reset for a victim's account with a custom Host: evil.com. The victim receives an email with a reset link pointing to evil.com. When clicked, the token is logged on evil.com. The attacker uses the token to reset the victim's password and takes over the account.
X-Forwarded-Host is a non-standard header that reverse proxies and CDNs add to indicate the original Host header sent by the client. Many frameworks prefer X-Forwarded-Host over Host when constructing absolute URLs. An attacker who can control X-Forwarded-Host (on an endpoint that does not properly configure trusted proxy networks) can inject an arbitrary hostname. Unlike Host (which proxy infrastructure often normalizes), X-Forwarded-Host may be passed through without modification.
CVE-2024-46452 (CVSS 8.1) affected an OpenCart-like e-commerce platform. The password reset flow used the Host header to construct the reset link URL. An attacker submitted a password reset for any account with a custom Host header, causing the reset email to contain a link to the attacker's domain. When the victim clicked the link, the reset token was delivered to the attacker. This is the canonical password reset poisoning vulnerability pattern.
If a caching layer does not include the Host or X-Forwarded-Host header in the cache key, but the application uses those headers to build URLs in the response (JavaScript sources, redirect URLs, canonical links), an attacker can poison the cache. The attacker sends a request with X-Forwarded-Host: evil.com; the response includes a script tag loading from evil.com. If cached, subsequent users receive the poisoned response — their browsers load attacker-controlled JavaScript.
Use Burp Suite's Param Miner extension — it automatically fuzzes Host, X-Forwarded-Host, X-Original-URL, X-Rewrite-URL, and related headers. Manually: send a request with X-Forwarded-Host: canary.oast.fun and a password reset email trigger. Check whether the reset email contains canary.oast.fun. Also check the response body for reflected host values. Use Burp Collaborator for OOB confirmation if no email access.
X-Forwarded-Host is the most common alternative. Others that some frameworks use: X-Original-URL (Symfony), X-Rewrite-URL (older IIS), Forwarded: host= (RFC 7239), Front-End-HTTPS (Microsoft), X-Real-IP (nginx), CF-Connecting-IP (Cloudflare). The key is whether the application uses any of these to construct absolute URLs in redirects, emails, or API responses without validation.
Nginx set_real_ip_from and real_ip_header directives define which IP ranges can be trusted to supply X-Forwarded-For and related headers. However, X-Forwarded-Host is not automatically restricted by these directives — it requires explicit configuration to strip or override. The correct pattern: in nginx.conf, always set proxy_set_header Host $host to forward the validated Host, and never forward X-Forwarded-Host from untrusted clients.
Django's ALLOWED_HOSTS setting defines an explicit allowlist of valid hostnames. If the Host header is not in ALLOWED_HOSTS, Django returns a 400 Bad Request before any application code runs. This prevents Host header injection at the framework level. The equivalent in Flask is not built-in — developers must configure SERVER_NAME or implement custom middleware. Express.js has no built-in Host validation.