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
16 changes: 15 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Choose the correct suite file based on category:
| Smuggling | `src/Http11Probe/TestCases/Suites/SmugglingSuite.cs` |
| Malformed Input | `src/Http11Probe/TestCases/Suites/MalformedInputSuite.cs` |
| Normalization | `src/Http11Probe/TestCases/Suites/NormalizationSuite.cs` |
| Cookies | `src/Http11Probe/TestCases/Suites/CookieSuite.cs` |

Append a `yield return new TestCase { ... };` inside the `GetTestCases()` method. Here is the full schema:

Expand Down Expand Up @@ -57,6 +58,7 @@ yield return new TestCase
| `SMUG-` | Smuggling |
| `MAL-` | Malformed Input |
| `NORM-` | Normalization |
| `COOK-` | Cookies |
| `RFC9112-X.X-` or `RFC9110-X.X-` | Compliance (maps directly to an RFC section) |

**Validation patterns — choose ONE:**
Expand Down Expand Up @@ -137,6 +139,7 @@ This step is **only needed** for `COMP-*` and `RFC*` prefixed tests. The followi
- `SMUG-XYZ` → `smuggling/xyz` (lowercased)
- `MAL-XYZ` → `malformed-input/xyz` (lowercased)
- `NORM-XYZ` → `normalization/xyz` (lowercased)
- `COOK-XYZ` → `cookies/xyz` (lowercased)

For compliance tests, add an entry to the `ComplianceSlugs` dictionary:
```csharp
Expand Down Expand Up @@ -166,6 +169,7 @@ Category slug mapping:
| Smuggling | `smuggling` |
| Malformed Input | `malformed-input` |
| Normalization | `normalization` |
| Cookies | `cookies` |

Use this exact template:

Expand Down Expand Up @@ -272,7 +276,8 @@ Your server MUST listen on **port 8080** and implement these endpoints:
| `/` | `HEAD` | Return `200 OK` with no body |
| `/` | `POST` | Read the full request body and return it in the response body |
| `/` | `OPTIONS` | Return `200 OK` |
| `/echo` | `POST` | Return all received request headers in the response body, one per line as `Name: Value` |
| `/echo` | `GET`, `POST` | Return all received request headers in the response body, one per line as `Name: Value` |
| `/cookie` | `GET`, `POST` | Parse the `Cookie` header and return each cookie as `name=value` on its own line |

The `/echo` endpoint is critical for normalization tests. It must echo back all headers the server received, preserving the names as the server internally represents them.

Expand All @@ -283,6 +288,14 @@ Content-Length: 11
Content-Type: text/plain
```

The `/cookie` endpoint is used by the Cookies test suite. It must split the `Cookie` header on `;`, trim leading whitespace from each pair, find the first `=`, and output `name=value\n` for each cookie.

Example — given `Cookie: foo=bar; baz=qux`, the response body should be:
```
foo=bar
baz=qux
```

### Step 3 — Add a Dockerfile

Create `src/Servers/YourServer/Dockerfile` that builds and runs the server.
Expand Down Expand Up @@ -352,6 +365,7 @@ Rules:
- `curl http://localhost:8080/` returns 200
- `curl -X POST -d "hello" http://localhost:8080/` returns "hello"
- `curl -X POST -d "test" http://localhost:8080/echo` returns headers
- `curl -H "Cookie: foo=bar; baz=qux" http://localhost:8080/cookie` returns `foo=bar` and `baz=qux` on separate lines
4. Run the probe: `dotnet run --project src/Http11Probe.Cli -- --host localhost --port 8080`

