Skip to content

Commit 2b67c94

Browse files
committed
Sequence tests feature
1 parent a78dc6b commit 2b67c94

20 files changed

Lines changed: 1028 additions & 11 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
##
88
## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore
99

10+
# Claude Code
11+
.claude/
12+
1013
# Rider / VS
1114
.idea/
1215
*.DotSettings.user
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
title: "CLTE-CONN-CLOSE"
3+
description: "CLTE-CONN-CLOSE sequence test documentation"
4+
weight: 10
5+
---
6+
7+
| | |
8+
|---|---|
9+
| **Test ID** | `SMUG-CLTE-CONN-CLOSE` |
10+
| **Category** | Smuggling |
11+
| **Type** | Sequence (2 steps) |
12+
| **Scored** | Yes |
13+
| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) |
14+
| **RFC Level** | MUST |
15+
| **Expected** | `400`, or `2xx` + connection close |
16+
17+
## What it does
18+
19+
This is a **sequence test** — it sends multiple requests on the same TCP connection to verify server behavior across the full exchange.
20+
21+
### Step 1: Ambiguous POST (CL+TE)
22+
23+
```http
24+
POST / HTTP/1.1\r\n
25+
Host: localhost:8080\r\n
26+
Content-Length: 5\r\n
27+
Transfer-Encoding: chunked\r\n
28+
\r\n
29+
0\r\n
30+
\r\n
31+
```
32+
33+
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.
34+
35+
### Step 2: Follow-up GET
36+
37+
```http
38+
GET / HTTP/1.1\r\n
39+
Host: localhost:8080\r\n
40+
\r\n
41+
```
42+
43+
A normal GET sent on the same connection. This step only executes if the connection is still open after step 1.
44+
45+
## What the RFC says
46+
47+
> "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
48+
49+
The key word is "regardless" — even if the server correctly processes the request via TE, it **must** close the connection afterward.
50+
51+
## Why it matters
52+
53+
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.
54+
55+
## Verdicts
56+
57+
- **Pass** — Server returns `400` (rejected outright), OR returns `2xx` and closes the connection (step 2 never executes)
58+
- **Fail** — Server returns `2xx` and keeps the connection open (step 2 executes and gets a response)
59+
60+
## Sources
61+
62+
- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1)
63+
- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
title: "CLTE-DESYNC"
3+
description: "CLTE-DESYNC sequence test documentation"
4+
weight: 13
5+
---
6+
7+
| | |
8+
|---|---|
9+
| **Test ID** | `SMUG-CLTE-DESYNC` |
10+
| **Category** | Smuggling |
11+
| **Type** | Sequence (2 steps) |
12+
| **Scored** | Yes |
13+
| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) |
14+
| **RFC Level** | MUST |
15+
| **Expected** | `400`, or connection close |
16+
17+
## What it does
18+
19+
This is a **sequence test** that detects actual CL.TE request boundary desynchronization — the classic request smuggling attack.
20+
21+
### Step 1: Poison POST (CL=6, TE=chunked, extra byte)
22+
23+
```http
24+
POST / HTTP/1.1\r\n
25+
Host: localhost:8080\r\n
26+
Content-Length: 6\r\n
27+
Transfer-Encoding: chunked\r\n
28+
\r\n
29+
0\r\n
30+
\r\n
31+
X
32+
```
33+
34+
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.
35+
36+
- If the server uses **TE**: reads the chunked terminator (5 bytes), body done. `X` is leftover on the wire.
37+
- If the server uses **CL**: reads 6 bytes (`0\r\n\r\nX`), body done.
38+
39+
Either way, `X` may poison the connection.
40+
41+
### Step 2: Follow-up GET
42+
43+
```http
44+
GET / HTTP/1.1\r\n
45+
Host: localhost:8080\r\n
46+
\r\n
47+
```
48+
49+
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.
50+
51+
## What the RFC says
52+
53+
> "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
54+
55+
The only safe outcomes are rejection (400) or closing the connection. Any other behavior risks desynchronization.
56+
57+
## Why it matters
58+
59+
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.
60+
61+
## Verdicts
62+
63+
- **Pass** — Server returns `400` (rejected outright), OR closes the connection (step 2 never executes)
64+
- **Fail** — Step 2 executes and returns `400` (desync confirmed — poison byte merged with GET)
65+
- **Fail** — Step 2 executes and returns `2xx` (MUST-close violated, connection stayed open)
66+
67+
## Sources
68+
69+
- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1)
70+
- [RFC 9112 §11.2](https://www.rfc-editor.org/rfc/rfc9112#section-11.2)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
title: "CLTE-KEEPALIVE"
3+
description: "CLTE-KEEPALIVE sequence test documentation"
4+
weight: 12
5+
---
6+
7+
| | |
8+
|---|---|
9+
| **Test ID** | `SMUG-CLTE-KEEPALIVE` |
10+
| **Category** | Smuggling |
11+
| **Type** | Sequence (2 steps) |
12+
| **Scored** | Yes |
13+
| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) |
14+
| **RFC Level** | MUST |
15+
| **Expected** | `400`, or `2xx` + connection close |
16+
17+
## What it does
18+
19+
This is a **sequence test** that verifies the MUST-close requirement still applies even when the client explicitly requests a persistent connection.
20+
21+
### Step 1: Ambiguous POST with keep-alive
22+
23+
```http
24+
POST / HTTP/1.1\r\n
25+
Host: localhost:8080\r\n
26+
Connection: keep-alive\r\n
27+
Content-Length: 5\r\n
28+
Transfer-Encoding: chunked\r\n
29+
\r\n
30+
0\r\n
31+
\r\n
32+
```
33+
34+
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.
35+
36+
### Step 2: Follow-up GET
37+
38+
```http
39+
GET / HTTP/1.1\r\n
40+
Host: localhost:8080\r\n
41+
\r\n
42+
```
43+
44+
A normal GET sent on the same connection. This step only executes if the connection is still open after step 1.
45+
46+
## What the RFC says
47+
48+
> "**Regardless, the server MUST close the connection after responding to such a request** to avoid the potential attacks." — RFC 9112 §6.1
49+
50+
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.
51+
52+
## Why it matters
53+
54+
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.
55+
56+
## Verdicts
57+
58+
- **Pass** — Server returns `400` (rejected outright), OR returns `2xx` and closes the connection despite `keep-alive` (step 2 never executes)
59+
- **Fail** — Server returns `2xx` and honors `keep-alive`, keeping the connection open (step 2 executes and gets a response)
60+
61+
## Sources
62+
63+
- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1)
64+
- [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
title: "PIPELINE-SAFE"
3+
description: "PIPELINE-SAFE baseline sequence test documentation"
4+
weight: 15
5+
---
6+
7+
| | |
8+
|---|---|
9+
| **Test ID** | `SMUG-PIPELINE-SAFE` |
10+
| **Category** | Smuggling |
11+
| **Type** | Sequence (2 steps) |
12+
| **Scored** | No |
13+
| **RFC** | [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) |
14+
| **RFC Level** | SHOULD |
15+
| **Expected** | `2xx` + `2xx` |
16+
17+
## What it does
18+
19+
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.
20+
21+
### Step 1: First GET
22+
23+
```http
24+
GET / HTTP/1.1\r\n
25+
Host: localhost:8080\r\n
26+
\r\n
27+
```
28+
29+
### Step 2: Second GET
30+
31+
```http
32+
GET / HTTP/1.1\r\n
33+
Host: localhost:8080\r\n
34+
\r\n
35+
```
36+
37+
Both requests are identical, clean, and unambiguous. No smuggling payload.
38+
39+
## What the RFC says
40+
41+
> "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
42+
43+
## Why it matters
44+
45+
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.
46+
47+
## Verdicts
48+
49+
- **Pass** — Both steps return `2xx` (pipelining works correctly)
50+
- **Warn** — Step 1 returns `2xx` but server closes connection before step 2 (no pipelining support)
51+
- **Fail** — Step 1 does not return `2xx`
52+
53+
## Sources
54+
55+
- [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
title: "TECL-CONN-CLOSE"
3+
description: "TECL-CONN-CLOSE sequence test documentation"
4+
weight: 11
5+
---
6+
7+
| | |
8+
|---|---|
9+
| **Test ID** | `SMUG-TECL-CONN-CLOSE` |
10+
| **Category** | Smuggling |
11+
| **Type** | Sequence (2 steps) |
12+
| **Scored** | Yes |
13+
| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) |
14+
| **RFC Level** | MUST |
15+
| **Expected** | `400`, or `2xx` + connection close |
16+
17+
## What it does
18+
19+
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.
20+
21+
### Step 1: Ambiguous POST (TE+CL)
22+
23+
```http
24+
POST / HTTP/1.1\r\n
25+
Host: localhost:8080\r\n
26+
Transfer-Encoding: chunked\r\n
27+
Content-Length: 5\r\n
28+
\r\n
29+
0\r\n
30+
\r\n
31+
```
32+
33+
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.
34+
35+
### Step 2: Follow-up GET
36+
37+
```http
38+
GET / HTTP/1.1\r\n
39+
Host: localhost:8080\r\n
40+
\r\n
41+
```
42+
43+
A normal GET sent on the same connection. This step only executes if the connection is still open after step 1.
44+
45+
## What the RFC says
46+
47+
> "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
48+
49+
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.
50+
51+
## Why it matters
52+
53+
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.
54+
55+
## Verdicts
56+
57+
- **Pass** — Server returns `400` (rejected outright), OR returns `2xx` and closes the connection (step 2 never executes)
58+
- **Fail** — Server returns `2xx` and keeps the connection open (step 2 executes and gets a response)
59+
60+
## Sources
61+
62+
- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
title: "TECL-DESYNC"
3+
description: "TECL-DESYNC sequence test documentation"
4+
weight: 14
5+
---
6+
7+
| | |
8+
|---|---|
9+
| **Test ID** | `SMUG-TECL-DESYNC` |
10+
| **Category** | Smuggling |
11+
| **Type** | Sequence (2 steps) |
12+
| **Scored** | Yes |
13+
| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) |
14+
| **RFC Level** | MUST |
15+
| **Expected** | `400`, or connection close |
16+
17+
## What it does
18+
19+
This is a **sequence test** that detects TE.CL request boundary desynchronization — the reverse of the classic CL.TE smuggling attack.
20+
21+
### Step 1: Poison POST (TE terminates early, CL=30)
22+
23+
```http
24+
POST / HTTP/1.1\r\n
25+
Host: localhost:8080\r\n
26+
Transfer-Encoding: chunked\r\n
27+
Content-Length: 30\r\n
28+
\r\n
29+
0\r\n
30+
\r\n
31+
X
32+
```
33+
34+
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.
35+
36+
- 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.
37+
- If the server uses **CL**: waits for 30 bytes total, which never arrive (timeout).
38+
39+
### Step 2: Follow-up GET
40+
41+
```http
42+
GET / HTTP/1.1\r\n
43+
Host: localhost:8080\r\n
44+
\r\n
45+
```
46+
47+
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.
48+
49+
## What the RFC says
50+
51+
> "**Regardless, the server MUST close the connection after responding to such a request** to avoid the potential attacks." — RFC 9112 §6.1
52+
53+
## Why it matters
54+
55+
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.
56+
57+
## Verdicts
58+
59+
- **Pass** — Server returns `400` (rejected outright), OR closes the connection (step 2 never executes)
60+
- **Fail** — Step 2 executes and returns `400` (desync confirmed — poison byte merged with GET)
61+
- **Fail** — Step 2 executes and returns `2xx` (MUST-close violated, connection stayed open)
62+
63+
## Sources
64+
65+
- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1)
66+
- [RFC 9112 §11.2](https://www.rfc-editor.org/rfc/rfc9112#section-11.2)

0 commit comments

Comments
 (0)