Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
##
## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore

# Claude Code
.claude/

# Rider / VS
.idea/
*.DotSettings.user
Expand Down
63 changes: 63 additions & 0 deletions docs/content/docs/smuggling/clte-conn-close.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: "CLTE-CONN-CLOSE"
description: "CLTE-CONN-CLOSE sequence test documentation"
weight: 10
---

| | |
|---|---|
| **Test ID** | `SMUG-CLTE-CONN-CLOSE` |
| **Category** | Smuggling |
| **Type** | Sequence (2 steps) |
| **Scored** | Yes |
| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) |
| **RFC Level** | MUST |
| **Expected** | `400`, or `2xx` + connection close |

## What it does

This is a **sequence test** — it sends multiple requests on the same TCP connection to verify server behavior across the full exchange.

### Step 1: Ambiguous POST (CL+TE)

```http
POST / HTTP/1.1\r\n
Host: localhost:8080\r\n
Content-Length: 5\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
```

A POST with both `Content-Length: 5` and `Transfer-Encoding: chunked`. The chunked body is the `0` terminator (5 bytes), which happens to match the CL value.

### Step 2: Follow-up GET

```http
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
\r\n
```

A normal GET sent on the same connection. This step only executes if the connection is still open after step 1.

## What the RFC says

> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. **Regardless, the server MUST close the connection after responding to such a request** to avoid the potential attacks." — RFC 9112 §6.1

The key word is "regardless" — even if the server correctly processes the request via TE, it **must** close the connection afterward.

## Why it matters

The MUST-close requirement exists because keeping the connection open after a dual CL+TE request creates a window for request smuggling. If the connection stays alive, any leftover bytes (or a pipelined request) could be misinterpreted. This sequence test verifies the close actually happens.

## Verdicts

- **Pass** — Server returns `400` (rejected outright), OR returns `2xx` and closes the connection (step 2 never executes)
- **Fail** — Server returns `2xx` and keeps the connection open (step 2 executes and gets a response)

## Sources

- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1)
- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3)
70 changes: 70 additions & 0 deletions docs/content/docs/smuggling/clte-desync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: "CLTE-DESYNC"
description: "CLTE-DESYNC sequence test documentation"
weight: 13
---

| | |
|---|---|
| **Test ID** | `SMUG-CLTE-DESYNC` |
| **Category** | Smuggling |
| **Type** | Sequence (2 steps) |
| **Scored** | Yes |
| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) |
| **RFC Level** | MUST |
| **Expected** | `400`, or connection close |

## What it does

This is a **sequence test** that detects actual CL.TE request boundary desynchronization — the classic request smuggling attack.

### Step 1: Poison POST (CL=6, TE=chunked, extra byte)

```http
POST / HTTP/1.1\r\n
Host: localhost:8080\r\n
Content-Length: 6\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
X
```

The chunked body terminates at `0\r\n\r\n` (5 bytes), but `Content-Length` claims 6 bytes. The extra `X` byte sits right after the chunked terminator.

- If the server uses **TE**: reads the chunked terminator (5 bytes), body done. `X` is leftover on the wire.
- If the server uses **CL**: reads 6 bytes (`0\r\n\r\nX`), body done.

Either way, `X` may poison the connection.

### Step 2: Follow-up GET

```http
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
\r\n
```

Sent immediately after step 1. If `X` is still on the wire, the server sees `XGET / HTTP/1.1` — a malformed request line that triggers a 400.

## What the RFC says

> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1

The only safe outcomes are rejection (400) or closing the connection. Any other behavior risks desynchronization.

## Why it matters

This test detects **actual request smuggling**, not just RFC non-compliance. If the poison byte `X` merges with the follow-up GET, the server's request boundary parsing is broken. In a real proxy chain, an attacker could replace `X` with a complete smuggled request.

## Verdicts

- **Pass** — Server returns `400` (rejected outright), OR closes the connection (step 2 never executes)
- **Fail** — Step 2 executes and returns `400` (desync confirmed — poison byte merged with GET)
- **Fail** — Step 2 executes and returns `2xx` (MUST-close violated, connection stayed open)

## Sources

- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1)
- [RFC 9112 §11.2](https://www.rfc-editor.org/rfc/rfc9112#section-11.2)
64 changes: 64 additions & 0 deletions docs/content/docs/smuggling/clte-keepalive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
title: "CLTE-KEEPALIVE"
description: "CLTE-KEEPALIVE sequence test documentation"
weight: 12
---

| | |
|---|---|
| **Test ID** | `SMUG-CLTE-KEEPALIVE` |
| **Category** | Smuggling |
| **Type** | Sequence (2 steps) |
| **Scored** | Yes |
| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) |
| **RFC Level** | MUST |
| **Expected** | `400`, or `2xx` + connection close |

## What it does

This is a **sequence test** that verifies the MUST-close requirement still applies even when the client explicitly requests a persistent connection.

### Step 1: Ambiguous POST with keep-alive

```http
POST / HTTP/1.1\r\n
Host: localhost:8080\r\n
Connection: keep-alive\r\n
Content-Length: 5\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
```

A POST with both `Content-Length: 5` and `Transfer-Encoding: chunked`, plus an explicit `Connection: keep-alive` header pressuring the server to maintain the connection.

### Step 2: Follow-up GET

```http
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
\r\n
```

A normal GET sent on the same connection. This step only executes if the connection is still open after step 1.

## What the RFC says

> "**Regardless, the server MUST close the connection after responding to such a request** to avoid the potential attacks." — RFC 9112 §6.1

The word "regardless" means the MUST-close requirement overrides any `Connection: keep-alive` request from the client. The server has no choice — it must close.

## Why it matters

This is the most tempting edge case for servers to get wrong. A server that correctly detects the CL+TE conflict might still honor the client's `keep-alive` request instead of closing. This test specifically targets that logic path.

## Verdicts

- **Pass** — Server returns `400` (rejected outright), OR returns `2xx` and closes the connection despite `keep-alive` (step 2 never executes)
- **Fail** — Server returns `2xx` and honors `keep-alive`, keeping the connection open (step 2 executes and gets a response)

## Sources

- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1)
- [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3)
55 changes: 55 additions & 0 deletions docs/content/docs/smuggling/pipeline-safe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
title: "PIPELINE-SAFE"
description: "PIPELINE-SAFE baseline sequence test documentation"
weight: 15
---

| | |
|---|---|
| **Test ID** | `SMUG-PIPELINE-SAFE` |
| **Category** | Smuggling |
| **Type** | Sequence (2 steps) |
| **Scored** | No |
| **RFC** | [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) |
| **RFC Level** | SHOULD |
| **Expected** | `2xx` + `2xx` |

## What it does

This is a **baseline sequence test** — it sends two clean, unambiguous requests on the same keep-alive connection to verify the server supports normal HTTP/1.1 pipelining.

### Step 1: First GET

```http
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
\r\n
```

### Step 2: Second GET

```http
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
\r\n
```

Both requests are identical, clean, and unambiguous. No smuggling payload.

## What the RFC says

> "A client that supports persistent connections MAY 'pipeline' its requests (i.e., send multiple requests without waiting for each response). A server MAY process a sequence of pipelined requests in parallel if they all have safe methods." — RFC 9112 §9.3

## Why it matters

This test serves as a **control** for the other sequence tests. If a server can't handle two clean GETs on one connection, the results of desync and MUST-close tests are unreliable — failures could be caused by missing pipelining support rather than smuggling vulnerabilities.

## Verdicts

- **Pass** — Both steps return `2xx` (pipelining works correctly)
- **Warn** — Step 1 returns `2xx` but server closes connection before step 2 (no pipelining support)
- **Fail** — Step 1 does not return `2xx`

## Sources

- [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3)
62 changes: 62 additions & 0 deletions docs/content/docs/smuggling/tecl-conn-close.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: "TECL-CONN-CLOSE"
description: "TECL-CONN-CLOSE sequence test documentation"
weight: 11
---

| | |
|---|---|
| **Test ID** | `SMUG-TECL-CONN-CLOSE` |
| **Category** | Smuggling |
| **Type** | Sequence (2 steps) |
| **Scored** | Yes |
| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) |
| **RFC Level** | MUST |
| **Expected** | `400`, or `2xx` + connection close |

