From 44552cc8b788e976fe2b91c075a2f5ad96315e7b Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Sat, 14 Feb 2026 14:48:03 +0000 Subject: [PATCH] Adding 9 new tests - 8 Compliance, 1 Smuggling --- AGENTS.md | 22 ++- CHANGELOG.md | 19 ++ docs/content/_index.md | 2 +- docs/content/docs/_index.md | 2 +- docs/content/docs/content-length/_index.md | 1 + .../docs/content-length/no-cl-in-204.md | 35 ++++ docs/content/docs/headers/_index.md | 3 + .../docs/headers/content-type-presence.md | 39 ++++ docs/content/docs/headers/date-header.md | 35 ++++ docs/content/docs/headers/no-1xx-http10.md | 38 ++++ docs/content/docs/request-line/405-allow.md | 40 ++++ docs/content/docs/request-line/_index.md | 4 + .../content/docs/request-line/head-no-body.md | 37 ++++ .../docs/request-line/options-allow.md | 35 ++++ .../docs/request-line/unknown-method.md | 41 ++++ .../content/docs/rfc-requirement-dashboard.md | 73 ++++--- docs/content/docs/smuggling/_index.md | 1 + .../content/docs/smuggling/cl-comma-triple.md | 37 ++++ src/Http11Probe.Cli/Reporting/DocsUrlMap.cs | 8 + .../TestCases/Suites/ComplianceSuite.cs | 185 ++++++++++++++++++ .../TestCases/Suites/SmugglingSuite.cs | 25 +++ 21 files changed, 649 insertions(+), 33 deletions(-) create mode 100644 docs/content/docs/content-length/no-cl-in-204.md create mode 100644 docs/content/docs/headers/content-type-presence.md create mode 100644 docs/content/docs/headers/date-header.md create mode 100644 docs/content/docs/headers/no-1xx-http10.md create mode 100644 docs/content/docs/request-line/405-allow.md create mode 100644 docs/content/docs/request-line/head-no-body.md create mode 100644 docs/content/docs/request-line/options-allow.md create mode 100644 docs/content/docs/request-line/unknown-method.md create mode 100644 docs/content/docs/smuggling/cl-comma-triple.md diff --git a/AGENTS.md b/AGENTS.md index 5da300c..8189162 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ Http11Probe is an HTTP/1.1 compliance and security tester. It sends raw TCP requ ## TASK A: Add a new test -Adding a test requires changes to **4 locations** (sometimes 3 if URL mapping is automatic). +Adding a test requires changes to **5 locations** (sometimes 4 if URL mapping is automatic). ### Step 1 — Add the test case to the suite file @@ -215,6 +215,26 @@ Find the `{{}}` block and add a new card entry. Place scored tests The `link` value is the filename without `.md`. +### Step 5 — Add a row to the RFC Requirement Dashboard + +**File:** `docs/content/docs/rfc-requirement-dashboard.md` + +This page classifies every test by its RFC 2119 requirement level. You must: + +1. **Add a row** to the correct table based on the test's requirement level: + - `MUST` / `MUST NOT` → "MUST-Level Requirements" table (use the "Reject with 400" sub-table if the RFC explicitly mandates 400, otherwise the "Reject (400 or Connection Close Acceptable)" sub-table) + - `SHOULD` / `SHOULD NOT` → "SHOULD-Level Requirements" table + - `MAY` → "MAY-Level Requirements" table + - `Scored = false` → "Unscored Tests" table (regardless of RFC keyword) + +2. **Update the counts** in: + - The summary table at the top (increment the matching requirement level) + - The total test count in both the `description` frontmatter and the "Total: N tests" line + - The "Requirement Level by Suite" section (increment the matching suite + level) + - The "RFC Section Cross-Reference" table (increment existing section count or add a new row) + +3. **Include** the test ID, suite name, RFC link, and an exact RFC quote with the keyword bolded (e.g., `**MUST**`). + ### Verification checklist After making all changes: diff --git a/CHANGELOG.md b/CHANGELOG.md index 483a890..b4f755c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to Http11Probe are documented in this file. +## [Unreleased] + +### Added +- **9 new RFC 9110 compliance tests** sourced from [mohammed90/http-compliance-testing](https://github.com/mohammed90/http-compliance-testing): + - `COMP-HEAD-NO-BODY` — HEAD response must not contain a message body (RFC 9110 §9.3.2, MUST) + - `COMP-UNKNOWN-METHOD` — unrecognized method should be rejected with 501/405 (RFC 9110 §9.1, SHOULD) + - `COMP-405-ALLOW` — 405 response must include Allow header (RFC 9110 §15.5.6, MUST) + - `COMP-DATE-HEADER` — origin server must include Date header in responses (RFC 9110 §6.6.1, MUST) + - `COMP-NO-1XX-HTTP10` — server must not send 1xx to HTTP/1.0 client (RFC 9110 §15.2, MUST NOT) + - `COMP-NO-CL-IN-204` — Content-Length forbidden in 204 responses (RFC 9110 §8.6, MUST NOT) + - `SMUG-CL-COMMA-TRIPLE` — three comma-separated identical CL values (RFC 9110 §8.6, unscored) + - `COMP-OPTIONS-ALLOW` — OPTIONS response should include Allow header (RFC 9110 §9.3.7, SHOULD) + - `COMP-CONTENT-TYPE` — response with content should include Content-Type (RFC 9110 §8.3, SHOULD) + +### Changed +- **AGENTS.md** — added Step 5 (RFC Requirement Dashboard) to the "Add a new test" task +- **RFC Requirement Dashboard** — updated with all 9 new tests, counts, and cross-references +- **Landing page cards** — removed hardcoded test count from RFC Requirement Dashboard subtitle + ## [2026-02-14] ### Added diff --git a/docs/content/_index.md b/docs/content/_index.md index c6a6e70..4059b07 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -24,7 +24,7 @@ layout: hextra-home {{< cards >}} {{< card link="probe-results" title="Leaderboard" subtitle="See which frameworks pass the most tests, ranked from best to worst compliance." icon="chart-bar" >}} - {{< card link="docs/rfc-requirement-dashboard" title="RFC Requirement Dashboard" subtitle="All 148 tests classified by RFC 2119 level (MUST/SHOULD/MAY)." icon="document-search" >}} + {{< card link="docs/rfc-requirement-dashboard" title="RFC Requirement Dashboard" subtitle="Every test classified by RFC 2119 requirement level (MUST/SHOULD/MAY)." icon="document-search" >}} {{< /cards >}}
diff --git a/docs/content/docs/_index.md b/docs/content/docs/_index.md index 32f2f2d..566548a 100644 --- a/docs/content/docs/_index.md +++ b/docs/content/docs/_index.md @@ -10,7 +10,7 @@ Reference documentation for every test in Http11Probe, organized by topic. Each {{< cards >}} {{< card link="http-overview" title="Understanding HTTP" subtitle="What HTTP is, how HTTP/1.1 works at the wire level, its history from 0.9 to 3, and alternatives." icon="globe-alt" >}} - {{< card link="rfc-requirement-dashboard" title="RFC Requirement Dashboard" subtitle="All 148 tests classified by RFC 2119 level (MUST/SHOULD/MAY)." icon="document-search" >}} + {{< card link="rfc-requirement-dashboard" title="RFC Requirement Dashboard" subtitle="Every test classified by RFC 2119 requirement level (MUST/SHOULD/MAY)." icon="document-search" >}} {{< card link="rfc-basics" title="RFC Basics" subtitle="What RFCs are, how to read requirement levels (MUST/SHOULD/MAY), and which RFCs define HTTP/1.1." icon="book-open" >}} {{< card link="baseline" title="Baseline" subtitle="Sanity request used to confirm the target is reachable before running negative tests." icon="check-circle" >}} {{< card link="line-endings" title="Line Endings" subtitle="CRLF requirements, bare LF handling, and bare CR rejection per RFC 9112 Section 2.2." icon="code" >}} diff --git a/docs/content/docs/content-length/_index.md b/docs/content/docs/content-length/_index.md index a8bc9c7..547656b 100644 --- a/docs/content/docs/content-length/_index.md +++ b/docs/content/docs/content-length/_index.md @@ -19,4 +19,5 @@ The `Content-Length` header indicates the size of the message body in bytes. Its {{< cards >}} {{< card link="cl-non-numeric" title="CL-NON-NUMERIC" subtitle="Non-numeric Content-Length value." >}} {{< card link="cl-plus-sign" title="CL-PLUS-SIGN" subtitle="Content-Length with a + prefix." >}} + {{< card link="no-cl-in-204" title="NO-CL-IN-204" subtitle="Content-Length forbidden in 204 responses." >}} {{< /cards >}} diff --git a/docs/content/docs/content-length/no-cl-in-204.md b/docs/content/docs/content-length/no-cl-in-204.md new file mode 100644 index 0000000..86cadc9 --- /dev/null +++ b/docs/content/docs/content-length/no-cl-in-204.md @@ -0,0 +1,35 @@ +--- +title: "NO-CL-IN-204" +description: "NO-CL-IN-204 test documentation" +weight: 3 +--- + +| | | +|---|---| +| **Test ID** | `COMP-NO-CL-IN-204` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | MUST NOT | +| **Expected** | `204` without `Content-Length` | + +## What it sends + +An OPTIONS request to the root path. Some servers respond with `204 No Content`, which triggers the validation. + +```http +OPTIONS / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "A server MUST NOT send a Content-Length header field in any response with a status code of 1xx (Informational) or 204 (No Content)." -- RFC 9110 Section 8.6 + +## Why it matters + +A `204 No Content` response explicitly signals that there is no body. Including `Content-Length` contradicts this, and some clients or proxies may attempt to read body bytes based on the Content-Length value. On persistent connections, this causes desync — the client reads the next response's bytes as body data for the 204, corrupting the entire connection. If the server does not return 204 for this request, the test reports a warning since the prohibition cannot be verified. + +## Sources + +- [RFC 9110 §8.6 -- Content-Length](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) diff --git a/docs/content/docs/headers/_index.md b/docs/content/docs/headers/_index.md index 78de358..0301d04 100644 --- a/docs/content/docs/headers/_index.md +++ b/docs/content/docs/headers/_index.md @@ -30,4 +30,7 @@ HTTP header fields follow a strict grammar: `field-name ":" OWS field-value OWS` {{< card link="header-no-colon" title="HEADER-NO-COLON" subtitle="Header line with no colon separator." >}} {{< card link="whitespace-before-headers" title="WHITESPACE-BEFORE-HEADERS" subtitle="Whitespace before the first header line." >}} {{< card link="expect-unknown" title="EXPECT-UNKNOWN" subtitle="Unknown Expect value. Should respond with 417." >}} + {{< card link="date-header" title="DATE-HEADER" subtitle="Origin server must include Date header in responses." >}} + {{< card link="no-1xx-http10" title="NO-1XX-HTTP10" subtitle="Server must not send 1xx to HTTP/1.0 client." >}} + {{< card link="content-type-presence" title="CONTENT-TYPE" subtitle="Response with content should include Content-Type." >}} {{< /cards >}} diff --git a/docs/content/docs/headers/content-type-presence.md b/docs/content/docs/headers/content-type-presence.md new file mode 100644 index 0000000..2777f2f --- /dev/null +++ b/docs/content/docs/headers/content-type-presence.md @@ -0,0 +1,39 @@ +--- +title: "CONTENT-TYPE" +description: "CONTENT-TYPE test documentation" +weight: 12 +--- + +| | | +|---|---| +| **Test ID** | `COMP-CONTENT-TYPE` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 §8.3](https://www.rfc-editor.org/rfc/rfc9110#section-8.3) | +| **Requirement** | SHOULD | +| **Expected** | `2xx` with `Content-Type` header | + +## What it sends + +A standard GET request. The test validates that the server includes a `Content-Type` header when the response contains a body. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "A sender that generates a message containing content SHOULD generate a Content-Type header field in the message unless the intended media type of the enclosed representation is unknown to the sender." -- RFC 9110 Section 8.3 + +And: + +> "If a Content-Type header field is not present, the recipient MAY either assume a media type of 'application/octet-stream' or examine the data to determine its type." -- RFC 9110 Section 8.3 + +## Why it matters + +Without Content-Type, clients must guess the media type through content sniffing, which is a well-known security risk. Browsers performing MIME sniffing may interpret a response as HTML when it was intended as plain text, enabling XSS attacks. Including Content-Type is a baseline security practice. + +## Sources + +- [RFC 9110 §8.3 -- Content-Type](https://www.rfc-editor.org/rfc/rfc9110#section-8.3) diff --git a/docs/content/docs/headers/date-header.md b/docs/content/docs/headers/date-header.md new file mode 100644 index 0000000..45e1f22 --- /dev/null +++ b/docs/content/docs/headers/date-header.md @@ -0,0 +1,35 @@ +--- +title: "DATE-HEADER" +description: "DATE-HEADER test documentation" +weight: 10 +--- + +| | | +|---|---| +| **Test ID** | `COMP-DATE-HEADER` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 §6.6.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.6.1) | +| **Requirement** | MUST | +| **Expected** | `2xx` with `Date` header | + +## What it sends + +A standard GET request. The test validates that the server includes a `Date` header in its response. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "An origin server with a clock MUST generate a Date header field in all 2xx (Successful), 3xx (Redirection), and 4xx (Client Error) responses, and MAY generate a Date header field in 1xx (Informational) and 5xx (Server Error) responses." -- RFC 9110 Section 6.6.1 + +## Why it matters + +The Date header is essential for HTTP caching. Caches use it to calculate age, determine freshness, and resolve clock skew between origin servers and intermediaries. Without it, caches cannot properly compute expiration times, leading to either stale content being served or unnecessary revalidation. + +## Sources + +- [RFC 9110 §6.6.1 -- Date](https://www.rfc-editor.org/rfc/rfc9110#section-6.6.1) diff --git a/docs/content/docs/headers/no-1xx-http10.md b/docs/content/docs/headers/no-1xx-http10.md new file mode 100644 index 0000000..6206f86 --- /dev/null +++ b/docs/content/docs/headers/no-1xx-http10.md @@ -0,0 +1,38 @@ +--- +title: "NO-1XX-HTTP10" +description: "NO-1XX-HTTP10 test documentation" +weight: 11 +--- + +| | | +|---|---| +| **Test ID** | `COMP-NO-1XX-HTTP10` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 §15.2](https://www.rfc-editor.org/rfc/rfc9110#section-15.2) | +| **Requirement** | MUST NOT | +| **Expected** | Non-1xx response | + +## What it sends + +An HTTP/1.0 POST with `Expect: 100-continue` and a body, designed to test whether the server incorrectly sends a `100 Continue` interim response to an HTTP/1.0 client. + +```http +POST / HTTP/1.0\r\n +Host: localhost:8080\r\n +Expect: 100-continue\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +## What the RFC says + +> "Since HTTP/1.0 did not define any 1xx status codes, a server MUST NOT send a 1xx response to an HTTP/1.0 client." -- RFC 9110 Section 15.2 + +## Why it matters + +HTTP/1.0 clients do not understand interim responses. If a server sends `100 Continue` to an HTTP/1.0 client, the client may interpret the `100` status line as a malformed final response, discard it as garbage, or enter an undefined state. This is especially dangerous in proxy chains where an HTTP/1.0 hop cannot forward 1xx responses correctly. + +## Sources + +- [RFC 9110 §15.2 -- Informational 1xx](https://www.rfc-editor.org/rfc/rfc9110#section-15.2) diff --git a/docs/content/docs/request-line/405-allow.md b/docs/content/docs/request-line/405-allow.md new file mode 100644 index 0000000..618ad06 --- /dev/null +++ b/docs/content/docs/request-line/405-allow.md @@ -0,0 +1,40 @@ +--- +title: "405-ALLOW" +description: "405-ALLOW test documentation" +weight: 17 +--- + +| | | +|---|---| +| **Test ID** | `COMP-405-ALLOW` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 §15.5.6](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.6) | +| **Requirement** | MUST | +| **Expected** | `405` with `Allow` header | + +## What it sends + +A DELETE request to the root path, which most servers do not support. This is intended to trigger a 405 response. + +```http +DELETE / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "The origin server MUST generate an Allow header field in a 405 response containing a list of the target resource's currently supported methods." -- RFC 9110 Section 15.5.6 + +And: + +> "An origin server MUST generate an Allow header field in a 405 (Method Not Allowed) response and MAY do so in any other response." -- RFC 9110 Section 10.2.1 + +## Why it matters + +The Allow header in a 405 response tells clients which methods are actually supported. Without it, clients have no way to discover valid methods for the resource, forcing them to guess. Automated tools and API clients depend on this header for correct operation. If the server returns a status other than 405 (e.g., it accepts DELETE or returns 501), the test reports a warning since the Allow requirement cannot be verified. + +## Sources + +- [RFC 9110 §15.5.6 -- 405 Method Not Allowed](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.6) +- [RFC 9110 §10.2.1 -- Allow](https://www.rfc-editor.org/rfc/rfc9110#section-10.2.1) diff --git a/docs/content/docs/request-line/_index.md b/docs/content/docs/request-line/_index.md index 8f3a3e7..7950838 100644 --- a/docs/content/docs/request-line/_index.md +++ b/docs/content/docs/request-line/_index.md @@ -26,6 +26,10 @@ Note this is a SHOULD, not a MUST. The RFC recommends 400 but does not mandate i {{< card link="options-star" title="OPTIONS-STAR" subtitle="OPTIONS * — valid asterisk-form request." >}} {{< card link="unknown-te-501" title="UNKNOWN-TE-501" subtitle="Unknown Transfer-Encoding without CL." >}} {{< card link="method-connect" title="METHOD-CONNECT" subtitle="CONNECT to an origin server must be rejected." >}} + {{< card link="head-no-body" title="HEAD-NO-BODY" subtitle="HEAD response must not contain a message body." >}} + {{< card link="unknown-method" title="UNKNOWN-METHOD" subtitle="Unrecognized method should be rejected with 501 or 405." >}} + {{< card link="405-allow" title="405-ALLOW" subtitle="405 response must include an Allow header." >}} + {{< card link="options-allow" title="OPTIONS-ALLOW" subtitle="OPTIONS response should include Allow header." >}} {{< /cards >}} ### Unscored diff --git a/docs/content/docs/request-line/head-no-body.md b/docs/content/docs/request-line/head-no-body.md new file mode 100644 index 0000000..58227cf --- /dev/null +++ b/docs/content/docs/request-line/head-no-body.md @@ -0,0 +1,37 @@ +--- +title: "HEAD-NO-BODY" +description: "HEAD-NO-BODY test documentation" +weight: 15 +--- + +| | | +|---|---| +| **Test ID** | `COMP-HEAD-NO-BODY` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 §9.3.2](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.2) | +| **Requirement** | MUST | +| **Expected** | `2xx` with no body | + +## What it sends + +A standard HEAD request. The server must respond with headers only — no message body. + +```http +HEAD / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "The HEAD method is identical to GET except that the server MUST NOT send content in the response." -- RFC 9110 Section 9.3.2 + +The server may include `Content-Length` or `Transfer-Encoding` headers to indicate what the body *would have been* for a GET request, but the actual response must contain zero body bytes. + +## Why it matters + +If a server sends body content in response to HEAD, it corrupts connection state on persistent connections. A client or proxy reading the connection will interpret those extra bytes as the start of the next response, leading to response desync. This is a particularly dangerous defect in proxy environments where multiple clients share connections. + +## Sources + +- [RFC 9110 §9.3.2 -- HEAD](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.2) diff --git a/docs/content/docs/request-line/options-allow.md b/docs/content/docs/request-line/options-allow.md new file mode 100644 index 0000000..0f30be7 --- /dev/null +++ b/docs/content/docs/request-line/options-allow.md @@ -0,0 +1,35 @@ +--- +title: "OPTIONS-ALLOW" +description: "OPTIONS-ALLOW test documentation" +weight: 18 +--- + +| | | +|---|---| +| **Test ID** | `COMP-OPTIONS-ALLOW` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 §9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) | +| **Requirement** | SHOULD | +| **Expected** | `2xx` with `Allow` header | + +## What it sends + +An OPTIONS request to the root path, asking the server to describe its capabilities for that resource. + +```http +OPTIONS / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "A server generating a successful response to OPTIONS SHOULD send any header that might indicate optional features implemented by the server and applicable to the target resource (e.g., Allow)." -- RFC 9110 Section 9.3.7 + +## Why it matters + +OPTIONS is the standard mechanism for clients to discover which methods a resource supports. The Allow header is the primary vehicle for this information. Without it, the OPTIONS response provides no actionable data. API clients and CORS preflight logic depend on this header to function correctly. + +## Sources + +- [RFC 9110 §9.3.7 -- OPTIONS](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) diff --git a/docs/content/docs/request-line/unknown-method.md b/docs/content/docs/request-line/unknown-method.md new file mode 100644 index 0000000..4ba497f --- /dev/null +++ b/docs/content/docs/request-line/unknown-method.md @@ -0,0 +1,41 @@ +--- +title: "UNKNOWN-METHOD" +description: "UNKNOWN-METHOD test documentation" +weight: 16 +--- + +| | | +|---|---| +| **Test ID** | `COMP-UNKNOWN-METHOD` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 §9.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.1) | +| **Requirement** | SHOULD | +| **Expected** | `501`, `405`, or `400` | + +## What it sends + +A request with a completely fabricated method name that no server should recognize. + +```http +FOOBAR / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "An origin server that receives a request method that is unrecognized or not implemented SHOULD respond with the 501 (Not Implemented) status code." -- RFC 9110 Section 9.1 + +The RFC also states: + +> "An origin server that receives a request method that is recognized and implemented, but not allowed for the target resource, SHOULD respond with the 405 (Method Not Allowed) status code." -- RFC 9110 Section 9.1 + +Since `FOOBAR` is not a recognized method, 501 is the most appropriate response. 405 and 400 are also acceptable alternatives. + +## Why it matters + +A server that silently accepts unknown methods may execute unintended logic or expose resources that should only be available through specific methods. Proper rejection ensures that only well-defined HTTP semantics are applied to requests. + +## Sources + +- [RFC 9110 §9.1 -- Overview](https://www.rfc-editor.org/rfc/rfc9110#section-9.1) diff --git a/docs/content/docs/rfc-requirement-dashboard.md b/docs/content/docs/rfc-requirement-dashboard.md index 30d786b..03a6a40 100644 --- a/docs/content/docs/rfc-requirement-dashboard.md +++ b/docs/content/docs/rfc-requirement-dashboard.md @@ -1,6 +1,6 @@ --- title: "RFC Requirement Dashboard" -description: "Complete RFC 2119 requirement-level analysis for all 148 Http11Probe tests" +description: "Complete RFC 2119 requirement-level analysis for all 157 Http11Probe tests" weight: 2 breadcrumbs: false --- @@ -11,14 +11,14 @@ This dashboard classifies every Http11Probe test by its [RFC 2119](https://www.r | Requirement Level | Count | Meaning (RFC 2119) | |---|---|---| -| **MUST** | 84 | Absolute requirement — no compliant implementation may deviate | -| **SHOULD** | 24 | Recommended — valid exceptions exist but must be understood | +| **MUST** | 89 | Absolute requirement — no compliant implementation may deviate | +| **SHOULD** | 27 | Recommended — valid exceptions exist but must be understood | | **MAY** | 10 | Truly optional — either behavior is fully compliant | | **"ought to"** | 1 | Weaker than SHOULD — recommended but not normative | -| **Unscored** | 18 | Informational — no pass/fail judgement | +| **Unscored** | 19 | Informational — no pass/fail judgement | | **N/A** | 11 | Best-practice / no single RFC verb applies | -**Total: 148 tests** +**Total: 157 tests** --- @@ -123,10 +123,15 @@ The RFC requires rejection, but the mechanism (400 status or connection close) h | 82 | `COMP-UNKNOWN-TE-501` | Compliance | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | "A server that receives a request message with a transfer coding it does not understand **SHOULD** respond with 501." Combined with unknown-TE-without-CL making body length indeterminate: **MUST** reject. | | 83 | `SMUG-TE-TRAILING-SPACE` | Smuggling | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | "chunked " (with trailing space) is not an exact match for the registered coding "chunked". Combined with CL present: "the server **MUST** close the connection after responding." | | 84 | `MAL-POST-CL-HUGE-NO-BODY` | Malformed | [RFC 9112 §6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) | "If the sender closes the connection or the recipient times out before the indicated number of octets are received, the recipient **MUST** consider the message to be incomplete and close the connection." | +| 85 | `COMP-HEAD-NO-BODY` | Compliance | [RFC 9110 §9.3.2](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.2) | "The HEAD method is identical to GET except that the server **MUST NOT** send content in the response." | +| 86 | `COMP-405-ALLOW` | Compliance | [RFC 9110 §15.5.6](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.6) | "The origin server **MUST** generate an Allow header field in a 405 response containing a list of the target resource's currently supported methods." | +| 87 | `COMP-DATE-HEADER` | Compliance | [RFC 9110 §6.6.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.6.1) | "An origin server with a clock **MUST** generate a Date header field in all 2xx (Successful), 3xx (Redirection), and 4xx (Client Error) responses." | +| 88 | `COMP-NO-1XX-HTTP10` | Compliance | [RFC 9110 §15.2](https://www.rfc-editor.org/rfc/rfc9110#section-15.2) | "Since HTTP/1.0 did not define any 1xx status codes, a server **MUST NOT** send a 1xx response to an HTTP/1.0 client." | +| 89 | `COMP-NO-CL-IN-204` | Compliance | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | "A server **MUST NOT** send a Content-Length header field in any response with a status code of 1xx (Informational) or 204 (No Content)." | --- -## SHOULD-Level Requirements (24 tests) +## SHOULD-Level Requirements (27 tests) The RFC recommends this behavior. Valid exceptions exist but must be understood and justified. @@ -156,6 +161,9 @@ The RFC recommends this behavior. Valid exceptions exist but must be understood | 22 | `MAL-LONG-METHOD` | Malformed | [RFC 9112 §3](https://www.rfc-editor.org/rfc/rfc9112#section-3) | "A server that receives a method longer than any that it implements **SHOULD** respond with a 501 (Not Implemented) status code." | | 23 | `MAL-LONG-HEADER-VALUE` | Malformed | [RFC 9110 §5.4](https://www.rfc-editor.org/rfc/rfc9110#section-5.4) | "A server that receives a request header field line, field value, or set of fields larger than it wishes to process **MUST** respond with an appropriate 4xx (Client Error) status code." (MUST for 4xx, SHOULD for having a limit.) | | 24 | `MAL-LONG-HEADER-NAME` | Malformed | [RFC 9110 §5.4](https://www.rfc-editor.org/rfc/rfc9110#section-5.4) | Same as above. | +| 25 | `COMP-UNKNOWN-METHOD` | Compliance | [RFC 9110 §9.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.1) | "An origin server that receives a request method that is unrecognized or not implemented **SHOULD** respond with the 501 (Not Implemented) status code." | +| 26 | `COMP-OPTIONS-ALLOW` | Compliance | [RFC 9110 §9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) | "A server generating a successful response to OPTIONS **SHOULD** send any header that might indicate optional features implemented by the server and applicable to the target resource (e.g., Allow)." | +| 27 | `COMP-CONTENT-TYPE` | Compliance | [RFC 9110 §8.3](https://www.rfc-editor.org/rfc/rfc9110#section-8.3) | "A sender that generates a message containing content **SHOULD** generate a Content-Type header field in the message." | --- @@ -188,7 +196,7 @@ Weaker than SHOULD — recommends but does not normatively require. --- -## Unscored Tests (18 tests) +## Unscored Tests (19 tests) These tests are informational — they produce warnings but never fail. @@ -196,22 +204,23 @@ These tests are informational — they produce warnings but never fail. |---|---------|-------|-----|-------| | 1 | `SMUG-TRANSFER_ENCODING` | Smuggling | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | `Transfer_Encoding` (underscore) is a valid token but not the standard header. Some parsers normalize. | | 2 | `SMUG-CL-COMMA-SAME` | Smuggling | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | "A recipient of a Content-Length header field value consisting of the same decimal value repeated as a comma-separated list **MAY** either reject the message as invalid or replace that invalid field value with a single instance." | -| 3 | `SMUG-CHUNKED-WITH-PARAMS` | Smuggling | [RFC 9112 §7](https://www.rfc-editor.org/rfc/rfc9112#section-7) | "The chunked coding does not define any parameters. Their presence **SHOULD** be treated as an error." | -| 4 | `SMUG-EXPECT-100-CL` | Smuggling | [RFC 9110 §10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) | Expect: 100-continue with CL — standard behavior, tested for proxy interaction. | -| 5 | `SMUG-TRAILER-CL` | Smuggling | [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) | Content-Length in trailers — prohibited trailer field. **MUST NOT** be used for framing. | -| 6 | `SMUG-TRAILER-TE` | Smuggling | [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) | Transfer-Encoding in trailers — prohibited trailer field. | -| 7 | `SMUG-TRAILER-HOST` | Smuggling | [RFC 9110 §6.5.2](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.2) | Host in trailers — must not be used for routing. | -| 8 | `SMUG-TRAILER-AUTH` | Smuggling | [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) | Authorization in trailers — prohibited trailer field. | -| 9 | `SMUG-TRAILER-CONTENT-TYPE` | Smuggling | [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) | Content-Type in trailers — prohibited trailer field. | -| 10 | `SMUG-HEAD-CL-BODY` | Smuggling | [RFC 9110 §9.3.2](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.2) | HEAD with body — server must not leave body on connection. | -| 11 | `SMUG-OPTIONS-CL-BODY` | Smuggling | [RFC 9110 §9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) | OPTIONS with body — server should consume or reject body. | -| 12 | `SMUG-TE-TAB-BEFORE-VALUE` | Smuggling | [RFC 9110 §5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) | Tab as OWS before TE value — valid per `OWS = *( SP / HTAB )`. | -| 13 | `SMUG-ABSOLUTE-URI-HOST-MISMATCH` | Smuggling | [RFC 9112 §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2) | Absolute-form URI host differs from Host header — routing confusion vector. | -| 14 | `COMP-ABSOLUTE-FORM` | Compliance | [RFC 9112 §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2) | Absolute-form request-target — server **MUST** accept per RFC but many reject. | -| 15 | `COMP-METHOD-CASE` | Compliance | [RFC 9110 §9.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.1) | Methods are case-sensitive. Lowercase "get" is an unknown method. Server **SHOULD** respond 501. | -| 16 | `MAL-RANGE-OVERLAPPING` | Malformed | [RFC 9110 §14.2](https://www.rfc-editor.org/rfc/rfc9110#section-14.2) | "A server that supports range requests **MAY** ignore or reject a Range header field that contains... a ranges-specifier with more than two overlapping ranges." | -| 17 | `MAL-URL-BACKSLASH` | Malformed | N/A | Backslash is not a valid URI character. Some servers normalize to `/`. | -| 18 | `NORM-CASE-TE` | Normalization | N/A | All-uppercase TRANSFER-ENCODING — tests header name case normalization. | +| 3 | `SMUG-CL-COMMA-TRIPLE` | Smuggling | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | Same — three comma-separated identical CL values. Extended merge test. | +| 4 | `SMUG-CHUNKED-WITH-PARAMS` | Smuggling | [RFC 9112 §7](https://www.rfc-editor.org/rfc/rfc9112#section-7) | "The chunked coding does not define any parameters. Their presence **SHOULD** be treated as an error." | +| 5 | `SMUG-EXPECT-100-CL` | Smuggling | [RFC 9110 §10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) | Expect: 100-continue with CL — standard behavior, tested for proxy interaction. | +| 6 | `SMUG-TRAILER-CL` | Smuggling | [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) | Content-Length in trailers — prohibited trailer field. **MUST NOT** be used for framing. | +| 7 | `SMUG-TRAILER-TE` | Smuggling | [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) | Transfer-Encoding in trailers — prohibited trailer field. | +| 8 | `SMUG-TRAILER-HOST` | Smuggling | [RFC 9110 §6.5.2](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.2) | Host in trailers — must not be used for routing. | +| 9 | `SMUG-TRAILER-AUTH` | Smuggling | [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) | Authorization in trailers — prohibited trailer field. | +| 10 | `SMUG-TRAILER-CONTENT-TYPE` | Smuggling | [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) | Content-Type in trailers — prohibited trailer field. | +| 11 | `SMUG-HEAD-CL-BODY` | Smuggling | [RFC 9110 §9.3.2](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.2) | HEAD with body — server must not leave body on connection. | +| 12 | `SMUG-OPTIONS-CL-BODY` | Smuggling | [RFC 9110 §9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) | OPTIONS with body — server should consume or reject body. | +| 13 | `SMUG-TE-TAB-BEFORE-VALUE` | Smuggling | [RFC 9110 §5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) | Tab as OWS before TE value — valid per `OWS = *( SP / HTAB )`. | +| 14 | `SMUG-ABSOLUTE-URI-HOST-MISMATCH` | Smuggling | [RFC 9112 §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2) | Absolute-form URI host differs from Host header — routing confusion vector. | +| 15 | `COMP-ABSOLUTE-FORM` | Compliance | [RFC 9112 §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2) | Absolute-form request-target — server **MUST** accept per RFC but many reject. | +| 16 | `COMP-METHOD-CASE` | Compliance | [RFC 9110 §9.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.1) | Methods are case-sensitive. Lowercase "get" is an unknown method. Server **SHOULD** respond 501. | +| 17 | `MAL-RANGE-OVERLAPPING` | Malformed | [RFC 9110 §14.2](https://www.rfc-editor.org/rfc/rfc9110#section-14.2) | "A server that supports range requests **MAY** ignore or reject a Range header field that contains... a ranges-specifier with more than two overlapping ranges." | +| 18 | `MAL-URL-BACKSLASH` | Malformed | N/A | Backslash is not a valid URI character. Some servers normalize to `/`. | +| 19 | `NORM-CASE-TE` | Normalization | N/A | All-uppercase TRANSFER-ENCODING — tests header name case normalization. | --- @@ -237,17 +246,17 @@ These tests don't map to a single RFC 2119 keyword but enforce defensive best pr ## Requirement Level by Suite -### Compliance Suite (57 tests) +### Compliance Suite (65 tests) | Level | Tests | |-------|-------| -| MUST | 38 | -| SHOULD | 10 | +| MUST | 43 | +| SHOULD | 13 | | MAY | 6 | | Unscored | 2 | | N/A | 1 | -### Smuggling Suite (60 tests) +### Smuggling Suite (61 tests) | Level | Tests | |-------|-------| @@ -255,7 +264,7 @@ These tests don't map to a single RFC 2119 keyword but enforce defensive best pr | SHOULD | 9 | | MAY | 3 | | "ought to" | 1 | -| Unscored | 13 | +| Unscored | 14 | ### Malformed Input Suite (26 tests) @@ -295,13 +304,17 @@ These tests don't map to a single RFC 2119 keyword but enforce defensive best pr | RFC 9112 §7.1.2 | 1 | Chunked trailer section | | RFC 9112 §9.3-9.6 | 2 | Connection management | | RFC 9110 §5.4-5.6 | 7 | Field limits, values, lists, tokens | +| RFC 9110 §6.6.1 | 1 | Date header | | RFC 9110 §7.2 | 1 | Host header semantics | | RFC 9110 §7.8 | 4 | Upgrade | -| RFC 9110 §8.6 | 12 | Content-Length semantics | -| RFC 9110 §9.1-9.3 | 6 | Methods (GET, HEAD, CONNECT, OPTIONS, TRACE) | +| RFC 9110 §8.3 | 1 | Content-Type | +| RFC 9110 §8.6 | 14 | Content-Length semantics | +| RFC 9110 §9.1-9.3 | 9 | Methods (GET, HEAD, CONNECT, OPTIONS, TRACE) | | RFC 9110 §10.1.1 | 2 | Expect header | | RFC 9110 §6.5 | 5 | Trailer field restrictions | | RFC 9110 §14.2 | 1 | Range requests | +| RFC 9110 §15.2 | 1 | 1xx status codes | +| RFC 9110 §15.5.6 | 1 | 405 Method Not Allowed | | RFC 6455 | 2 | WebSocket handshake | | RFC 6585 | 3 | 431 status code | | RFC 3629 | 1 | UTF-8 encoding | diff --git a/docs/content/docs/smuggling/_index.md b/docs/content/docs/smuggling/_index.md index 8b6d85b..f83a187 100644 --- a/docs/content/docs/smuggling/_index.md +++ b/docs/content/docs/smuggling/_index.md @@ -113,6 +113,7 @@ For these, `400` is the strict/safe response and `2xx` is RFC-compliant. Http11P {{< card link="te-case-mismatch" title="TE-CASE-MISMATCH" subtitle="'Chunked' vs 'chunked'. Case is valid per RFC." >}} {{< card link="transfer-encoding-underscore" title="TRANSFER_ENCODING" subtitle="Underscore instead of hyphen in header name." >}} {{< card link="cl-comma-same" title="CL-COMMA-SAME" subtitle="Comma-separated identical CL values." >}} + {{< card link="cl-comma-triple" title="CL-COMMA-TRIPLE" subtitle="Three comma-separated identical CL values." >}} {{< card link="chunked-with-params" title="CHUNKED-WITH-PARAMS" subtitle="Parameters on chunked encoding." >}} {{< card link="expect-100-cl" title="EXPECT-100-CL" subtitle="Expect: 100-continue with Content-Length." >}} {{< card link="trailer-cl" title="TRAILER-CL" subtitle="Content-Length in chunked trailers (prohibited)." >}} diff --git a/docs/content/docs/smuggling/cl-comma-triple.md b/docs/content/docs/smuggling/cl-comma-triple.md new file mode 100644 index 0000000..aadf31a --- /dev/null +++ b/docs/content/docs/smuggling/cl-comma-triple.md @@ -0,0 +1,37 @@ +--- +title: "CL-COMMA-TRIPLE" +description: "CL-COMMA-TRIPLE test documentation" +weight: 61 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-COMMA-TRIPLE` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | Unscored | +| **Expected** | `400` or `2xx` | + +## What it sends + +A POST request with three comma-separated identical Content-Length values, extending the duplicate-value merge test. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5, 5, 5\r\n +\r\n +hello +``` + +## What the RFC says + +> "A recipient of a Content-Length header field value consisting of the same decimal value repeated as a comma-separated list (e.g, 'Content-Length: 42, 42') MAY either reject the message as invalid or replace that invalid field value with a single instance of the decimal value, since this is likely the result of a duplicate being appended by an intermediary." -- RFC 9110 Section 8.6 + +## Why it matters + +While the two-value case (`5, 5`) is the example given in the RFC, real-world intermediaries may append the header multiple times, producing three or more repetitions. Servers that handle the two-value case correctly may fail on three values if their parsing logic only checks for exactly one comma. This test verifies that the merge-or-reject logic generalizes beyond the minimum RFC example. A server that rejects is being strict (pass); a server that merges to the single value is RFC-compliant (warn). + +## Sources + +- [RFC 9110 §8.6 -- Content-Length](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) diff --git a/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs b/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs index 2f9400a..33d74d4 100644 --- a/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs +++ b/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs @@ -23,13 +23,17 @@ internal static class DocsUrlMap // content-length ["RFC9112-6.1-CL-NON-NUMERIC"] = "content-length/cl-non-numeric", ["RFC9112-6.1-CL-PLUS-SIGN"] = "content-length/cl-plus-sign", + ["COMP-NO-CL-IN-204"] = "content-length/no-cl-in-204", // headers ["COMP-CONNECTION-CLOSE"] = "headers/connection-close", + ["COMP-CONTENT-TYPE"] = "headers/content-type-presence", + ["COMP-DATE-HEADER"] = "headers/date-header", ["RFC9112-5-EMPTY-HEADER-NAME"] = "headers/empty-header-name", ["COMP-EXPECT-UNKNOWN"] = "headers/expect-unknown", ["RFC9112-5-HEADER-NO-COLON"] = "headers/header-no-colon", ["COMP-HTTP10-DEFAULT-CLOSE"] = "headers/http10-default-close", + ["COMP-NO-1XX-HTTP10"] = "headers/no-1xx-http10", ["RFC9112-5-INVALID-HEADER-NAME"] = "headers/invalid-header-name", ["RFC9112-5.1-OBS-FOLD"] = "headers/obs-fold", ["RFC9110-5.6.2-SP-BEFORE-COLON"] = "headers/sp-before-colon", @@ -51,6 +55,7 @@ internal static class DocsUrlMap ["COMP-LEADING-CRLF"] = "line-endings/leading-crlf", // request-line + ["COMP-405-ALLOW"] = "request-line/405-allow", ["COMP-ABSOLUTE-FORM"] = "request-line/absolute-form", ["COMP-ASTERISK-WITH-GET"] = "request-line/asterisk-with-get", ["RFC9112-3.2-FRAGMENT-IN-TARGET"] = "request-line/fragment-in-target", @@ -58,13 +63,16 @@ internal static class DocsUrlMap ["COMP-HTTP12-VERSION"] = "request-line/http12-version", ["RFC9112-2.3-INVALID-VERSION"] = "request-line/invalid-version", ["COMP-METHOD-CASE"] = "request-line/method-case", + ["COMP-HEAD-NO-BODY"] = "request-line/head-no-body", ["COMP-METHOD-CONNECT"] = "request-line/method-connect", ["COMP-METHOD-TRACE"] = "request-line/method-trace", ["RFC9112-3-MISSING-TARGET"] = "request-line/missing-target", ["RFC9112-3-MULTI-SP-REQUEST-LINE"] = "request-line/multi-sp-request-line", + ["COMP-OPTIONS-ALLOW"] = "request-line/options-allow", ["COMP-OPTIONS-STAR"] = "request-line/options-star", ["COMP-REQUEST-LINE-TAB"] = "request-line/request-line-tab", ["COMP-TRACE-WITH-BODY"] = "request-line/trace-with-body", + ["COMP-UNKNOWN-METHOD"] = "request-line/unknown-method", ["COMP-UNKNOWN-TE-501"] = "request-line/unknown-te-501", ["COMP-VERSION-LEADING-ZEROS"] = "request-line/version-leading-zeros", ["COMP-VERSION-MISSING-MINOR"] = "request-line/version-missing-minor", diff --git a/src/Http11Probe/TestCases/Suites/ComplianceSuite.cs b/src/Http11Probe/TestCases/Suites/ComplianceSuite.cs index 6d8c82e..fcd9771 100644 --- a/src/Http11Probe/TestCases/Suites/ComplianceSuite.cs +++ b/src/Http11Probe/TestCases/Suites/ComplianceSuite.cs @@ -1047,6 +1047,191 @@ public static IEnumerable GetTestCases() ExpectedStatus = StatusCodeRange.Range(200, 299) } }; + + // ── RFC 9110 response semantics ────────────────────────────── + + yield return new TestCase + { + Id = "COMP-HEAD-NO-BODY", + Description = "HEAD response must not contain a message body", + Category = TestCategory.Compliance, + RfcReference = "RFC 9110 §9.3.2", + PayloadFactory = ctx => MakeRequest( + $"HEAD / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "2xx with no body", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Fail : TestVerdict.Fail; + if (response.StatusCode is >= 200 and < 300) + return string.IsNullOrEmpty(response.Body) ? TestVerdict.Pass : TestVerdict.Fail; + return TestVerdict.Fail; + } + } + }; + + yield return new TestCase + { + Id = "COMP-UNKNOWN-METHOD", + Description = "Unrecognized method should be rejected with 501 or 405", + Category = TestCategory.Compliance, + RfcReference = "RFC 9110 §9.1", + PayloadFactory = ctx => MakeRequest( + $"FOOBAR / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "501/405/400 or close", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; + if (response.StatusCode is 501 or 405 or 400) + return TestVerdict.Pass; + if (response.StatusCode is >= 200 and < 300) + return TestVerdict.Warn; + return TestVerdict.Fail; + } + } + }; + + yield return new TestCase + { + Id = "COMP-405-ALLOW", + Description = "405 response must include an Allow header", + Category = TestCategory.Compliance, + RfcReference = "RFC 9110 §15.5.6", + PayloadFactory = ctx => MakeRequest( + $"DELETE / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "405 + Allow header", + CustomValidator = (response, state) => + { + if (response is null) + return TestVerdict.Fail; + if (response.StatusCode == 405) + return response.Headers.ContainsKey("Allow") ? TestVerdict.Pass : TestVerdict.Fail; + // Server didn't return 405 — can't verify the Allow requirement + return TestVerdict.Warn; + } + } + }; + + yield return new TestCase + { + Id = "COMP-DATE-HEADER", + Description = "Origin server must include Date header in responses", + Category = TestCategory.Compliance, + RfcReference = "RFC 9110 §6.6.1", + PayloadFactory = ctx => MakeRequest( + $"GET / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "2xx with Date header", + CustomValidator = (response, state) => + { + if (response is null) + return TestVerdict.Fail; + if (response.StatusCode is >= 200 and < 300) + return response.Headers.ContainsKey("Date") ? TestVerdict.Pass : TestVerdict.Fail; + return TestVerdict.Fail; + } + } + }; + + yield return new TestCase + { + Id = "COMP-NO-1XX-HTTP10", + Description = "Server must not send 1xx responses to an HTTP/1.0 client", + Category = TestCategory.Compliance, + RfcReference = "RFC 9110 §15.2", + PayloadFactory = ctx => MakeRequest( + $"POST / HTTP/1.0\r\nHost: {ctx.HostHeader}\r\nExpect: 100-continue\r\nContent-Length: 5\r\n\r\nhello"), + Expected = new ExpectedBehavior + { + Description = "non-1xx response", + CustomValidator = (response, state) => + { + if (response is null) + return state is ConnectionState.ClosedByServer or ConnectionState.TimedOut + ? TestVerdict.Pass + : TestVerdict.Fail; + // Server sent 100 Continue to an HTTP/1.0 client — violation + if (response.StatusCode is >= 100 and < 200) + return TestVerdict.Fail; + return TestVerdict.Pass; + } + } + }; + + yield return new TestCase + { + Id = "COMP-NO-CL-IN-204", + Description = "Server must not send Content-Length in a 204 response", + Category = TestCategory.Compliance, + RfcReference = "RFC 9110 §8.6", + PayloadFactory = ctx => MakeRequest( + $"OPTIONS / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "204 without CL", + CustomValidator = (response, state) => + { + if (response is null) + return TestVerdict.Fail; + if (response.StatusCode == 204) + return response.Headers.ContainsKey("Content-Length") ? TestVerdict.Fail : TestVerdict.Pass; + // Server didn't return 204 — can't verify the CL prohibition + return TestVerdict.Warn; + } + } + }; + + yield return new TestCase + { + Id = "COMP-OPTIONS-ALLOW", + Description = "OPTIONS response should include Allow header listing supported methods", + Category = TestCategory.Compliance, + RfcReference = "RFC 9110 §9.3.7", + PayloadFactory = ctx => MakeRequest( + $"OPTIONS / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "2xx with Allow header", + CustomValidator = (response, state) => + { + if (response is null) + return TestVerdict.Fail; + if (response.StatusCode is >= 200 and < 300) + return response.Headers.ContainsKey("Allow") ? TestVerdict.Pass : TestVerdict.Warn; + return TestVerdict.Fail; + } + } + }; + + yield return new TestCase + { + Id = "COMP-CONTENT-TYPE", + Description = "Response with content should include Content-Type header", + Category = TestCategory.Compliance, + RfcReference = "RFC 9110 §8.3", + PayloadFactory = ctx => MakeRequest( + $"GET / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "2xx with Content-Type", + CustomValidator = (response, state) => + { + if (response is null) + return TestVerdict.Fail; + if (response.StatusCode is >= 200 and < 300) + return response.Headers.ContainsKey("Content-Type") ? TestVerdict.Pass : TestVerdict.Warn; + return TestVerdict.Fail; + } + } + }; } private static byte[] MakeRequest(string request) => Encoding.ASCII.GetBytes(request); diff --git a/src/Http11Probe/TestCases/Suites/SmugglingSuite.cs b/src/Http11Probe/TestCases/Suites/SmugglingSuite.cs index 9028a21..3b986e5 100644 --- a/src/Http11Probe/TestCases/Suites/SmugglingSuite.cs +++ b/src/Http11Probe/TestCases/Suites/SmugglingSuite.cs @@ -926,6 +926,31 @@ public static IEnumerable GetTestCases() } }; + yield return new TestCase + { + Id = "SMUG-CL-COMMA-TRIPLE", + Description = "Content-Length with three comma-separated identical values — extended merge test", + Category = TestCategory.Smuggling, + RfcReference = "RFC 9110 §8.6", + Scored = false, + PayloadFactory = ctx => MakeRequest( + $"POST / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nContent-Length: 5, 5, 5\r\n\r\nhello"), + Expected = new ExpectedBehavior + { + Description = "400 or 2xx", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; + if (response.StatusCode == 400) + return TestVerdict.Pass; + if (response.StatusCode is >= 200 and < 300) + return TestVerdict.Warn; + return TestVerdict.Fail; + } + } + }; + yield return new TestCase { Id = "SMUG-CHUNKED-WITH-PARAMS",