diff --git a/docs/content/compliance/_index.md b/docs/content/compliance/_index.md index 99e16f1..c29c7b5 100644 --- a/docs/content/compliance/_index.md +++ b/docs/content/compliance/_index.md @@ -42,7 +42,8 @@ Each test sends a request that violates a specific **MUST** or **MUST NOT** requ 'COMP-ABSOLUTE-FORM', 'COMP-METHOD-CASE','COMP-REQUEST-LINE-TAB', 'COMP-VERSION-MISSING-MINOR','COMP-VERSION-LEADING-ZEROS', - 'COMP-VERSION-WHITESPACE','COMP-HTTP12-VERSION', + 'COMP-VERSION-WHITESPACE','COMP-VERSION-CASE','COMP-HTTP12-VERSION', + 'COMP-LONG-URL-OK','COMP-SPACE-IN-TARGET', 'RFC9112-5.1-OBS-FOLD','RFC9110-5.6.2-SP-BEFORE-COLON', 'RFC9112-5-EMPTY-HEADER-NAME','RFC9112-5-INVALID-HEADER-NAME', 'RFC9112-5-HEADER-NO-COLON', @@ -57,12 +58,14 @@ Each test sends a request that violates a specific **MUST** or **MUST NOT** requ 'COMP-CHUNKED-EMPTY','COMP-CHUNKED-NO-FINAL', 'COMP-GET-WITH-CL-BODY','COMP-CHUNKED-EXTENSION', 'COMP-CHUNKED-TRAILER-VALID','COMP-CHUNKED-HEX-UPPERCASE', - 'COMP-RANGE-POST' + 'COMP-RANGE-POST','COMP-RANGE-INVALID', + 'COMP-DUPLICATE-CT','COMP-POST-UNSUPPORTED-CT', + 'COMP-ACCEPT-NONSENSE' ]}, { key: 'methods-upgrade', label: 'Methods & Upgrade', testIds: [ 'COMP-METHOD-CONNECT', 'COMP-UNKNOWN-TE-501','COMP-EXPECT-UNKNOWN','COMP-METHOD-TRACE', - 'COMP-TRACE-WITH-BODY', + 'COMP-TRACE-WITH-BODY','COMP-TRACE-SENSITIVE', 'COMP-UPGRADE-POST','COMP-UPGRADE-MISSING-CONN', 'COMP-UPGRADE-UNKNOWN','COMP-UPGRADE-INVALID-VER','COMP-UPGRADE-HTTP10', 'COMP-CONNECTION-CLOSE','COMP-HTTP10-DEFAULT-CLOSE','COMP-HTTP10-NO-HOST' diff --git a/docs/content/docs/body/post-unsupported-ct.md b/docs/content/docs/body/post-unsupported-ct.md new file mode 100644 index 0000000..0db9c6e --- /dev/null +++ b/docs/content/docs/body/post-unsupported-ct.md @@ -0,0 +1,46 @@ +--- +title: "POST-UNSUPPORTED-CT" +description: "POST-UNSUPPORTED-CT test documentation" +weight: 15 +--- + +| | | +|---|---| +| **Test ID** | `COMP-POST-UNSUPPORTED-CT` | +| **Category** | Compliance | +| **Scored** | No | +| **RFC** | [RFC 9110 §15.5.16](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.16) | +| **RFC Level** | MAY | +| **Expected** | `415` or `2xx` | + +## What it sends + +A POST request with an unrecognized `Content-Type`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +Content-Type: application/x-nonsense\r\n +\r\n +hello +``` + +## What the RFC says + +> "The 415 (Unsupported Media Type) status code indicates that the origin server is refusing to service the request because the content is in a format not supported by this method on the target resource." — RFC 9110 §15.5.16 + +The server is not required to reject unsupported content types — it may choose to accept the body regardless of the declared type. + +## Why it matters + +A server that validates `Content-Type` and returns `415` for unsupported formats provides better API hygiene, helping clients detect misconfigured requests early. A server that ignores unknown content types and processes the body anyway is also valid behavior — many servers treat the body as opaque bytes regardless of the declared type. + +## Verdicts + +- **Pass** — Server returns `415` (validates content type) or `2xx` (accepts any type) +- **Warn** — Server returns an unexpected status + +## Sources + +- [RFC 9110 §15.5.16](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.16) diff --git a/docs/content/docs/body/range-invalid.md b/docs/content/docs/body/range-invalid.md new file mode 100644 index 0000000..f8e2e1c --- /dev/null +++ b/docs/content/docs/body/range-invalid.md @@ -0,0 +1,48 @@ +--- +title: "RANGE-INVALID" +description: "RANGE-INVALID test documentation" +weight: 14 +--- + +| | | +|---|---| +| **Test ID** | `COMP-RANGE-INVALID` | +| **Category** | Compliance | +| **Scored** | No | +| **RFC** | [RFC 9110 §14.2](https://www.rfc-editor.org/rfc/rfc9110#section-14.2) | +| **RFC Level** | MAY | +| **Expected** | `2xx` (ignore) or `416` | + +## What it sends + +A GET request with a syntactically invalid `Range` header. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Range: bytes=abc-xyz\r\n +\r\n +``` + +The range value `abc-xyz` does not match the required integer format. + +## What the RFC says + +> "A server MAY ignore the Range header field." — RFC 9110 §14.2 + +> "An origin server MUST ignore a Range header field that contains a range unit it does not understand. A proxy MAY discard a Range header field that contains a range unit it does not understand." — RFC 9110 §14.2 + +> "A server that supports range requests MAY ignore or reject a Range header field that consists of more than two overlapping ranges, or a set of many small ranges that are not listed in ascending order, since both are indications of either a broken client or a deliberate denial-of-service attack." — RFC 9110 §14.2 + +## Why it matters + +A server that receives an unparseable Range value should either ignore it (serve the full resource with `200`) or reject it with `416 Range Not Satisfiable`. Returning `206 Partial Content` with bogus range values could expose unexpected data or cause client-side parsing errors. + +## Verdicts + +- **Pass** — Server returns `2xx` (ignoring the invalid range) or `416` +- **Warn** — Server returns an unexpected status + +## Sources + +- [RFC 9110 §14.2](https://www.rfc-editor.org/rfc/rfc9110#section-14.2) diff --git a/docs/content/docs/headers/accept-nonsense.md b/docs/content/docs/headers/accept-nonsense.md new file mode 100644 index 0000000..fda8a4a --- /dev/null +++ b/docs/content/docs/headers/accept-nonsense.md @@ -0,0 +1,45 @@ +--- +title: "ACCEPT-NONSENSE" +description: "ACCEPT-NONSENSE test documentation" +weight: 21 +--- + +| | | +|---|---| +| **Test ID** | `COMP-ACCEPT-NONSENSE` | +| **Category** | Compliance | +| **Scored** | No | +| **RFC** | [RFC 9110 §12.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-12.5.1) | +| **RFC Level** | SHOULD | +| **Expected** | `406` preferred, `2xx` acceptable | + +## What it sends + +A GET request with an `Accept` header requesting a non-existent media type. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Accept: application/x-nonsense\r\n +\r\n +``` + +## What the RFC says + +> "A request without any Accept header field implies that the user agent will accept any media type in response." — RFC 9110 §12.5.1 + +> "If the header field is present in a request and none of the available representations for the response have a media type that is listed as acceptable, the origin server can either honor the header field by sending a 406 (Not Acceptable) response or disregard the header field by treating the response as if it is not subject to content negotiation for that request." — RFC 9110 §12.5.1 + +## Why it matters + +Content negotiation allows servers to serve different representations of a resource based on client capabilities. A server that returns `406 Not Acceptable` for unrecognized media types actively enforces content negotiation. A server that ignores the `Accept` header and serves a default representation is also compliant — the RFC explicitly allows both behaviors. + +## Verdicts + +- **Pass** — Server returns `406 Not Acceptable` (enforces content negotiation) +- **Warn** — Server returns `2xx` (ignores Accept, serves default representation) +- **Fail** — Server returns an unexpected error status + +## Sources + +- [RFC 9110 §12.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-12.5.1) diff --git a/docs/content/docs/headers/date-format.md b/docs/content/docs/headers/date-format.md new file mode 100644 index 0000000..211c51d --- /dev/null +++ b/docs/content/docs/headers/date-format.md @@ -0,0 +1,54 @@ +--- +title: "DATE-FORMAT" +description: "DATE-FORMAT test documentation" +weight: 20 +--- + +| | | +|---|---| +| **Test ID** | `COMP-DATE-FORMAT` | +| **Category** | Compliance | +| **Scored** | No | +| **RFC** | [RFC 9110 §5.6.7](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.7) | +| **RFC Level** | SHOULD | +| **Expected** | IMF-fixdate format | + +## What it does + +Sends a standard GET request and checks whether the `Date` response header uses the preferred IMF-fixdate format. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +The test inspects the `Date` header value in the response. + +## What the RFC says + +> "An HTTP-date value represents time as an instance of Coordinated Universal Time (UTC). The first two formats [IMF-fixdate and rfc850-date] indicate UTC by the three-letter abbreviation for Greenwich Mean Time, 'GMT'... **A recipient that parses a timestamp value in an HTTP field MUST accept all three HTTP-date formats.**" -- RFC 9110 §5.6.7 + +> "HTTP-date = IMF-fixdate / obs-date" -- RFC 9110 §5.6.7 + +> "A sender MUST generate timestamps in the IMF-fixdate format." -- RFC 9110 §5.6.7 (quoted from RFC 7231 §7.1.1.1, carried forward) + +The preferred format is **IMF-fixdate**: + +``` +Sun, 06 Nov 1994 08:49:37 GMT +``` + +## Why it matters + +While all three date formats are valid for *recipients* to accept, **senders** (including origin servers) should generate the IMF-fixdate format. Servers using obsolete formats (RFC 850 or asctime) are technically non-conforming senders, though recipients must still parse them. + +## Verdicts + +- **Pass** -- Date header present and uses IMF-fixdate format +- **Warn** -- Date header missing or uses a non-standard format + +## Sources + +- [RFC 9110 §5.6.7](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.7) +- [RFC 9110 §6.6.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.6.1) diff --git a/docs/content/docs/headers/duplicate-ct.md b/docs/content/docs/headers/duplicate-ct.md new file mode 100644 index 0000000..7a23774 --- /dev/null +++ b/docs/content/docs/headers/duplicate-ct.md @@ -0,0 +1,50 @@ +--- +title: "DUPLICATE-CT" +description: "DUPLICATE-CT test documentation" +weight: 20 +--- + +| | | +|---|---| +| **Test ID** | `COMP-DUPLICATE-CT` | +| **Category** | Compliance | +| **Scored** | Yes | +| **RFC** | [RFC 9110 §5.3](https://www.rfc-editor.org/rfc/rfc9110#section-5.3) | +| **RFC Level** | SHOULD | +| **Expected** | `400` preferred, `2xx` acceptable | + +## What it sends + +A POST request with two `Content-Type` headers that have conflicting values. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +Content-Type: text/plain\r\n +Content-Type: text/html\r\n +\r\n +hello +``` + +## What the RFC says + +> "A sender MUST NOT generate multiple header fields with the same field name in a message unless either the entire field value for that header field is defined as a comma-separated list or the header field is a well-known exception." — RFC 9110 §5.3 + +> "A recipient MAY combine multiple header fields with the same field name into one 'field-name: field-value' pair... by appending each subsequent field value to the combined field value in order, separated by a comma." — RFC 9110 §5.3 + +`Content-Type` is not a list-based header — it has a single value. Duplicate `Content-Type` headers with different values create ambiguity about which value the server uses. + +## Why it matters + +When a proxy and origin server disagree on which `Content-Type` to use, it can lead to content-type confusion attacks. An attacker could craft a request that a proxy interprets as `text/plain` while the origin processes as `text/html`, enabling XSS or other injection attacks. + +## Verdicts + +- **Pass** — Server rejects with `400` or closes the connection +- **Warn** — Server accepts with `2xx` (silently picks one value) +- **Fail** — Server returns an unexpected error status + +## Sources + +- [RFC 9110 §5.3](https://www.rfc-editor.org/rfc/rfc9110#section-5.3) diff --git a/docs/content/docs/request-line/long-url-ok.md b/docs/content/docs/request-line/long-url-ok.md new file mode 100644 index 0000000..cb1b34c --- /dev/null +++ b/docs/content/docs/request-line/long-url-ok.md @@ -0,0 +1,48 @@ +--- +title: "LONG-URL-OK" +description: "LONG-URL-OK test documentation" +weight: 31 +--- + +| | | +|---|---| +| **Test ID** | `COMP-LONG-URL-OK` | +| **Category** | Compliance | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §3](https://www.rfc-editor.org/rfc/rfc9112#section-3) | +| **RFC Level** | SHOULD | +| **Expected** | Any status except `414` | + +## What it sends + +A GET request with a ~7900-character path (well under 8000 octets total for the request-line). + +```http +GET /aaaa...aaa HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +The path contains 7900 repetitions of `a`. + +## What the RFC says + +> "A server that receives a request-target longer than any URI it wishes to parse MUST respond with a 414 (URI Too Long) status code." — RFC 9112 §3 + +> "It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets." — RFC 9112 §3 + +## Why it matters + +Servers that reject URLs well within the 8000-octet recommendation may break legitimate applications that use long query strings or path parameters. This test verifies the server can handle a request-line just under the recommended minimum. + +This is the inverse of `MAL-LONG-URL`, which tests rejection of extremely long URLs (~100KB). Together they verify a server has reasonable upper and lower bounds. + +## Verdicts + +- **Pass** — Server returns any status other than `414` +- **Fail** — Server returns `414 URI Too Long` +- **Warn** — Server closes the connection without a response + +## Sources + +- [RFC 9112 §3](https://www.rfc-editor.org/rfc/rfc9112#section-3) diff --git a/docs/content/docs/request-line/space-in-target.md b/docs/content/docs/request-line/space-in-target.md new file mode 100644 index 0000000..af5266e --- /dev/null +++ b/docs/content/docs/request-line/space-in-target.md @@ -0,0 +1,47 @@ +--- +title: "SPACE-IN-TARGET" +description: "SPACE-IN-TARGET test documentation" +weight: 32 +--- + +| | | +|---|---| +| **Test ID** | `COMP-SPACE-IN-TARGET` | +| **Category** | Compliance | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | +| **RFC Level** | MUST | +| **Expected** | `400` or connection close | + +## What it sends + +A GET request with an unencoded space inside the request-target. + +```http +GET /pa th HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +The request-target `/pa th` contains a bare space character (0x20) which is not a valid URI character. + +## What the RFC says + +> "Recipients of an invalid request-line SHOULD respond with either a 400 (Bad Request) error or a 301 (Moved Permanently) redirect with the request-target properly encoded." — RFC 9112 §3.2 + +> "request-target = origin-form / absolute-form / authority-form / asterisk-form" — RFC 9112 §3.2 + +The space character is the delimiter between the method, request-target, and HTTP-version in the request-line. An unencoded space in the target makes the request-line ambiguous — the parser sees `GET /pa th HTTP/1.1` as having four tokens instead of three. + +## Why it matters + +A server that accepts a bare space in the request-target must be performing heuristic parsing to guess where the target ends. This ambiguity is a classic source of request smuggling and cache poisoning vulnerabilities, where different parsers in a chain disagree on the boundaries of the request-line. + +## Verdicts + +- **Pass** — Server rejects with `400` or closes the connection +- **Fail** — Server accepts the request + +## Sources + +- [RFC 9112 §3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) diff --git a/docs/content/docs/request-line/trace-sensitive.md b/docs/content/docs/request-line/trace-sensitive.md new file mode 100644 index 0000000..3e82717 --- /dev/null +++ b/docs/content/docs/request-line/trace-sensitive.md @@ -0,0 +1,46 @@ +--- +title: "TRACE-SENSITIVE" +description: "TRACE-SENSITIVE test documentation" +weight: 33 +--- + +| | | +|---|---| +| **Test ID** | `COMP-TRACE-SENSITIVE` | +| **Category** | Compliance | +| **Scored** | No | +| **RFC** | [RFC 9110 §9.3.8](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.8) | +| **RFC Level** | SHOULD | +| **Expected** | Sensitive headers excluded from echo | + +## What it sends + +A TRACE request that includes an `Authorization` header with a bearer token. + +```http +TRACE / HTTP/1.1\r\n +Host: localhost:8080\r\n +Authorization: Bearer secret-token-123\r\n +\r\n +``` + +The test checks whether the echoed response body contains the sensitive token value. + +## What the RFC says + +> "A client MUST NOT generate header fields in a TRACE request containing sensitive data that might be disclosed by the response. For example, it would be foolish for a user agent to send stored credentials [RFC2617] in a TRACE request." — RFC 9110 §9.3.8 + +> "A server SHOULD exclude any request header fields that are likely to contain sensitive data when that server generates the response to a TRACE request." — RFC 9110 §9.3.8 + +## Why it matters + +TRACE echoes the received request back in the response body. If the server includes sensitive headers like `Authorization`, `Cookie`, or `Proxy-Authorization` in the echo, an attacker who can trigger a TRACE request (via XSS or other means) can steal authentication credentials. This is the basis of the Cross-Site Tracing (XST) attack. + +## Verdicts + +- **Pass** — TRACE disabled (`405`/`501`), or TRACE response excludes the Authorization header +- **Warn** — TRACE echoes the `Authorization` header including the secret token + +## Sources + +- [RFC 9110 §9.3.8](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.8) diff --git a/docs/content/docs/request-line/version-case.md b/docs/content/docs/request-line/version-case.md new file mode 100644 index 0000000..41e05ca --- /dev/null +++ b/docs/content/docs/request-line/version-case.md @@ -0,0 +1,47 @@ +--- +title: "VERSION-CASE" +description: "VERSION-CASE test documentation" +weight: 30 +--- + +| | | +|---|---| +| **Test ID** | `COMP-VERSION-CASE` | +| **Category** | Compliance | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | +| **RFC Level** | MUST | +| **Expected** | `400` or connection close | + +## What it sends + +A GET request with lowercase `http/1.1` instead of `HTTP/1.1`. + +```http +GET / http/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "HTTP-version is case-sensitive." — RFC 9112 §2.3 + +> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" — RFC 9112 §2.3 + +> "HTTP-name = %x48.54.54.50 ; 'HTTP'" — RFC 9112 §2.3 + +The ABNF specifies the exact octets `H`, `T`, `T`, `P` — only uppercase matches. + +## Why it matters + +A server that accepts `http/1.1` as valid is performing case-insensitive comparison on the HTTP version, which violates the protocol specification. While unlikely to cause security issues on its own, lenient parsing of protocol-level tokens can mask deeper parsing inconsistencies that smuggling attacks exploit. + +## Verdicts + +- **Pass** — Server rejects with `400` or closes the connection +- **Fail** — Server accepts the request + +## Sources + +- [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) diff --git a/docs/content/docs/rfc-requirement-dashboard.md b/docs/content/docs/rfc-requirement-dashboard.md index 03a6a40..30bb6c0 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 157 Http11Probe tests" +description: "Complete RFC 2119 requirement-level analysis for all 172 Http11Probe tests" weight: 2 breadcrumbs: false --- @@ -11,18 +11,18 @@ This dashboard classifies every Http11Probe test by its [RFC 2119](https://www.r | Requirement Level | Count | Meaning (RFC 2119) | |---|---|---| -| **MUST** | 89 | Absolute requirement — no compliant implementation may deviate | -| **SHOULD** | 27 | Recommended — valid exceptions exist but must be understood | +| **MUST** | 96 | Absolute requirement — no compliant implementation may deviate | +| **SHOULD** | 29 | 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** | 19 | Informational — no pass/fail judgement | +| **Unscored** | 25 | Informational — no pass/fail judgement | | **N/A** | 11 | Best-practice / no single RFC verb applies | -**Total: 157 tests** +**Total: 172 tests** --- -## MUST-Level Requirements (84 tests) +## MUST-Level Requirements (96 tests) These tests enforce absolute RFC requirements. A compliant server has no discretion — it **MUST** behave as specified. @@ -57,6 +57,8 @@ The RFC requires rejection, but the mechanism (400 status or connection close) h | 16 | `COMP-VERSION-MISSING-MINOR` | Compliance | [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | Grammar: `HTTP-version = HTTP-name "/" DIGIT "." DIGIT`. "HTTP/1" has no minor version digit — violates the grammar. **MUST** reject. | | 17 | `COMP-VERSION-LEADING-ZEROS` | Compliance | [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | Grammar: `HTTP-version = HTTP-name "/" DIGIT "." DIGIT`. Each version component is exactly one DIGIT — "01" is two digits. **MUST** reject. | | 18 | `COMP-VERSION-WHITESPACE` | Compliance | [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | Grammar: `HTTP-version = HTTP-name "/" DIGIT "." DIGIT`. No whitespace is permitted within the version token. **MUST** reject. | +| 19 | `COMP-VERSION-CASE` | Compliance | [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | "HTTP-version is case-sensitive." `HTTP-name = %x48.54.54.50` — only uppercase octets match. **MUST** reject lowercase `http/1.1`. | +| 20 | `COMP-SPACE-IN-TARGET` | Compliance | [RFC 9112 §3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | Unencoded space in request-target makes the request-line ambiguous (four tokens instead of three). Grammar: `request-line = method SP request-target SP HTTP-version`. **MUST** reject. | | 19 | `RFC9112-6.1-CL-NON-NUMERIC` | Compliance | [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) | "If a message is received without Transfer-Encoding and with an invalid Content-Length header field, then the message framing is invalid and the recipient **MUST** treat it as an unrecoverable error... the server **MUST** respond with a 400 (Bad Request) status code and then close the connection." | | 20 | `RFC9112-6.1-CL-PLUS-SIGN` | Compliance | [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) | Same as above — `Content-Length = 1*DIGIT`. A plus sign is not a DIGIT. **MUST** reject as invalid Content-Length. | | 21 | `SMUG-DUPLICATE-CL` | Smuggling | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | "If a message is received without Transfer-Encoding and with an invalid Content-Length header field, then the message framing is invalid and the recipient **MUST** treat it as an unrecoverable error." Duplicate CL with different values = invalid. | @@ -92,6 +94,11 @@ The RFC requires rejection, but the mechanism (400 status or connection close) h | 51 | `SMUG-CLTE-PIPELINE` | Smuggling | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | "Regardless, the server **MUST** close the connection after responding to such a request to avoid the potential attacks." CL+TE combined — **MUST** close connection. | | 52 | `SMUG-TECL-PIPELINE` | Smuggling | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | Same as above — TE+CL combined in reverse smuggling direction. **MUST** close connection. | | 53 | `SMUG-TE-XCHUNKED` | Smuggling | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | Unknown TE with CL present: "Regardless, the server **MUST** close the connection after responding to such a request." Combined with §6.1: "A server that receives a request message with a transfer coding it does not understand **SHOULD** respond with 501." | +| 54 | `SMUG-CLTE-CONN-CLOSE` | Smuggling | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | Sequence test: CL+TE combined, then follow-up GET on same socket. "The server **MUST** close the connection after responding to such a request." If follow-up receives a response, MUST-close violated. | +| 55 | `SMUG-TECL-CONN-CLOSE` | Smuggling | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | Same as CLTE-CONN-CLOSE with TE before CL header order. **MUST** close connection. | +| 56 | `SMUG-CLTE-KEEPALIVE` | Smuggling | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | CL+TE conflict with explicit `Connection: keep-alive`. **MUST** close connection regardless of keep-alive. | +| 57 | `SMUG-CLTE-DESYNC` | Smuggling | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | Classic CL.TE desync: CL=6 with TE=chunked body `0\r\n\r\nX`. Poison byte after CL boundary confirms desync. **MUST** close connection. | +| 58 | `SMUG-TECL-DESYNC` | Smuggling | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | Reverse TE.CL desync: TE=chunked terminates at `0\r\n\r\n` but CL=30. Extra bytes on wire confirm desync. **MUST** close connection. | | 54 | `COMP-CONNECTION-CLOSE` | Compliance | [RFC 9112 §9.6](https://www.rfc-editor.org/rfc/rfc9112#section-9.6) | "A server that receives a 'close' connection option **MUST** initiate closure of the connection after it sends the final response to the request that contained the 'close' connection option." | | 55 | `COMP-OPTIONS-STAR` | Compliance | [RFC 9112 §3.2.4](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.4) | The asterisk-form `*` is defined only for OPTIONS. A valid OPTIONS * request **MUST** be accepted. | | 56 | `COMP-POST-CL-BODY` | Compliance | [RFC 9112 §6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) | "If a valid Content-Length header field is present without Transfer-Encoding, its decimal value defines the expected message body length in octets." Server **MUST** accept a well-formed POST with matching body. | @@ -131,7 +138,7 @@ The RFC requires rejection, but the mechanism (400 status or connection close) h --- -## SHOULD-Level Requirements (27 tests) +## SHOULD-Level Requirements (29 tests) The RFC recommends this behavior. Valid exceptions exist but must be understood and justified. @@ -164,6 +171,8 @@ The RFC recommends this behavior. Valid exceptions exist but must be understood | 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." | +| 28 | `COMP-LONG-URL-OK` | Compliance | [RFC 9112 §3](https://www.rfc-editor.org/rfc/rfc9112#section-3) | "It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets." Server **SHOULD** accept ~7900-char path. | +| 29 | `COMP-DUPLICATE-CT` | Compliance | [RFC 9110 §5.3](https://www.rfc-editor.org/rfc/rfc9110#section-5.3) | "A sender **MUST NOT** generate multiple header fields with the same field name." Content-Type is not list-based — duplicate values **SHOULD** be rejected with 400. | --- @@ -196,7 +205,7 @@ Weaker than SHOULD — recommends but does not normatively require. --- -## Unscored Tests (19 tests) +## Unscored Tests (25 tests) These tests are informational — they produce warnings but never fail. @@ -221,6 +230,12 @@ These tests are informational — they produce warnings but never fail. | 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. | +| 20 | `COMP-TRACE-SENSITIVE` | Compliance | [RFC 9110 §9.3.8](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.8) | "A server **SHOULD** exclude any request header fields that are likely to contain sensitive data." TRACE with Authorization header — checks if secret is echoed. | +| 21 | `COMP-ACCEPT-NONSENSE` | Compliance | [RFC 9110 §12.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-12.5.1) | Unrecognized Accept value — server may return 406 or serve default representation. Both behaviors valid. | +| 22 | `COMP-DATE-FORMAT` | Compliance | [RFC 9110 §5.6.7](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.7) | "A sender **MUST** generate timestamps in the IMF-fixdate format." Checks Date header format. | +| 23 | `COMP-RANGE-INVALID` | Compliance | [RFC 9110 §14.2](https://www.rfc-editor.org/rfc/rfc9110#section-14.2) | "A server **MAY** ignore the Range header field." Invalid Range syntax — 2xx or 416 both acceptable. | +| 24 | `COMP-POST-UNSUPPORTED-CT` | Compliance | [RFC 9110 §15.5.16](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.16) | POST with unknown Content-Type — 415 or 2xx both acceptable. | +| 25 | `SMUG-PIPELINE-SAFE` | Smuggling | [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) | Baseline: two clean pipelined GETs. Validates sequence test infrastructure against the target. | --- @@ -246,25 +261,25 @@ These tests don't map to a single RFC 2119 keyword but enforce defensive best pr ## Requirement Level by Suite -### Compliance Suite (65 tests) +### Compliance Suite (74 tests) | Level | Tests | |-------|-------| -| MUST | 43 | -| SHOULD | 13 | +| MUST | 45 | +| SHOULD | 15 | | MAY | 6 | -| Unscored | 2 | +| Unscored | 7 | | N/A | 1 | -### Smuggling Suite (61 tests) +### Smuggling Suite (67 tests) | Level | Tests | |-------|-------| -| MUST | 34 | +| MUST | 39 | | SHOULD | 9 | | MAY | 3 | | "ought to" | 1 | -| Unscored | 14 | +| Unscored | 15 | ### Malformed Input Suite (26 tests) @@ -291,30 +306,33 @@ These tests don't map to a single RFC 2119 keyword but enforce defensive best pr | RFC Section | Tests | Topic | |-------------|-------|-------| | RFC 9112 §2.2 | 14 | Line endings, bare CR/LF, message parsing | -| RFC 9112 §2.3 | 5 | HTTP version | -| RFC 9112 §3 | 8 | Request line, method, request-target | -| RFC 9112 §3.2 | 10 | Host header, request-target forms | +| RFC 9112 §2.3 | 6 | HTTP version | +| RFC 9112 §3 | 9 | Request line, method, request-target | +| RFC 9112 §3.2 | 11 | Host header, request-target forms | | RFC 9112 §5 | 7 | Header field syntax, sp-before-colon | | RFC 9112 §5.2 | 2 | Obsolete line folding | -| RFC 9112 §6.1 | 16 | Transfer-Encoding, CL+TE ambiguity | +| RFC 9112 §6.1 | 21 | Transfer-Encoding, CL+TE ambiguity | | RFC 9112 §6.2 | 4 | Content-Length body framing | | RFC 9112 §6.3 | 5 | Message body length determination | | RFC 9112 §7.1 | 15 | Chunked transfer coding format | | RFC 9112 §7.1.1 | 4 | Chunk extensions | | 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 9112 §9.3-9.6 | 3 | Connection management | +| RFC 9110 §5.3 | 1 | Header field duplication | +| RFC 9110 §5.4-5.6 | 8 | 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.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 §9.1-9.3 | 10 | 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 §12.5.1 | 1 | Content negotiation (Accept) | +| RFC 9110 §14.2 | 2 | Range requests | | RFC 9110 §15.2 | 1 | 1xx status codes | | RFC 9110 §15.5.6 | 1 | 405 Method Not Allowed | +| RFC 9110 §15.5.16 | 1 | 415 Unsupported Media Type | | RFC 6455 | 2 | WebSocket handshake | | RFC 6585 | 3 | 431 status code | | RFC 3629 | 1 | UTF-8 encoding | diff --git a/docs/static/probe/render.js b/docs/static/probe/render.js index 98ab136..2937af7 100644 --- a/docs/static/probe/render.js +++ b/docs/static/probe/render.js @@ -263,28 +263,36 @@ window.ProbeRender = (function () { 'COMP-CHUNKED-MULTI': '/Http11Probe/docs/body/chunked-multi/', 'COMP-CHUNKED-NO-FINAL': '/Http11Probe/docs/body/chunked-no-final/', 'COMP-DUPLICATE-HOST-SAME': '/Http11Probe/docs/host-header/duplicate-host-same/', + 'COMP-DUPLICATE-CT': '/Http11Probe/docs/headers/duplicate-ct/', 'COMP-EXPECT-UNKNOWN': '/Http11Probe/docs/headers/expect-unknown/', 'COMP-GET-WITH-CL-BODY': '/Http11Probe/docs/body/get-with-cl-body/', 'COMP-HOST-WITH-PATH': '/Http11Probe/docs/host-header/host-with-path/', 'COMP-HOST-WITH-USERINFO': '/Http11Probe/docs/host-header/host-with-userinfo/', 'COMP-LEADING-CRLF': '/Http11Probe/docs/line-endings/leading-crlf/', + 'COMP-LONG-URL-OK': '/Http11Probe/docs/request-line/long-url-ok/', 'COMP-METHOD-CASE': '/Http11Probe/docs/request-line/method-case/', 'COMP-METHOD-CONNECT': '/Http11Probe/docs/request-line/method-connect/', 'COMP-METHOD-TRACE': '/Http11Probe/docs/request-line/method-trace/', 'COMP-OPTIONS-ALLOW': '/Http11Probe/docs/request-line/options-allow/', 'COMP-OPTIONS-STAR': '/Http11Probe/docs/request-line/options-star/', 'COMP-POST-CL-BODY': '/Http11Probe/docs/body/post-cl-body/', + 'COMP-POST-UNSUPPORTED-CT': '/Http11Probe/docs/body/post-unsupported-ct/', 'COMP-POST-CL-UNDERSEND': '/Http11Probe/docs/body/post-cl-undersend/', 'COMP-POST-CL-ZERO': '/Http11Probe/docs/body/post-cl-zero/', 'COMP-POST-NO-CL-NO-TE': '/Http11Probe/docs/body/post-no-cl-no-te/', + 'COMP-SPACE-IN-TARGET': '/Http11Probe/docs/request-line/space-in-target/', + 'COMP-TRACE-SENSITIVE': '/Http11Probe/docs/request-line/trace-sensitive/', 'COMP-UNKNOWN-METHOD': '/Http11Probe/docs/request-line/unknown-method/', 'COMP-UNKNOWN-TE-501': '/Http11Probe/docs/request-line/unknown-te-501/', + 'COMP-RANGE-INVALID': '/Http11Probe/docs/body/range-invalid/', 'COMP-RANGE-POST': '/Http11Probe/docs/body/range-post/', 'COMP-UPGRADE-HTTP10': '/Http11Probe/docs/upgrade/upgrade-http10/', 'COMP-UPGRADE-INVALID-VER': '/Http11Probe/docs/upgrade/upgrade-invalid-ver/', 'COMP-UPGRADE-MISSING-CONN': '/Http11Probe/docs/upgrade/upgrade-missing-conn/', 'COMP-UPGRADE-POST': '/Http11Probe/docs/upgrade/upgrade-post/', 'COMP-UPGRADE-UNKNOWN': '/Http11Probe/docs/upgrade/upgrade-unknown/', + 'COMP-VERSION-CASE': '/Http11Probe/docs/request-line/version-case/', + 'COMP-ACCEPT-NONSENSE': '/Http11Probe/docs/headers/accept-nonsense/', 'COMP-WHITESPACE-BEFORE-HEADERS': '/Http11Probe/docs/headers/whitespace-before-headers/', 'MAL-BINARY-GARBAGE': '/Http11Probe/docs/malformed-input/binary-garbage/', 'MAL-CHUNK-EXT-64K': '/Http11Probe/docs/malformed-input/chunk-extension-long/', @@ -383,6 +391,7 @@ window.ProbeRender = (function () { 'COMP-CHUNKED-TRAILER-VALID': '/Http11Probe/docs/body/chunked-trailer-valid/', 'COMP-CONNECTION-CLOSE': '/Http11Probe/docs/headers/connection-close/', 'COMP-CONTENT-TYPE': '/Http11Probe/docs/headers/content-type-presence/', + 'COMP-DATE-FORMAT': '/Http11Probe/docs/headers/date-format/', 'COMP-DATE-HEADER': '/Http11Probe/docs/headers/date-header/', 'COMP-HEAD-NO-BODY': '/Http11Probe/docs/request-line/head-no-body/', 'COMP-HOST-EMPTY-VALUE': '/Http11Probe/docs/host-header/host-empty-value/', diff --git a/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs b/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs index bd4bca3..50c368d 100644 --- a/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs +++ b/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs @@ -19,6 +19,7 @@ internal static class DocsUrlMap ["COMP-POST-CL-UNDERSEND"] = "body/post-cl-undersend", ["COMP-POST-CL-ZERO"] = "body/post-cl-zero", ["COMP-POST-NO-CL-NO-TE"] = "body/post-no-cl-no-te", + ["COMP-POST-UNSUPPORTED-CT"] = "body/post-unsupported-ct", // content-length ["RFC9112-6.1-CL-NON-NUMERIC"] = "content-length/cl-non-numeric", @@ -26,9 +27,12 @@ internal static class DocsUrlMap ["COMP-NO-CL-IN-204"] = "content-length/no-cl-in-204", // headers + ["COMP-ACCEPT-NONSENSE"] = "headers/accept-nonsense", ["COMP-CONNECTION-CLOSE"] = "headers/connection-close", ["COMP-CONTENT-TYPE"] = "headers/content-type-presence", + ["COMP-DATE-FORMAT"] = "headers/date-format", ["COMP-DATE-HEADER"] = "headers/date-header", + ["COMP-DUPLICATE-CT"] = "headers/duplicate-ct", ["RFC9112-5-EMPTY-HEADER-NAME"] = "headers/empty-header-name", ["COMP-EXPECT-UNKNOWN"] = "headers/expect-unknown", ["RFC9112-5-HEADER-NO-COLON"] = "headers/header-no-colon", @@ -71,14 +75,19 @@ internal static class DocsUrlMap ["COMP-OPTIONS-ALLOW"] = "request-line/options-allow", ["COMP-OPTIONS-STAR"] = "request-line/options-star", ["COMP-REQUEST-LINE-TAB"] = "request-line/request-line-tab", + ["COMP-SPACE-IN-TARGET"] = "request-line/space-in-target", + ["COMP-TRACE-SENSITIVE"] = "request-line/trace-sensitive", ["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-CASE"] = "request-line/version-case", ["COMP-VERSION-LEADING-ZEROS"] = "request-line/version-leading-zeros", ["COMP-VERSION-MISSING-MINOR"] = "request-line/version-missing-minor", ["COMP-VERSION-WHITESPACE"] = "request-line/version-whitespace", + ["COMP-LONG-URL-OK"] = "request-line/long-url-ok", // range + ["COMP-RANGE-INVALID"] = "body/range-invalid", ["COMP-RANGE-POST"] = "body/range-post", // upgrade diff --git a/src/Http11Probe/TestCases/Suites/ComplianceSuite.cs b/src/Http11Probe/TestCases/Suites/ComplianceSuite.cs index 7a0b535..eae6965 100644 --- a/src/Http11Probe/TestCases/Suites/ComplianceSuite.cs +++ b/src/Http11Probe/TestCases/Suites/ComplianceSuite.cs @@ -1232,6 +1232,50 @@ public static IEnumerable GetTestCases() } }; + yield return new TestCase + { + Id = "COMP-DATE-FORMAT", + Description = "Date header should use IMF-fixdate format", + Category = TestCategory.Compliance, + Scored = false, + RfcLevel = RfcLevel.Should, + RfcReference = "RFC 9110 §5.6.7", + PayloadFactory = ctx => MakeRequest( + $"GET / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "IMF-fixdate format", + CustomValidator = (response, state) => + { + if (response is null) + return TestVerdict.Fail; + if (response.StatusCode is not (>= 200 and < 300)) + return TestVerdict.Fail; + if (!response.Headers.TryGetValue("Date", out var date)) + return TestVerdict.Warn; + // IMF-fixdate: "Sun, 06 Nov 1994 08:49:37 GMT" + return DateTime.TryParseExact(date.Trim(), + "ddd, dd MMM yyyy HH:mm:ss 'GMT'", + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.None, out _) + ? TestVerdict.Pass + : TestVerdict.Warn; + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + if (!response.Headers.TryGetValue("Date", out var date)) + return "No Date header present"; + return DateTime.TryParseExact(date.Trim(), + "ddd, dd MMM yyyy HH:mm:ss 'GMT'", + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.None, out _) + ? $"IMF-fixdate: {date.Trim()}" + : $"Non-standard format: {date.Trim()}"; + } + }; + yield return new TestCase { Id = "COMP-NO-1XX-HTTP10", @@ -1325,6 +1369,242 @@ public static IEnumerable GetTestCases() } } }; + + // ── Version case-sensitivity ──────────────────────────────── + yield return new TestCase + { + Id = "COMP-VERSION-CASE", + Description = "HTTP version is case-sensitive — lowercase 'http' must be rejected", + Category = TestCategory.Compliance, + RfcReference = "RFC 9112 §2.3", + PayloadFactory = ctx => MakeRequest( + $"GET / http/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "400 or close", + AllowConnectionClose = true, + ExpectedStatus = StatusCodeRange.Exact(400) + } + }; + + // ── Long URL acceptance ───────────────────────────────────── + yield return new TestCase + { + Id = "COMP-LONG-URL-OK", + Description = "Server should accept request-lines of at least 8000 octets", + Category = TestCategory.Compliance, + RfcLevel = RfcLevel.Should, + RfcReference = "RFC 9112 §3", + PayloadFactory = ctx => + { + var path = "/" + new string('a', 7900); + return MakeRequest($"GET {path} HTTP/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"); + }, + Expected = new ExpectedBehavior + { + Description = "not 414", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Warn : TestVerdict.Fail; + if (response.StatusCode == 414) + return TestVerdict.Fail; + return TestVerdict.Pass; + } + } + }; + + // ── Space inside request-target ───────────────────────────── + yield return new TestCase + { + Id = "COMP-SPACE-IN-TARGET", + Description = "Whitespace inside request-target is invalid", + Category = TestCategory.Compliance, + RfcReference = "RFC 9112 §3.2", + PayloadFactory = ctx => MakeRequest( + $"GET /pa th HTTP/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "400 or close", + AllowConnectionClose = true, + ExpectedStatus = StatusCodeRange.Exact(400) + } + }; + + // ── Duplicate Content-Type ────────────────────────────────── + yield return new TestCase + { + Id = "COMP-DUPLICATE-CT", + Description = "Duplicate Content-Type headers with different values", + Category = TestCategory.Compliance, + RfcLevel = RfcLevel.Should, + RfcReference = "RFC 9110 §5.3", + PayloadFactory = ctx => MakeRequest( + $"POST / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nContent-Length: 5\r\nContent-Type: text/plain\r\nContent-Type: text/html\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; + } + } + }; + + // ── TRACE with sensitive headers ──────────────────────────── + yield return new TestCase + { + Id = "COMP-TRACE-SENSITIVE", + Description = "TRACE should exclude sensitive headers from echoed response", + Category = TestCategory.Compliance, + Scored = false, + RfcLevel = RfcLevel.Should, + RfcReference = "RFC 9110 §9.3.8", + PayloadFactory = ctx => MakeRequest( + $"TRACE / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nAuthorization: Bearer secret-token-123\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "405/501, or 200 without Auth", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; + // TRACE disabled — good + if (response.StatusCode is 405 or 501) + return TestVerdict.Pass; + if (response.StatusCode is >= 200 and < 300) + { + // Check if the echoed body contains the Authorization value + var body = response.Body ?? ""; + if (body.Contains("secret-token-123", StringComparison.OrdinalIgnoreCase)) + return TestVerdict.Warn; + return TestVerdict.Pass; + } + return TestVerdict.Fail; + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + if (response.StatusCode is 405 or 501) + return "TRACE disabled — sensitive headers not exposed"; + if (response.StatusCode is >= 200 and < 300) + { + var body = response.Body ?? ""; + return body.Contains("secret-token-123", StringComparison.OrdinalIgnoreCase) + ? "TRACE echoed Authorization header — sensitive data exposed" + : "TRACE response excludes Authorization header — safe"; + } + return null; + } + }; + + // ── Invalid Range syntax ──────────────────────────────────── + yield return new TestCase + { + Id = "COMP-RANGE-INVALID", + Description = "Invalid Range header syntax should be ignored", + Category = TestCategory.Compliance, + Scored = false, + RfcLevel = RfcLevel.May, + RfcReference = "RFC 9110 §14.2", + PayloadFactory = ctx => MakeRequest( + $"GET / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nRange: bytes=abc-xyz\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "200 or 416", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Warn : TestVerdict.Fail; + // Server ignored invalid Range — returned full content + if (response.StatusCode is >= 200 and < 300 && response.StatusCode != 206) + return TestVerdict.Pass; + // Server explicitly rejected invalid Range + if (response.StatusCode == 416) + return TestVerdict.Pass; + return TestVerdict.Warn; + } + } + }; + + // ── Unrecognized Accept value ─────────────────────────────── + yield return new TestCase + { + Id = "COMP-ACCEPT-NONSENSE", + Description = "Unrecognized Accept value — server may return 406 or default representation", + Category = TestCategory.Compliance, + Scored = false, + RfcLevel = RfcLevel.Should, + RfcReference = "RFC 9110 §12.5.1", + PayloadFactory = ctx => MakeRequest( + $"GET / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nAccept: application/x-nonsense\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "406 or 2xx", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Warn : TestVerdict.Fail; + if (response.StatusCode == 406) + return TestVerdict.Pass; + if (response.StatusCode is >= 200 and < 300) + return TestVerdict.Warn; + return TestVerdict.Fail; + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + if (response.StatusCode == 406) + return "Server enforces Accept negotiation — returned 406"; + if (response.StatusCode is >= 200 and < 300) + return "Server ignored unrecognized Accept — returned default representation"; + return null; + } + }; + + // ── Unsupported Content-Type on POST ──────────────────────── + yield return new TestCase + { + Id = "COMP-POST-UNSUPPORTED-CT", + Description = "POST with unrecognized Content-Type — server may return 415", + Category = TestCategory.Compliance, + Scored = false, + RfcLevel = RfcLevel.May, + RfcReference = "RFC 9110 §15.5.16", + PayloadFactory = ctx => MakeRequest( + $"POST / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nContent-Length: 5\r\nContent-Type: application/x-nonsense\r\n\r\nhello"), + Expected = new ExpectedBehavior + { + Description = "415 or 2xx", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Warn : TestVerdict.Fail; + if (response.StatusCode == 415) + return TestVerdict.Pass; + if (response.StatusCode is >= 200 and < 300) + return TestVerdict.Pass; + return TestVerdict.Warn; + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + if (response.StatusCode == 415) + return "Server enforces Content-Type validation — returned 415"; + if (response.StatusCode is >= 200 and < 300) + return "Server accepted unknown Content-Type"; + return null; + } + }; } // ── Echo verification helpers ──────────────────────────────