## What it does

This is a **sequence test** — it sends multiple requests on the same TCP connection to verify server behavior across the full exchange. It is a mirror of [CLTE-CONN-CLOSE](/Http11Probe/docs/smuggling/clte-conn-close/) with the header order reversed.

### Step 1: Ambiguous POST (TE+CL)

```http
POST / HTTP/1.1\r\n
Host: localhost:8080\r\n
Transfer-Encoding: chunked\r\n
Content-Length: 5\r\n
\r\n
0\r\n
\r\n
```

A POST with `Transfer-Encoding: chunked` listed **before** `Content-Length: 5`. Some parsers treat headers differently depending on order. The chunked body is the `0` terminator (5 bytes), matching the CL value.

### Step 2: Follow-up GET

```http
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
\r\n
```

A normal GET sent on the same connection. This step only executes if the connection is still open after step 1.

## What the RFC says

> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. **Regardless, the server MUST close the connection after responding to such a request** to avoid the potential attacks." — RFC 9112 §6.1

The MUST-close requirement applies regardless of header order. This test verifies servers don't accidentally rely on header ordering when deciding whether to close.

## Why it matters

Some servers process headers in order and may handle `TE, CL` differently from `CL, TE`. If a server only triggers its MUST-close logic when `Content-Length` appears first, the reversed order could bypass the protection, leaving the connection open for smuggling.

## Verdicts

- **Pass** — Server returns `400` (rejected outright), OR returns `2xx` and closes the connection (step 2 never executes)
- **Fail** — Server returns `2xx` and keeps the connection open (step 2 executes and gets a response)

## Sources

- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1)
66 changes: 66 additions & 0 deletions docs/content/docs/smuggling/tecl-desync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: "TECL-DESYNC"
description: "TECL-DESYNC sequence test documentation"
weight: 14
---

| | |
|---|---|
| **Test ID** | `SMUG-TECL-DESYNC` |
| **Category** | Smuggling |
| **Type** | Sequence (2 steps) |
| **Scored** | Yes |
| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) |
| **RFC Level** | MUST |
| **Expected** | `400`, or connection close |

## What it does

This is a **sequence test** that detects TE.CL request boundary desynchronization — the reverse of the classic CL.TE smuggling attack.

### Step 1: Poison POST (TE terminates early, CL=30)

```http
POST / HTTP/1.1\r\n
Host: localhost:8080\r\n
Transfer-Encoding: chunked\r\n
Content-Length: 30\r\n
\r\n
0\r\n
\r\n
X
```

The `Transfer-Encoding: chunked` body terminates at `0\r\n\r\n` (5 bytes), but `Content-Length` claims 30 bytes. The extra `X` sits after the chunked terminator.

- If the server uses **TE**: reads the chunked terminator (5 bytes), body done. Still expects 25 more bytes per CL — `X` and any subsequent data become part of the expected body or a new request.
- If the server uses **CL**: waits for 30 bytes total, which never arrive (timeout).

### Step 2: Follow-up GET

```http
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
\r\n
```

Sent immediately after step 1. If the server used TE and left `X` on the wire, it sees `XGET / HTTP/1.1` — a malformed request that triggers a 400.

## What the RFC says

> "**Regardless, the server MUST close the connection after responding to such a request** to avoid the potential attacks." — RFC 9112 §6.1

## Why it matters

In a proxy chain where the front-end uses CL and the back-end uses TE, this pattern allows an attacker to smuggle a request by placing it after the chunked terminator but within the CL-declared body. This test verifies the server doesn't leave the connection in an ambiguous state.

## Verdicts

- **Pass** — Server returns `400` (rejected outright), OR closes the connection (step 2 never executes)
- **Fail** — Step 2 executes and returns `400` (desync confirmed — poison byte merged with GET)
- **Fail** — Step 2 executes and returns `2xx` (MUST-close violated, connection stayed open)

## Sources

- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1)
- [RFC 9112 §11.2](https://www.rfc-editor.org/rfc/rfc9112#section-11.2)
Loading
Loading