Introduction
HTTP/2 introduced multiplexed streams, binary framing, and HPACK header compression to speed up web traffic. These same features also give attackers a new playground for request smuggling-the art of making a single client request appear as multiple or altered requests to different servers in a reverse-proxy chain. Understanding how HTTP/2 frames, header compression, and framing rules interact is essential for detecting and preventing smuggling attacks.
Prerequisites
- Basic knowledge of the HTTP/1.1 request/response model.
- Familiarity with the TCP/IP stack and how proxies forward traffic.
- Understanding of HPACK header compression basics.
- Experience with network debugging tools (Wireshark, tcpdump).
Core Concepts
Before diving into smuggling vectors, let’s review the building blocks that enable them.
HTTP/2 Frame Types and Sequencing
HTTP/2 is a binary protocol built around a stream-based framing layer. Each frame starts with a 9-byte header:
<len> // Length (24 bits)
<type> // Type (8 bits)
<flags> // Flags (8 bits)
<R + Stream Identifier> // 32 bits
Key frame types relevant to request smuggling:
HEADERS- carries the request/response headers and may include theEND_HEADERSflag.CONTINUATION- used when a header block spans multiple frames; must follow aHEADERSframe on the same stream.DATA- payload of the request body; may be split arbitrarily.RST_STREAM- aborts a stream; useful for crafting malformed streams.
Because HTTP/2 streams are multiplexed over a single connection, mis-ordered or overlapping frames can lead to divergent interpretations by upstream and downstream proxies.
HPACK Header Compression Mechanics
HPACK compresses header blocks using a dynamic and static table. Each header field is represented as an index or a literal. The compression context is maintained per connection, not per stream, which means that a malicious client can exploit the shared state:
- Send a
HEADERSframe that references a header field by index. - On a subsequent stream, send a
HEADERSframe that re-uses the same index but changes the value, relying on the server to merge or replace entries incorrectly.
Moreover, HPACK allows a client to send a CONTINUATION frame that is not properly linked to a preceding HEADERS frame, causing the receiver to interpret the header block as belonging to a different stream.
Common Smuggling Vectors (CL.TE, TE.CL, TE.TE)
These acronyms describe the placement of Content-Length (CL) and Transfer-Encoding (TE) headers across the request body and the effect on server parsing:
- CL.TE -
Content-Lengthappears beforeTransfer-Encodingin the header block. - TE.CL -
Transfer-Encodingappears beforeContent-Length. - TE.TE - two
Transfer-Encodingheaders, one of which is malformed or uses non-standard tokens.
When these headers are split across HTTP/2 frames or mis-ordered via HPACK, a proxy that implements the HTTP/1.1 semantics may interpret the body length differently from the back-end, resulting in a smuggled request.
Attack Flow and Payload Construction
Below is a step-by-step construction of a typical HTTP/2 smuggling payload that exploits a CL.TE vector.
- Open a raw TCP connection to the front-end proxy on port 443 (TLS termination may be bypassed with a man-in-the-middle or by using a client that supports ALPN).
- Send a
SETTINGSframe to negotiate max header size and frame size. - Craft a
HEADERSframe on stream 1 containing aContent-Length: 5header and theTransfer-Encoding: chunkedheader but set theEND_HEADERSflag only after aCONTINUATIONframe that contains the actual header values. - Send a
DATAframe with five bytes of payload followed by aDATAframe that starts a chunked transfer (4 abcd 0). The front-end will see the body as 5 bytes and close the stream, while the back-end interprets the chunked body and processes the smuggled request.
In practice, you can automate this with h2c or custom netcat scripts. A minimal Python example follows.
import socket, struct
def send_frame(sock, typ, flags, stream_id, payload=b''): length = len(payload) header = struct.pack('!I', length)[1:] # 24 bits header += struct.pack('!B', typ) header += struct.pack('!B', flags) header += struct.pack('!I', stream_id & 0x7FFFFFFF) # clear reserved bit sock.sendall(header + payload)
s = socket.create_connection(('proxy.example.com', 443))
# SETTINGS frame (typ=4)
send_frame(s, 4, 0, 0, b'\x00\x00\x00\x00\x00\x00\x00\x00')
# HEADERS frame (typ=1)
# ... (build HPACK header block and send)
The full implementation is beyond the scope of this guide, but the above skeleton illustrates how to manipulate frame boundaries.
Detection Techniques and Tooling
- Network traffic analysis - Use Wireshark to inspect the frame order and header block boundaries. Look for
CONTINUATIONframes that are not immediately following aHEADERSframe on the same stream. - Proxy logs - Many HTTP/2 aware proxies log stream IDs and frame types. Correlate anomalies with downstream request counts.
- Custom IDS signatures - Deploy Suricata or Zeek scripts that flag mismatched
Content-LengthandTransfer-Encodingacross frames. - Fuzzing tools -
h2specandrfc7540-fuzzercan generate malformed frame sequences that exercise edge-cases.
Practical Lab Setup and Exploitation Steps
- Set up a three-box environment: Client (Linux VM), Front-end Proxy (NGINX 1.19+ with HTTP/2 enabled), Back-end Server (Apache 2.4).
- Configure the front-end to proxy HTTP/2 to the back-end over HTTP/1.1.
- Use
h2loador a custom script to send the smuggling payload described earlier. - Verify that the back-end receives two distinct requests: one normal, one smuggled.
- Enable logging on both proxies to capture the divergent interpretations.
Practical Examples
Case Study 1: Content-Length: 0 + Transfer-Encoding: chunked smuggling via a single HEADERS frame that is split across two CONTINUATION frames. The front-end treats the request as an empty body, while the back-end reads the chunked data and executes unintended logic.
Case Study 2: Double Transfer-Encoding headers, one of which is gzip, the other chunked. The front-end honors only the first header, discarding the rest; the back-end processes the entire header block, leading to a decompression bomb.
Tools & Commands
h2spec -t 10- run HTTP/2 conformance tests.h2load -n 100 -c 10 - generate load.</li> <li><code>tcpdump -i eth0 -nn -s 0 -vvv 'tcp port 443'- capture frames.suricata -c suricata.yaml -v- start IDS with custom HTTP/2 rules.- Custom Python script (see snippet above).
Defense & Mitigation
- Strict header parsing - Ensure the front-end validates the
Content-LengthandTransfer-Encodingheaders per RFC 9110 before forwarding. - Disable HTTP/2 for sensitive services - If smuggling is a high risk, consider falling back to HTTP/1.1 for critical endpoints.
- Implement header size limits - Configure
client_header_buffer_sizeandlarge_client_header_buffersin NGINX to prevent oversized header blocks. - Use TLS mutual authentication - Mitigate man-in-the-middle attacks that may inject smuggling frames.
- Continuous monitoring - Deploy logging and alerting for anomalous frame sequences.
Common Mistakes
- Assuming all proxies forward HTTP/2 frames verbatim; many drop
CONTINUATIONframes. - Overlooking HPACK table reuse; a malicious client can corrupt the shared state.
- Ignoring the fact that
Content-Lengthmay be overridden byTransfer-Encodingper RFC 9110. - Underestimating the need for TLS validation; plain HTTP/2 can be exploited over unencrypted connections.
Real-World Impact
Smuggling attacks can lead to:
- Bypassing authentication - injecting a second request that re-uses session cookies.
- Cache poisoning - manipulating the cache key by altering the request body.
- Data exfiltration - embedding payloads in a smuggled request that bypasses WAF rules.
Recent studies show that 12% of corporate proxies had mis-configured HTTP/2 handling, exposing them to smuggling. As HTTP/2 adoption continues, the attack surface will grow unless mitigated.
Practice Exercises
- Build a minimal HTTP/2 client in Python that sends a
HEADERSframe followed by aCONTINUATIONframe on a different stream ID. - Using Wireshark, capture a smuggled request and annotate the frame sequence that led to the split.
- Configure NGINX to reject
Transfer-Encodingheaders that appear afterContent-Lengthand test with the provided script. - Write a Suricata rule that flags a
HEADERSframe withEND_HEADERSmissing on the same stream.
Further Reading
- RFC 7540 - HTTP/2: A Framework for Multiplexed Connections
- RFC 9110 - HTTP Semantics
- OWASP HTTP/2 Security Cheat Sheet
- Suricata HTTP/2 Ruleset
- NGINX HTTP/2 Configuration Guide
Summary
HTTP/2’s binary framing and HPACK compression give attackers powerful new smuggling vectors. By mastering frame sequencing, header compression mechanics, and the CL.TE/TE.CL/TE.TE patterns, you can craft attacks that split requests across a proxy chain. Detecting smuggling requires careful traffic inspection and IDS rule tuning, while mitigation hinges on strict header validation, disabling HTTP/2 where necessary, and continuous monitoring.