Skip to content

Commit aa9db78

Browse files
committed
Multiple website improvements
1 parent 1fb453b commit aa9db78

22 files changed

Lines changed: 1104 additions & 41 deletions

File tree

docs/assets/css/custom.css

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,28 +73,13 @@ html.dark .hextra-card:hover {
7373

7474
/* ── Nav ─────────────────────────────────────────────────────── */
7575

76-
/* Probe Results — primary nav item */
76+
/* Leaderboards — primary nav item */
7777
a[href$="/probe-results"],
7878
a[href$="/probe-results/"] {
7979
font-weight: 700 !important;
8080
font-size: 1.1em !important;
8181
}
8282

83-
/* Separator before Glossary */
84-
a[href$="/docs"],
85-
a[href$="/docs/"] {
86-
margin-left: 12px !important;
87-
padding-left: 14px !important;
88-
border-left: 1px solid rgba(128, 128, 128, 0.3) !important;
89-
}
90-
91-
/* Separator before Add a Framework */
92-
a[href$="/add-a-framework"],
93-
a[href$="/add-a-framework/"] {
94-
margin-left: 12px !important;
95-
padding-left: 14px !important;
96-
border-left: 1px solid rgba(128, 128, 128, 0.3) !important;
97-
}
9883

9984
/* ── Code blocks: fix dark-mode readability ─────────────────── */
10085

docs/content/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Http11Probe sends a suite of crafted HTTP requests to each server and checks whe
4040
{{< card link="compliance" title="Compliance" subtitle="RFC 9110/9112 protocol requirements — line endings, request-line format, header syntax, Host validation, Content-Length parsing." icon="check-circle" >}}
4141
{{< card link="smuggling" title="Smuggling" subtitle="CL/TE ambiguity, duplicate Content-Length, obfuscated Transfer-Encoding, pipeline injection vectors." icon="shield-exclamation" >}}
4242
{{< card link="malformed-input" title="Robustness" subtitle="Binary garbage, oversized fields, too many headers, control characters, integer overflow, incomplete requests." icon="lightning-bolt" >}}
43+
{{< card link="normalization" title="Normalization" subtitle="Header normalization behavior — underscore-to-hyphen, space before colon, tab in name, case folding on Transfer-Encoding." icon="adjustments" >}}
4344
{{< /cards >}}
4445

4546
<div style="height:60px"></div>

docs/content/add-a-framework/_index.md

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,39 @@
11
---
22
title: Add a Framework
3-
toc: false
3+
toc: true
44
---
55

66
Http11Probe is designed so anyone can contribute their HTTP server and get compliance results without touching the test infrastructure.
77

8+
## Required Endpoints
9+
10+
Your server must listen on **port 8080** and implement three endpoints:
11+
12+
| Endpoint | Method | Behavior |
13+
|----------|--------|----------|
14+
| `/` | `GET` | Return `200 OK`. This is the baseline reachability check. |
15+
| `/` | `POST` | Read the full request body and return it in the response. Used by body handling and smuggling tests. |
16+
| `/echo` | `POST` | Return all received request headers in the response body, one per line as `Name: Value`. Used by normalization tests. |
17+
18+
### Why `/echo`?
19+
20+
Normalization tests need to see how the server internally represents headers after parsing. For example, if the test sends `Content_Length: 99`, the `/echo` endpoint reveals whether the server normalized the underscore to a hyphen, preserved it as-is, or dropped it entirely. Without this endpoint, normalization tests cannot run.
21+
22+
### Response format for `/echo`
23+
24+
The response body should contain one header per line in `Name: Value` format:
25+
26+
```
27+
Host: localhost:8080
28+
Content-Length: 11
29+
Content-Type: text/plain
30+
```
31+
32+
The order does not matter. Include all headers the server received (framework-added headers like `Connection` are fine).
33+
834
## Steps
935

10-
**1. Write a minimal server**Create a directory under `src/Servers/YourServer/` with a simple HTTP server that listens on **port 8080** and returns `200 OK` on `GET /`. Any language, any framework.
36+
**1. Create a server directory**Add a directory under `src/Servers/YourServer/` with your server source code implementing the three endpoints above.
1137

1238
**2. Add a Dockerfile** — Build and run your server. It will run with `--network host`.
1339

@@ -17,7 +43,7 @@ Http11Probe is designed so anyone can contribute their HTTP server and get compl
1743
{"name": "Your Server"}
1844
```
1945

20-
That's it. Open a PR and the probe runs automatically.
46+
Open a PR and the probe runs automatically.
2147

2248
## How It Works
2349

@@ -26,14 +52,14 @@ The CI pipeline scans `src/Servers/*/probe.json` to discover servers. For each o
2652
1. Builds the Docker image from the Dockerfile in that directory
2753
2. Runs the container on port 8080 with `--network host`
2854
3. Waits for the server to become ready
29-
4. Runs the full compliance probe suite
55+
4. Runs the full probe suite (compliance, smuggling, malformed input, normalization)
3056
5. Stops the container and moves to the next server
3157

3258
No workflow edits, no port allocation, no config files.
3359

3460
## Example
3561

36-
Here's the full Flask server as a reference:
62+
Here's the Flask server as a reference:
3763

3864
**`src/Servers/FlaskServer/probe.json`**
3965
```json
@@ -49,4 +75,36 @@ COPY src/Servers/FlaskServer/app.py .
4975
ENTRYPOINT ["python3", "app.py", "8080"]
5076
```
5177

52-
**`src/Servers/FlaskServer/app.py`** — a minimal Flask app that reads the port from `sys.argv` and returns `200 OK` on `GET /`.
78+
**`src/Servers/FlaskServer/app.py`**
79+
```python
80+
import sys
81+
from flask import Flask, request
82+
from werkzeug.routing import Rule
83+
84+
app = Flask(__name__)
85+
86+
@app.route('/echo', methods=['GET','POST','PUT','DELETE','PATCH','OPTIONS','HEAD'])
87+
def echo():
88+
lines = []
89+
for name, value in request.headers:
90+
lines.append(f"{name}: {value}")
91+
return '\n'.join(lines) + '\n', 200, {'Content-Type': 'text/plain'}
92+
93+
app.url_map.add(Rule('/', defaults={"path": ""}, endpoint='catch_all'))
94+
app.url_map.add(Rule('/<path:path>', endpoint='catch_all'))
95+
96+
@app.endpoint('catch_all')
97+
def catch_all(path):
98+
if request.method == 'POST':
99+
return request.get_data(as_text=True)
100+
return "OK"
101+
102+
if __name__ == "__main__":
103+
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
104+
app.run(host="0.0.0.0", port=port)
105+
```
106+
107+
The key parts:
108+
- **`/echo`** — echoes all received headers back as plain text.
109+
- **`POST /`** — reads and returns the request body (needed for body and smuggling tests).
110+
- **`GET /`** (catch-all) — returns `"OK"` with `200`.

docs/content/docs/body/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Body Handling
33
description: "Body Handling — Http11Probe documentation"
4-
weight: 6
4+
weight: 9
55
sidebar:
66
open: false
77
---

docs/content/docs/content-length/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Content-Length
33
description: "Content-Length — Http11Probe documentation"
4-
weight: 6
4+
weight: 8
55
sidebar:
66
open: false
77
---

docs/content/docs/headers/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Header Syntax
33
description: "Header Syntax — Http11Probe documentation"
4-
weight: 4
4+
weight: 6
55
sidebar:
66
open: false
77
---

docs/content/docs/host-header/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Host Header
33
description: "Host Header — Http11Probe documentation"
4-
weight: 5
4+
weight: 7
55
sidebar:
66
open: false
77
---
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
title: Understanding HTTP
3+
description: "What HTTP is, how HTTP/1.1 works in depth, its history from 0.9 to 3, and alternatives."
4+
weight: 2
5+
sidebar:
6+
open: false
7+
---
8+
9+
A comprehensive guide to HTTP — what it is, why it was designed the way it was, and how HTTP/1.1 works at the wire level. Start here before diving into the individual test categories.
10+
11+
{{< cards >}}
12+
{{< card link="what-is-http" title="What is HTTP?" subtitle="Application-layer request/response protocol, client-server model, stateless design, and core design goals." icon="question-mark-circle" >}}
13+
{{< card link="message-syntax" title="Message Syntax" subtitle="Request and response structure, methods (GET, POST, PUT...), status codes (1xx–5xx), and the request-line grammar." icon="code" >}}
14+
{{< card link="headers" title="Headers" subtitle="Header structure, common request and response headers, the Host header, and why it's the only required header." icon="document-text" >}}
15+
{{< card link="connections" title="Connections" subtitle="Persistent connections, keep-alive, pipelining, head-of-line blocking, Upgrade, and 100 Continue." icon="switch-horizontal" >}}
16+
{{< card link="body-and-framing" title="Body and Framing" subtitle="Content-Length, chunked transfer encoding, trailers, and why CL+TE conflicts cause request smuggling." icon="document-download" >}}
17+
{{< card link="caching-and-negotiation" title="Caching and Negotiation" subtitle="Content negotiation with Accept headers, Cache-Control, ETags, conditional requests, and Vary." icon="refresh" >}}
18+
{{< card link="history-and-future" title="History and Future" subtitle="HTTP/0.9 to HTTP/3, the current IETF work, alternatives to HTTP, and learning resources." icon="clock" >}}
19+
{{< /cards >}}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
---
2+
title: Body and Framing
3+
description: "Content-Length, chunked transfer encoding, trailers, and why CL+TE conflicts cause request smuggling."
4+
weight: 5
5+
---
6+
7+
HTTP/1.1 messages optionally carry a **message body** after the header section. The critical question for any parser is: **where does the body end?** Getting this wrong is the root cause of HTTP request smuggling.
8+
9+
## When Is a Body Present?
10+
11+
- **Requests** — a body is present if `Content-Length` or `Transfer-Encoding` is set. `GET`, `HEAD`, `DELETE`, and `OPTIONS` typically have no body (though the spec doesn't forbid it).
12+
- **Responses** — all responses to `HEAD` requests and all `1xx`, `204`, and `304` responses have no body. Everything else may have a body.
13+
14+
## Content-Length
15+
16+
The `Content-Length` header declares the exact size of the body in bytes as a decimal integer:
17+
18+
```http
19+
POST /data HTTP/1.1
20+
Host: example.com
21+
Content-Type: text/plain
22+
Content-Length: 13
23+
24+
Hello, World!
25+
```
26+
27+
The parser reads exactly 13 bytes after the empty line, then the next bytes are the start of the next message (on a persistent connection) or the connection ends.
28+
29+
### Rules
30+
31+
- The value **MUST** be a non-negative decimal integer.
32+
- **No leading zeros**`Content-Length: 007` is invalid.
33+
- **No signs**`Content-Length: +13` or `Content-Length: -1` are invalid.
34+
- **No whitespace** within the value — `Content-Length: 1 3` is invalid.
35+
- If `Content-Length` **doesn't match** the actual body size, the message is malformed. The server SHOULD close the connection.
36+
- **Multiple `Content-Length` headers** are allowed only if all values are identical. If they differ, the message is malformed and MUST be rejected.
37+
38+
### Why Strictness Matters
39+
40+
Lenient parsing of `Content-Length` is a common source of vulnerabilities:
41+
42+
- `Content-Length: 0x0d` — if parsed as hex, this is 13 bytes. If parsed as decimal, it's invalid. A parser mismatch between front-end and back-end enables smuggling.
43+
- `Content-Length: 13, 14` — a list of two differing values. One parser might take the first, another the last.
44+
45+
## Chunked Transfer Encoding
46+
47+
When the total body size is unknown at the time headers are sent (streaming, server-generated content, compression), HTTP/1.1 uses **chunked transfer encoding**.
48+
49+
### Format
50+
51+
```
52+
chunk-size (hex) CRLF
53+
chunk-data CRLF
54+
...
55+
0 CRLF
56+
[ trailer-section ]
57+
CRLF
58+
```
59+
60+
Each chunk starts with the chunk size in hexadecimal, followed by CRLF, then exactly that many bytes of data, followed by CRLF. A zero-length chunk signals the end of the body.
61+
62+
### Full Example
63+
64+
```http
65+
HTTP/1.1 200 OK
66+
Transfer-Encoding: chunked
67+
68+
4\r\n
69+
Wiki\r\n
70+
7\r\n
71+
pedia i\r\n
72+
B\r\n
73+
n chunks.\r\n
74+
0\r\n
75+
\r\n
76+
```
77+
78+
Decoded body: `Wikipedia in chunks.`
79+
80+
### Chunk Extensions
81+
82+
A chunk-size may be followed by semicolon-separated extensions:
83+
84+
```
85+
a;ext-name=ext-value\r\n
86+
0123456789\r\n
87+
```
88+
89+
Most servers and proxies **ignore** chunk extensions. They exist for potential use cases like per-chunk checksums or metadata, but are rarely used in practice. Some security tools test whether servers handle unexpected extensions safely.
90+
91+
### Trailers
92+
93+
After the final zero-length chunk, **trailer fields** may appear — headers sent after the body:
94+
95+
```http
96+
HTTP/1.1 200 OK
97+
Transfer-Encoding: chunked
98+
Trailer: Checksum
99+
100+
4\r\n
101+
data\r\n
102+
0\r\n
103+
Checksum: abc123\r\n
104+
\r\n
105+
```
106+
107+
Trailers are useful for:
108+
- **Checksums/signatures** — computed as the body streams.
109+
- **Processing status** — whether the server completed successfully.
110+
- **Metadata** — anything that can't be determined until after the body is generated.
111+
112+
The `Trailer` header in the response declares which trailer fields to expect (though this is advisory, not enforced).
113+
114+
### Rules
115+
116+
- Chunk sizes **MUST** be hexadecimal, case-insensitive (`a` and `A` are both valid).
117+
- A zero-length chunk **MUST** be present to terminate the body.
118+
- After the zero-length chunk, the trailer section and final CRLF complete the message.
119+
120+
## Content-Length vs Transfer-Encoding
121+
122+
A message **MUST NOT** contain both `Content-Length` and `Transfer-Encoding`.
123+
124+
RFC 9112 §6.1 is explicit:
125+
126+
> If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and **ought to be handled as an error**.
127+
128+
### The Request Smuggling Problem
129+
130+
This ambiguity is the **root cause of HTTP request smuggling**. Consider a message with both headers:
131+
132+
```http
133+
POST / HTTP/1.1
134+
Host: example.com
135+
Content-Length: 6
136+
Transfer-Encoding: chunked
137+
138+
0\r\n
139+
\r\n
140+
GPOST
141+
```
142+
143+
- A parser that uses **Transfer-Encoding** sees a zero-length chunk → body ends immediately. The remaining bytes (`GPOST`) are the start of the next request.
144+
- A parser that uses **Content-Length** reads 6 bytes (`0\r\n\r\nG`) as the body. `POST` becomes part of the next request with a different method.
145+
146+
If a front-end proxy uses one interpretation and a back-end server uses another, the attacker controls where one request ends and the next begins. This can:
147+
- **Bypass access controls** — smuggle a request to an internal endpoint.
148+
- **Poison caches** — make the cache store an attacker-controlled response for a victim's URL.
149+
- **Hijack connections** — capture another user's request.
150+
151+
### How Servers Should Handle It
152+
153+
Strict servers should:
154+
1. **Reject** messages with both `Content-Length` and `Transfer-Encoding` with a 400 response.
155+
2. If not rejecting, **always prioritize `Transfer-Encoding`** and ignore `Content-Length`.
156+
3. **Never trust `Content-Length`** when `Transfer-Encoding` is present.
157+
158+
This is one of the most critical compliance checks that Http11Probe performs.
159+
160+
## Transfer-Encoding Obfuscation
161+
162+
Attackers may try to hide `Transfer-Encoding` from one parser while making another recognize it:
163+
164+
```http
165+
Transfer-Encoding: chunked
166+
Transfer-Encoding : chunked
167+
Transfer-Encoding: xchunked
168+
Transfer-Encoding: chunked\r\n (extra space)
169+
Transfer-Encoding:
170+
chunked
171+
```
172+
173+
Each of these variants exploits differences in how parsers handle:
174+
- Whitespace before the colon (forbidden by RFC 9112 §5.1).
175+
- Unknown transfer coding names.
176+
- Obs-fold (deprecated line folding).
177+
- Leading/trailing whitespace in the value.
178+
179+
Strict, RFC-compliant parsing eliminates these attack surfaces.

0 commit comments

Comments
 (0)