No changes to CI workflows, configs, or other files are needed. The pipeline auto-discovers servers from `src/Servers/*/probe.json`.
1 change: 1 addition & 0 deletions docs/content/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Http11Probe sends a suite of crafted HTTP requests to each server and checks whe
{{< card link="malformed-input" title="Robustness" subtitle="Binary garbage, oversized fields, too many headers, control characters, integer overflow, incomplete requests." icon="lightning-bolt" >}}
{{< 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" >}}
{{< card link="caching" title="Caching" subtitle="Conditional request support — ETag, Last-Modified, If-None-Match precedence, weak comparison, edge cases." icon="beaker" >}}
{{< card link="cookies" title="Cookies" subtitle="Cookie header parsing resilience — oversized values, NUL bytes, control characters, malformed pairs, multiple headers." icon="cake" >}}
{{< /cards >}}

<div style="height:60px"></div>
Expand Down
4 changes: 3 additions & 1 deletion docs/content/add-a-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Pick the suite that matches your test's category and add a `yield return new Tes
| Smuggling | `src/Http11Probe/TestCases/Suites/SmugglingSuite.cs` |
| Malformed Input | `src/Http11Probe/TestCases/Suites/MalformedInputSuite.cs` |
| Normalization | `src/Http11Probe/TestCases/Suites/NormalizationSuite.cs` |
| Cookies | `src/Http11Probe/TestCases/Suites/CookieSuite.cs` |

```csharp
yield return new TestCase
Expand Down Expand Up @@ -46,6 +47,7 @@ yield return new TestCase
| `SMUG-` | Smuggling |
| `MAL-` | Malformed Input |
| `NORM-` | Normalization |
| `COOK-` | Cookies |
| `RFC9112-...` or `RFC9110-...` | Compliance (when the test maps directly to a specific RFC section) |

### Validation options
Expand Down Expand Up @@ -97,7 +99,7 @@ Expected = new ExpectedBehavior

**File:** `src/Http11Probe.Cli/Reporting/DocsUrlMap.cs`

Tests prefixed with `SMUG-`, `MAL-`, or `NORM-` are auto-mapped to their doc URL based on the ID. For example, `SMUG-CL-TE-BOTH` maps to `smuggling/cl-te-both`.
Tests prefixed with `SMUG-`, `MAL-`, `NORM-`, or `COOK-` are auto-mapped to their doc URL based on the ID. For example, `SMUG-CL-TE-BOTH` maps to `smuggling/cl-te-both`.

For `COMP-*` or `RFC*` prefixed tests, add an entry to the `ComplianceSlugs` dictionary:

Expand Down
2 changes: 1 addition & 1 deletion docs/content/add-with-ai-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ For a new **test**, the agent will:
For a new **framework**, the agent will:

1. Create a server directory under `src/Servers/`
2. Implement the server with all required endpoints (GET, HEAD, POST, OPTIONS on `/` and POST on `/echo`)
2. Implement the server with all required endpoints (GET, HEAD, POST, OPTIONS on `/`, GET/POST on `/echo`, and GET/POST on `/cookie`)
3. Write a Dockerfile that builds and runs the server on port 8080
4. Add a `probe.json` with the display name

Expand Down
64 changes: 64 additions & 0 deletions docs/content/cookies/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
title: Cookies
layout: wide
toc: false
---

## Cookie Handling

These tests check how servers and frameworks handle adversarial `Cookie` headers. Cookie parsing is done at the framework level, not by application code, so malformed cookies can crash parsers or produce mangled values before your code ever runs. All cookie tests are **unscored** since cookies are governed by RFC 6265, not RFC 9110/9112.

<style>h1.hx\:mt-2{display:none}.probe-hint{background:#ddf4ff;border:1px solid #54aeff;border-radius:6px;padding:10px 14px;font-size:13px;color:#0969da;font-weight:500}html.dark .probe-hint{background:#1c2333;border-color:#1f6feb;color:#58a6ff}</style>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:16px;">
<div class="probe-hint"><strong style="font-size:14px;">Server Name</strong><br>Click to view Dockerfile and source code</div>
<div class="probe-hint"><strong style="font-size:14px;">Table Row</strong><br>Click to expand all results for that server</div>
<div class="probe-hint"><strong style="font-size:14px;">Result Cell</strong><br>Click to see the full HTTP request and response</div>
</div>

<div class="probe-filters">
<div id="lang-filter"></div>
<div id="method-filter"></div>
<div id="rfc-level-filter"></div>
</div>
<div id="table-cookies"><p><em>Loading...</em></p></div>

<script src="/Http11Probe/probe/data.js"></script>
<script src="/Http11Probe/probe/render.js"></script>
<script>
(function () {
if (!window.PROBE_DATA) {
document.getElementById('table-cookies').innerHTML = '<p><em>No probe data available yet. Run the Probe workflow manually on <code>main</code> to generate results.</em></p>';
return;
}
var GROUPS = [
{ key: 'echo', label: 'Echo-Based (Survivability)', testIds: [
'COOK-ECHO','COOK-OVERSIZED','COOK-EMPTY','COOK-NUL',
'COOK-CONTROL-CHARS','COOK-MANY-PAIRS','COOK-MALFORMED','COOK-MULTI-HEADER'
]},
{ key: 'parsed', label: 'Parsed Cookies (Framework Parser)', testIds: [
'COOK-PARSED-BASIC','COOK-PARSED-MULTI','COOK-PARSED-EMPTY-VAL','COOK-PARSED-SPECIAL'
]}
];

var ALL_IDS = [];
GROUPS.forEach(function (g) { g.testIds.forEach(function (tid) { ALL_IDS.push(tid); }); });

var langData = window.PROBE_DATA;
var methodFilter = null;
var rfcLevelFilter = null;

function rerender() {
var data = langData;
if (methodFilter) data = ProbeRender.filterByMethod(data, methodFilter);
if (rfcLevelFilter) data = ProbeRender.filterByRfcLevel(data, rfcLevelFilter);
var ctx = ProbeRender.buildLookups(data.servers);
ctx.testIds = ALL_IDS;
ProbeRender.renderSubTables('table-cookies', 'Cookies', ctx, GROUPS);
}
rerender();
var catData = ProbeRender.filterByCategory(window.PROBE_DATA, ['Cookies']);
ProbeRender.renderLanguageFilter('lang-filter', window.PROBE_DATA, function (d) { langData = d; rerender(); });
ProbeRender.renderMethodFilter('method-filter', catData, function (m) { methodFilter = m; rerender(); });
ProbeRender.renderRfcLevelFilter('rfc-level-filter', catData, function (l) { rfcLevelFilter = l; rerender(); });
})();
</script>
4 changes: 2 additions & 2 deletions docs/content/docs/caching/etag-304.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ Captures the `ETag` header from the response for use in step 2.
```http
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
If-None-Match: "abc123"\r\n
If-None-Match: {ETag from step 1}\r\n
\r\n
```

Sends the captured ETag value in an `If-None-Match` header. If the resource hasn't changed, the server should return `304 Not Modified`.
Replays the `ETag` value captured from step 1 in an `If-None-Match` header. If the resource hasn't changed, the server should return `304 Not Modified`. If the server did not include an `ETag` header in step 1, the test reports Warn immediately.

## What the RFC says

Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/caching/etag-in-304.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ Captures the `ETag` header from the response.
```http
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
If-None-Match: "abc123"\r\n
If-None-Match: {ETag from step 1}\r\n
\r\n
```

Sends the captured ETag. If the server returns `304`, this test checks whether the `ETag` header is present in that response.
Replays the `ETag` value captured from step 1. If the server returns `304`, this test checks whether the `ETag` header is present in that response.

## What the RFC says

Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/caching/etag-weak.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Captures the `ETag` header from the response. If the ETag is strong (e.g., `"abc
```http
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
If-None-Match: W/"abc123"\r\n
If-None-Match: {ETag from step 1, weak}\r\n
\r\n
```

Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/caching/inm-precedence.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ Captures the `ETag` header from the response.
```http
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
If-None-Match: "abc123"\r\n
If-None-Match: {ETag from step 1}\r\n
If-Modified-Since: Thu, 01 Jan 1970 00:00:00 GMT\r\n
\r\n
```

The `If-None-Match` header matches the current ETag (should produce `304`), but the `If-Modified-Since` is set to epoch (should produce `200` since the resource was certainly modified after 1970). If the server returns `304`, it correctly evaluated `If-None-Match` first.
Replays the `ETag` value captured from step 1 in `If-None-Match` (should produce `304`), combined with `If-Modified-Since` set to epoch (should produce `200` since the resource was certainly modified after 1970). If the server returns `304`, it correctly evaluated `If-None-Match` first.

## What the RFC says

Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/caching/inm-unquoted.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ Captures the `ETag` header from the response for use in step 2.
```http
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
If-None-Match: abc123\r\n
If-None-Match: {ETag from step 1, unquoted}\r\n
\r\n
```

Sends the ETag value without the required surrounding double quotes. According to the RFC grammar, `entity-tag = [ weak ] opaque-tag` and `opaque-tag = DQUOTE *etagc DQUOTE` — the quotes are mandatory.
Sends the ETag value captured from step 1, stripped of the required surrounding double quotes. According to the RFC grammar, `entity-tag = [ weak ] opaque-tag` and `opaque-tag = DQUOTE *etagc DQUOTE` — the quotes are mandatory.

## What the RFC says

Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/caching/last-modified-304.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ Captures the `Last-Modified` header from the response for use in step 2.
```http
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
If-Modified-Since: Sun, 01 Jan 2025 00:00:00 GMT\r\n
If-Modified-Since: {Last-Modified from step 1}\r\n
\r\n
```

Sends the captured Last-Modified value in an `If-Modified-Since` header. If the resource hasn't changed since that date, the server should return `304 Not Modified`.
Replays the `Last-Modified` value captured from step 1 in an `If-Modified-Since` header. If the resource hasn't changed since that date, the server should return `304 Not Modified`. If the server did not include a `Last-Modified` header in step 1, the test reports Warn immediately.

## What the RFC says

Expand Down
5 changes: 5 additions & 0 deletions docs/content/docs/cookies/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Cookie Handling
sidebar:
open: false
---
37 changes: 37 additions & 0 deletions docs/content/docs/cookies/control-chars.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: "CONTROL-CHARS"
description: "COOK-CONTROL-CHARS cookie test documentation"
weight: 5
---

| | |
|---|---|
| **Test ID** | `COOK-CONTROL-CHARS` |
| **Category** | Cookies |
| **Scored** | No |
| **RFC Level** | N/A |
| **Expected** | `400 (rejected) or 2xx without control chars` |

## What it sends

Control characters (0x01-0x03) in cookie value — dangerous if preserved.

```http
GET /echo HTTP/1.1\r\n
Host: localhost:8080\r\n
Cookie: foo=\x01\x02\x03\r\n
\r\n
```

## Why it matters

Control characters in cookie values violate RFC 6265's cookie-octet grammar and can enable response splitting or log injection if passed through to output.

## Verdicts

- **Pass** — 400 rejected, or 2xx with control chars stripped
- **Fail** — 2xx with control chars preserved (dangerous), or 500

## Sources

- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header
37 changes: 37 additions & 0 deletions docs/content/docs/cookies/echo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: "ECHO"
description: "COOK-ECHO cookie test documentation"
weight: 1
---

| | |
|---|---|
| **Test ID** | `COOK-ECHO` |
| **Category** | Cookies |
| **Scored** | No |
| **RFC Level** | N/A |
| **Expected** | `2xx with Cookie in body` |

## What it sends

Basic Cookie header echoed back by /echo endpoint.

```http
GET /echo HTTP/1.1\r\n
Host: localhost:8080\r\n
Cookie: foo=bar\r\n
\r\n
```

## Why it matters

Baseline test — verifies the server's echo endpoint reflects Cookie headers, which is required for all other cookie tests to work.

## Verdicts

- **Pass** — 2xx and body contains `Cookie:` header
- **Fail** — No response or missing header

## Sources

- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header
37 changes: 37 additions & 0 deletions docs/content/docs/cookies/empty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: "EMPTY"
description: "COOK-EMPTY cookie test documentation"
weight: 3
---

| | |
|---|---|
| **Test ID** | `COOK-EMPTY` |
| **Category** | Cookies |
| **Scored** | No |
| **RFC Level** | N/A |
| **Expected** | `2xx or 400` |

## What it sends

Empty Cookie header value — tests parser resilience.

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

## Why it matters

Empty Cookie headers can cause null-reference exceptions or crashes in parsers that assume at least one key=value pair.

## Verdicts

- **Pass** — 2xx or 400
- **Fail** — 500 (crash)

## Sources

- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header
Loading
Loading