From fe92d8753ad6506121e075d0b2563b9f0e4825ac Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Fri, 13 Feb 2026 00:07:36 +0000 Subject: [PATCH] Go through and re evaluate some tests --- docs/content/docs/_index.md | 1 + docs/content/docs/baseline.md | 25 ++++++++ docs/content/docs/body/chunked-extension.md | 8 ++- .../docs/request-line/absolute-form.md | 8 +-- docs/content/docs/smuggling/_index.md | 17 +++++- docs/content/docs/smuggling/chunk-ext-cr.md | 37 ++++++++++++ docs/content/docs/smuggling/te-formfeed.md | 35 +++++++++++ docs/content/docs/smuggling/te-identity.md | 4 +- docs/content/docs/smuggling/te-null.md | 35 +++++++++++ docs/content/docs/smuggling/te-obs-fold.md | 13 ++-- .../docs/smuggling/te-trailing-space.md | 11 ++-- docs/content/docs/smuggling/te-vtab.md | 35 +++++++++++ docs/content/docs/smuggling/te-xchunked.md | 4 +- docs/content/docs/smuggling/trailer-auth.md | 37 ++++++++++++ .../docs/upgrade/upgrade-invalid-ver.md | 2 +- src/Http11Probe.Cli/Reporting/DocsUrlMap.cs | 1 + .../TestCases/Suites/ComplianceSuite.cs | 22 ++++--- .../TestCases/Suites/SmugglingSuite.cs | 60 ++++++++++++++++--- 18 files changed, 314 insertions(+), 41 deletions(-) create mode 100644 docs/content/docs/baseline.md create mode 100644 docs/content/docs/smuggling/chunk-ext-cr.md create mode 100644 docs/content/docs/smuggling/te-formfeed.md create mode 100644 docs/content/docs/smuggling/te-null.md create mode 100644 docs/content/docs/smuggling/te-vtab.md create mode 100644 docs/content/docs/smuggling/trailer-auth.md diff --git a/docs/content/docs/_index.md b/docs/content/docs/_index.md index 9bd81cb..7f84ffd 100644 --- a/docs/content/docs/_index.md +++ b/docs/content/docs/_index.md @@ -10,6 +10,7 @@ Reference documentation for every test in Http11Probe, organized by topic. Each {{< cards >}} {{< 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" >}} {{< card link="request-line" title="Request Line" subtitle="Request-line format, multiple spaces, missing target, fragments, HTTP version validation." icon="terminal" >}} {{< card link="headers" title="Header Syntax" subtitle="Obs-fold, space before colon, empty names, invalid characters, missing colon." icon="document-text" >}} diff --git a/docs/content/docs/baseline.md b/docs/content/docs/baseline.md new file mode 100644 index 0000000..7e9613c --- /dev/null +++ b/docs/content/docs/baseline.md @@ -0,0 +1,25 @@ +--- +title: "BASELINE" +description: "BASELINE test documentation" +weight: 2 +--- + +| | | +|---|---| +| **Test ID** | `COMP-BASELINE` | +| **Category** | Compliance | +| **Expected** | `2xx` | + +## What it sends + +A well-formed minimal HTTP/1.1 GET request. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## Why it matters + +This is the sanity check for reachability and parser baseline. If this fails, later negative tests are not meaningful. diff --git a/docs/content/docs/body/chunked-extension.md b/docs/content/docs/body/chunked-extension.md index d922481..b77c5b5 100644 --- a/docs/content/docs/body/chunked-extension.md +++ b/docs/content/docs/body/chunked-extension.md @@ -10,7 +10,7 @@ weight: 10 | **Category** | Compliance | | **RFC** | [RFC 9112 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) | | **Requirement** | MUST ignore unrecognized extensions | -| **Expected** | `2xx` or `400` | +| **Expected** | `2xx` = Pass, `400` = Warn | ## What it sends @@ -75,9 +75,11 @@ chunk-ext-val = token / quoted-string 4. However, the RFC also says servers "ought to limit the total length of chunk extensions" and may generate a 4xx response if limits are exceeded. This introduces a legitimate reason for a `400` response. 5. The extension in this test (`ext=value`) is short (9 bytes), so a length-limit rejection would be unreasonable. But the RFC permits it in principle. -### Scored / Unscored justification +### Scoring justification -**Unscored.** The MUST keyword applies to *ignoring unrecognized* extensions, which implies the server should parse and skip them. However, the RFC also explicitly permits servers to reject requests with excessive chunk extensions via a 4xx response. Because the boundary between "acceptable" and "excessive" is left to the server's discretion, there is room for a compliant server to reject even short extensions. The test uses SHOULD accept (`2xx` = Pass, `400` = Warn) to acknowledge that `2xx` is the preferred behavior while `400` is not a clear violation. +This test is **scored** because the payload uses a short, syntactically valid chunk extension. For this input, RFC 9112 §7.1.1 says recipients MUST ignore unrecognized extensions and continue processing. +`2xx` is Pass. +`400` is Warn (strict behavior seen in the wild, but not the preferred RFC behavior for this specific payload). ### Edge cases diff --git a/docs/content/docs/request-line/absolute-form.md b/docs/content/docs/request-line/absolute-form.md index 3e0a6e0..94cf060 100644 --- a/docs/content/docs/request-line/absolute-form.md +++ b/docs/content/docs/request-line/absolute-form.md @@ -10,7 +10,7 @@ weight: 9 | **Category** | Compliance | | **RFC** | [RFC 9112 Section 3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2) | | **Requirement** | MUST accept (server) | -| **Expected** | `400` or `2xx` | +| **Expected** | `2xx` = Pass, `400` = Warn (unscored) | ## What it sends @@ -35,8 +35,8 @@ Although the RFC says servers MUST accept absolute-form, in practice most non-pr ## Why it matters -**Pass:** Server rejects with `400` (common origin-server behavior). -**Warn:** Server accepts with `2xx` (RFC-compliant, accepts absolute-form). +**Pass:** Server accepts with `2xx` (RFC-compliant). +**Warn:** Server rejects with `400` (common in practice, but non-compliant with MUST accept). ## Deep Analysis @@ -74,7 +74,7 @@ The `absolute-form` production requires a complete `absolute-URI` as defined in ### Scoring Justification -**Unscored.** Although the RFC uses "MUST accept," this requirement primarily targets proxy servers. An origin server that rejects absolute-form (returning `400`) is technically non-compliant but is not creating a security vulnerability -- it is simply refusing a request format it was not designed to handle. Both `400` and `2xx` are treated as acceptable outcomes. +**Unscored.** RFC 9112 uses a server-side MUST to accept absolute-form. In practice, many origin stacks still reject it. To preserve interoperability visibility without hard-failing broad classes of servers, this test is unscored: `2xx` is Pass and `400` is Warn. ### Edge Cases diff --git a/docs/content/docs/smuggling/_index.md b/docs/content/docs/smuggling/_index.md index d56f773..b10f48d 100644 --- a/docs/content/docs/smuggling/_index.md +++ b/docs/content/docs/smuggling/_index.md @@ -87,9 +87,21 @@ For these, `400` is the strict/safe response and `2xx` is RFC-compliant. Http11P {{< card link="chunk-spill" title="CHUNK-SPILL" subtitle="Chunk declares size 5 but sends 7 bytes." >}} {{< card link="chunk-lf-term" title="CHUNK-LF-TERM" subtitle="Bare LF as chunk data terminator." >}} {{< card link="chunk-ext-ctrl" title="CHUNK-EXT-CTRL" subtitle="NUL byte in chunk extension." >}} + {{< card link="chunk-ext-cr" title="CHUNK-EXT-CR" subtitle="Bare CR inside chunk extension metadata." >}} {{< card link="chunk-lf-trailer" title="CHUNK-LF-TRAILER" subtitle="Bare LF in trailer section termination." >}} {{< card link="te-identity" title="TE-IDENTITY" subtitle="Transfer-Encoding: identity (deprecated) with CL." >}} + {{< card link="te-vtab" title="TE-VTAB" subtitle="Vertical tab before chunked token." >}} + {{< card link="te-formfeed" title="TE-FORMFEED" subtitle="Form-feed before chunked token." >}} + {{< card link="te-null" title="TE-NULL" subtitle="NUL byte appended to chunked token." >}} {{< card link="chunk-negative" title="CHUNK-NEGATIVE" subtitle="Negative chunk size (-1)." >}} + {{< card link="chunk-bare-cr-term" title="CHUNK-BARE-CR-TERM" subtitle="Bare CR as chunk size line terminator." >}} + {{< card link="cl-underscore" title="CL-UNDERSCORE" subtitle="Content-Length with underscore digit separator (1_0)." >}} + {{< card link="cl-negative-zero" title="CL-NEGATIVE-ZERO" subtitle="Content-Length: -0 — not valid 1*DIGIT." >}} + {{< card link="cl-double-zero" title="CL-DOUBLE-ZERO" subtitle="Content-Length: 00 — leading zero ambiguity." >}} + {{< card link="cl-leading-zeros-octal" title="CL-LEADING-ZEROS-OCTAL" subtitle="Content-Length: 0200 — octal vs decimal disagreement." >}} + {{< card link="te-obs-fold" title="TE-OBS-FOLD" subtitle="Transfer-Encoding with obs-fold line wrapping." >}} + {{< card link="te-trailing-comma" title="TE-TRAILING-COMMA" subtitle="Transfer-Encoding: chunked, — trailing comma." >}} + {{< card link="multiple-host-comma" title="MULTIPLE-HOST-COMMA" subtitle="Host with comma-separated values." >}} {{< /cards >}} ### Unscored @@ -97,7 +109,6 @@ For these, `400` is the strict/safe response and `2xx` is RFC-compliant. Http11P {{< cards >}} {{< card link="cl-trailing-space" title="CL-TRAILING-SPACE" subtitle="Trailing space in CL value. OWS trimming is valid." >}} {{< card link="cl-extra-leading-sp" title="CL-EXTRA-LEADING-SP" subtitle="Extra space after CL colon. OWS is valid." >}} - {{< card link="header-injection" title="HEADER-INJECTION" subtitle="CRLF injection in header value." >}} {{< card link="te-double-chunked" title="TE-DOUBLE-CHUNKED" subtitle="Duplicate 'chunked' TE with CL." >}} {{< 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." >}} @@ -107,6 +118,10 @@ For these, `400` is the strict/safe response and `2xx` is RFC-compliant. Http11P {{< card link="trailer-cl" title="TRAILER-CL" subtitle="Content-Length in chunked trailers (prohibited)." >}} {{< card link="trailer-te" title="TRAILER-TE" subtitle="Transfer-Encoding in chunked trailers (prohibited)." >}} {{< card link="trailer-host" title="TRAILER-HOST" subtitle="Host header in chunked trailers (must not route)." >}} + {{< card link="trailer-auth" title="TRAILER-AUTH" subtitle="Authorization in chunked trailers (prohibited)." >}} + {{< card link="trailer-content-type" title="TRAILER-CONTENT-TYPE" subtitle="Content-Type in chunked trailers (prohibited)." >}} {{< card link="head-cl-body" title="HEAD-CL-BODY" subtitle="HEAD with Content-Length and body." >}} {{< card link="options-cl-body" title="OPTIONS-CL-BODY" subtitle="OPTIONS with Content-Length and body." >}} + {{< card link="te-tab-before-value" title="TE-TAB-BEFORE-VALUE" subtitle="Tab as OWS before Transfer-Encoding value." >}} + {{< card link="absolute-uri-host-mismatch" title="ABSOLUTE-URI-HOST-MISMATCH" subtitle="Absolute-form URI with different Host header." >}} {{< /cards >}} diff --git a/docs/content/docs/smuggling/chunk-ext-cr.md b/docs/content/docs/smuggling/chunk-ext-cr.md new file mode 100644 index 0000000..4d9cf4c --- /dev/null +++ b/docs/content/docs/smuggling/chunk-ext-cr.md @@ -0,0 +1,37 @@ +--- +title: "CHUNK-EXT-CR" +description: "CHUNK-EXT-CR test documentation" +weight: 51 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-EXT-CR` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1), [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | +| **Requirement** | MUST reject malformed chunk line | +| **Expected** | `400` or close | + +## What it sends + +A chunk-size line where a bare CR appears inside the extension area, not as a valid `CRLF` terminator. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +5;a\rX\r\n +hello\r\n +0\r\n +\r\n +``` + +## Why it matters + +Differential handling of bare CR in framing metadata can produce parser disagreement across hops and create desync risk. + +## Sources + +- [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) +- [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) diff --git a/docs/content/docs/smuggling/te-formfeed.md b/docs/content/docs/smuggling/te-formfeed.md new file mode 100644 index 0000000..31621e7 --- /dev/null +++ b/docs/content/docs/smuggling/te-formfeed.md @@ -0,0 +1,35 @@ +--- +title: "TE-FORMFEED" +description: "TE-FORMFEED test documentation" +weight: 53 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-FORMFEED` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5), [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **Requirement** | MUST reject invalid transfer-coding token | +| **Expected** | `400` or close | + +## What it sends + +`Transfer-Encoding: chunked` with `Content-Length` present. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: \x0cchunked\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +## Why it matters + +Form-feed control characters in TE values are an obfuscation vector that can trigger parser disagreement in proxy chains. + +## Sources + +- [RFC 9110 §5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/te-identity.md b/docs/content/docs/smuggling/te-identity.md index 69faed6..c8db4ca 100644 --- a/docs/content/docs/smuggling/te-identity.md +++ b/docs/content/docs/smuggling/te-identity.md @@ -10,7 +10,7 @@ weight: 30 | **Category** | Smuggling | | **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | | **Requirement** | MUST reject | -| **Expected** | `400` or close | +| **Expected** | `400`/`501` or close | ## What it sends @@ -76,7 +76,7 @@ The token `identity` is syntactically valid per the ABNF (it consists entirely o This test is **scored** (MUST reject). Although the SHOULD in RFC 9112 section 6.1 for unrecognized transfer codings is not a MUST, the combined presence of Transfer-Encoding and Content-Length triggers the MUST-level requirement in section 6.1 to close the connection. The server cannot safely process `Transfer-Encoding: identity` because it is not a recognized coding, and the dual-header scenario mandates connection closure at minimum. -- **Pass (400 or close):** The server correctly rejects the unknown transfer coding or closes the connection per the dual-header rule. +- **Pass (400/501 or close):** The server rejects the unknown transfer coding or closes the connection per the dual-header rule. - **Fail (2xx):** The server accepted a request with an unrecognized transfer coding and conflicting Content-Length, violating the connection-closure requirement. ### Smuggling Attack Scenarios diff --git a/docs/content/docs/smuggling/te-null.md b/docs/content/docs/smuggling/te-null.md new file mode 100644 index 0000000..97066fa --- /dev/null +++ b/docs/content/docs/smuggling/te-null.md @@ -0,0 +1,35 @@ +--- +title: "TE-NULL" +description: "TE-NULL test documentation" +weight: 54 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-NULL` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5), [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **Requirement** | MUST reject malformed field value | +| **Expected** | `400` or close | + +## What it sends + +`Transfer-Encoding: chunked` with `Content-Length` present. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\x00\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +## Why it matters + +NUL handling differences (truncate vs reject) are a classic parser differential that can destabilize message framing. + +## Sources + +- [RFC 9110 §5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/te-obs-fold.md b/docs/content/docs/smuggling/te-obs-fold.md index 850ddfa..a3df41b 100644 --- a/docs/content/docs/smuggling/te-obs-fold.md +++ b/docs/content/docs/smuggling/te-obs-fold.md @@ -9,8 +9,8 @@ weight: 50 | **Test ID** | `SMUG-TE-OBS-FOLD` | | **Category** | Smuggling | | **RFC** | [RFC 9112 §5.2](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) | -| **Requirement** | MUST | -| **Expected** | `400` | +| **Requirement** | MUST reject or unfold obs-fold | +| **Expected** | `400`, or `2xx` with connection close | ## What it sends @@ -37,7 +37,7 @@ When obs-fold is used on the Transfer-Encoding header with Content-Length also p ## Why it matters -This is a high-confidence smuggling vector. The obs-fold mechanism was deprecated precisely because of parser disagreements. When applied to Transfer-Encoding — the header that determines message framing — it creates a situation where one parser uses chunked encoding and another uses Content-Length, enabling request smuggling. The RFC requires rejection (MUST), and no `AllowConnectionClose` alternative is acceptable because the server must actively reject the malformed header rather than simply closing the connection. +This is a high-confidence smuggling vector. The obs-fold mechanism was deprecated precisely because of parser disagreements. When applied to Transfer-Encoding, one parser can unfold to `chunked` while another ignores it and falls back to Content-Length. ## Deep Analysis @@ -71,10 +71,11 @@ The `obs-fold` rule (obsolete line folding) allows a field value to be continued ### Scored / Unscored Justification -This test is **scored** (MUST reject with `400`). RFC 9112 section 5.2 provides a MUST-level requirement for servers receiving obs-fold. While the RFC allows two options (reject or unfold), this test expects strict `400` rejection because the obs-fold is applied to the Transfer-Encoding header -- the header that determines message framing. Allowing an unfolded interpretation when Content-Length is also present would require the server to then handle the CL/TE dual-header scenario, adding further complexity and risk. No `AllowConnectionClose` alternative is acceptable because the server must actively reject the malformed header. +This test is **scored**. RFC 9112 §5.2 gives two compliant server behaviors: reject with `400`, or replace obs-fold with SP and continue. If unfolded, the message still carries both TE and CL, so RFC 9112 §6.1 requires closing the connection after responding. -- **Pass (400):** The server correctly rejects the obs-fold per the MUST requirement. -- **Fail (2xx or close):** The server either silently accepted the folded header or merely closed the connection without the required `400` response. +- **Pass:** `400`. +- **Warn:** `2xx` with connection close (accepted unfold path). +- **Fail:** `2xx` without connection close. ### Smuggling Attack Scenarios diff --git a/docs/content/docs/smuggling/te-trailing-space.md b/docs/content/docs/smuggling/te-trailing-space.md index df5176f..cfbcccd 100644 --- a/docs/content/docs/smuggling/te-trailing-space.md +++ b/docs/content/docs/smuggling/te-trailing-space.md @@ -9,8 +9,8 @@ weight: 6 | **Test ID** | `SMUG-TE-TRAILING-SPACE` | | **Category** | Smuggling | | **RFC** | [RFC 9110 §5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5), [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | +| **Requirement** | MAY reject or process with TE; MUST close connection if CL+TE is processed | +| **Expected** | `400`/`501`, or `2xx` with connection close | ## What it sends @@ -77,10 +77,11 @@ The `field-line` rule includes trailing `OWS` after `field-value`. Per RFC 9112 ### Scored / Unscored Justification -This test is **scored** (MUST reject). The combined presence of Transfer-Encoding and Content-Length triggers the MUST-level connection-closure requirement in RFC 9112 section 6.1. While RFC 9110 section 5.5 requires stripping trailing whitespace (which would make the value `chunked`), the trailing space creates a practical ambiguity that many parsers handle inconsistently. The server MUST at minimum close the connection after responding to a request with both TE and CL. +This test is **scored**. RFC 9110 §5.5 requires trimming trailing OWS before field-value evaluation, so `chunked ` can become `chunked`. RFC 9112 §6.1 then applies the CL+TE rule: reject, or process with TE and close the connection. -- **Pass (400 or close):** The server correctly rejects the request or closes the connection per the dual-header rules. -- **Fail (2xx):** The server processed the request without closing the connection, violating the MUST requirement in section 6.1. +- **Pass:** `400`/`501` (strict rejection path). +- **Warn:** `2xx` with connection close (lenient parse path, still RFC-safe on connection handling). +- **Fail:** `2xx` without connection close. ### Smuggling Attack Scenarios diff --git a/docs/content/docs/smuggling/te-vtab.md b/docs/content/docs/smuggling/te-vtab.md new file mode 100644 index 0000000..451a8c6 --- /dev/null +++ b/docs/content/docs/smuggling/te-vtab.md @@ -0,0 +1,35 @@ +--- +title: "TE-VTAB" +description: "TE-VTAB test documentation" +weight: 52 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-VTAB` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5), [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **Requirement** | MUST reject invalid transfer-coding token | +| **Expected** | `400` or close | + +## What it sends + +`Transfer-Encoding: chunked` with `Content-Length` present. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: \x0bchunked\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +## Why it matters + +Control-character obfuscation is a known TE parsing differential. One hop can reject while another normalizes and parses differently. + +## Sources + +- [RFC 9110 §5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/te-xchunked.md b/docs/content/docs/smuggling/te-xchunked.md index 85d7202..39c9ff6 100644 --- a/docs/content/docs/smuggling/te-xchunked.md +++ b/docs/content/docs/smuggling/te-xchunked.md @@ -10,7 +10,7 @@ weight: 5 | **Category** | Smuggling | | **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | | **Requirement** | MUST reject | -| **Expected** | `400` or close | +| **Expected** | `400`/`501` or close | ## What it sends @@ -74,7 +74,7 @@ The token `xchunked` is syntactically valid per the ABNF -- it consists entirely This test is **scored** (MUST reject). The MUST-level connection-closure requirement in RFC 9112 section 6.1 applies to all requests containing both Transfer-Encoding and Content-Length. Additionally, `xchunked` is not a recognized transfer coding, so the SHOULD-level guidance to respond with `501` reinforces rejection. The server cannot safely process a body framed with an unknown coding. -- **Pass (400 or close):** The server correctly rejects the unknown transfer coding or closes the connection per the dual-header rule. +- **Pass (400/501 or close):** The server rejects the unknown transfer coding or closes the connection per the dual-header rule. - **Fail (2xx):** The server accepted a request with an unrecognized transfer coding and conflicting Content-Length, violating the connection-closure requirement. ### Smuggling Attack Scenarios diff --git a/docs/content/docs/smuggling/trailer-auth.md b/docs/content/docs/smuggling/trailer-auth.md new file mode 100644 index 0000000..a554ece --- /dev/null +++ b/docs/content/docs/smuggling/trailer-auth.md @@ -0,0 +1,37 @@ +--- +title: "TRAILER-AUTH" +description: "TRAILER-AUTH test documentation" +weight: 55 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TRAILER-AUTH` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) | +| **Requirement** | Unscored | +| **Expected** | `400` or `2xx` | + +## What it sends + +A chunked request that places `Authorization` in the trailer section. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n +0\r\n +Authorization: Bearer evil\r\n +\r\n +``` + +## Why this test is unscored + +`Authorization` in trailers is prohibited for senders, but recipients can either reject or ignore/discard it. Status code alone cannot prove whether downstream components consumed it. + +## Sources + +- [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) diff --git a/docs/content/docs/upgrade/upgrade-invalid-ver.md b/docs/content/docs/upgrade/upgrade-invalid-ver.md index 4a68f17..3b21cec 100644 --- a/docs/content/docs/upgrade/upgrade-invalid-ver.md +++ b/docs/content/docs/upgrade/upgrade-invalid-ver.md @@ -9,7 +9,7 @@ weight: 4 | **Test ID** | `COMP-UPGRADE-INVALID-VER` | | **Category** | Compliance | | **RFC** | [RFC 6455 Section 4.4](https://www.rfc-editor.org/rfc/rfc6455#section-4.4) | -| **Requirement** | SHOULD return 426 | +| **Requirement** | MUST abort handshake (426 preferred) | | **Expected** | `426` or non-`101` | ## What it sends diff --git a/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs b/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs index 4e91440..234416b 100644 --- a/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs +++ b/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs @@ -80,6 +80,7 @@ internal static class DocsUrlMap // Special cases where the doc filename doesn't match the ID suffix private static readonly Dictionary SpecialSlugs = new(StringComparer.OrdinalIgnoreCase) { + ["COMP-BASELINE"] = "baseline", ["MAL-CHUNK-EXT-64K"] = "malformed-input/chunk-extension-long", ["SMUG-TRANSFER_ENCODING"] = "smuggling/transfer-encoding-underscore", }; diff --git a/src/Http11Probe/TestCases/Suites/ComplianceSuite.cs b/src/Http11Probe/TestCases/Suites/ComplianceSuite.cs index 96e8127..6d8c82e 100644 --- a/src/Http11Probe/TestCases/Suites/ComplianceSuite.cs +++ b/src/Http11Probe/TestCases/Suites/ComplianceSuite.cs @@ -431,18 +431,19 @@ public static IEnumerable GetTestCases() Id = "COMP-ABSOLUTE-FORM", Description = "Absolute-form request-target — server should accept per RFC", Category = TestCategory.Compliance, + Scored = false, RfcReference = "RFC 9112 §3.2.2", PayloadFactory = ctx => MakeRequest($"GET http://{ctx.HostHeader}/ HTTP/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"), Expected = new ExpectedBehavior { - Description = "400 or 2xx", + Description = "2xx preferred; 400 warns", CustomValidator = (response, state) => { if (response is null) - return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; - if (response.StatusCode == 400) - return TestVerdict.Pass; + return state == ConnectionState.ClosedByServer ? TestVerdict.Warn : TestVerdict.Fail; if (response.StatusCode is >= 200 and < 300) + return TestVerdict.Pass; + if (response.StatusCode == 400) return TestVerdict.Warn; return TestVerdict.Fail; } @@ -756,13 +757,15 @@ public static IEnumerable GetTestCases() $"POST / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nTransfer-Encoding: chunked\r\n\r\n5;ext=value\r\nhello\r\n0\r\n\r\n"), Expected = new ExpectedBehavior { - Description = "2xx or 400", + Description = "2xx preferred; 400 warns", CustomValidator = (response, state) => { if (response is null) - return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; - if (response.StatusCode is >= 200 and < 300 || response.StatusCode == 400) + return state == ConnectionState.ClosedByServer ? TestVerdict.Warn : TestVerdict.Fail; + if (response.StatusCode is >= 200 and < 300) return TestVerdict.Pass; + if (response.StatusCode == 400) + return TestVerdict.Warn; return TestVerdict.Fail; } } @@ -778,7 +781,7 @@ public static IEnumerable GetTestCases() $"GET / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 99\r\n\r\n"), Expected = new ExpectedBehavior { - Description = "426 or 2xx", + Description = "non-101 (426 preferred)", CustomValidator = (response, state) => { if (response is null) @@ -787,9 +790,10 @@ public static IEnumerable GetTestCases() return TestVerdict.Fail; if (response.StatusCode == 426) return TestVerdict.Pass; + // Some servers ignore Upgrade entirely and process the GET. if (response.StatusCode is >= 200 and < 300) return TestVerdict.Warn; - return TestVerdict.Fail; + return TestVerdict.Pass; } } }; diff --git a/src/Http11Probe/TestCases/Suites/SmugglingSuite.cs b/src/Http11Probe/TestCases/Suites/SmugglingSuite.cs index 486a804..9028a21 100644 --- a/src/Http11Probe/TestCases/Suites/SmugglingSuite.cs +++ b/src/Http11Probe/TestCases/Suites/SmugglingSuite.cs @@ -130,8 +130,15 @@ public static IEnumerable GetTestCases() BehavioralAnalyzer = AnalyzeTeWithClFallback, Expected = new ExpectedBehavior { - ExpectedStatus = StatusCodeRange.Exact(400), - AllowConnectionClose = true + Description = "400/501 or close", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; + return response.StatusCode is 400 or 501 + ? TestVerdict.Pass + : TestVerdict.Fail; + } } }; @@ -146,8 +153,22 @@ public static IEnumerable GetTestCases() BehavioralAnalyzer = AnalyzeTeWithClFallback, Expected = new ExpectedBehavior { - ExpectedStatus = StatusCodeRange.Exact(400), - AllowConnectionClose = true + Description = "400/501 or 2xx+close", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; + + if (response.StatusCode is 400 or 501) + return TestVerdict.Pass; + + // If recipient trims OWS and recognizes chunked, RFC allows processing; + // with CL+TE present, connection should be closed after response. + if (response.StatusCode is >= 200 and < 300) + return state == ConnectionState.ClosedByServer ? TestVerdict.Warn : TestVerdict.Fail; + + return TestVerdict.Fail; + } } }; @@ -825,8 +846,15 @@ public static IEnumerable GetTestCases() BehavioralAnalyzer = AnalyzeTeWithClFallback, Expected = new ExpectedBehavior { - ExpectedStatus = StatusCodeRange.Exact(400), - AllowConnectionClose = true + Description = "400/501 or close", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; + return response.StatusCode is 400 or 501 + ? TestVerdict.Pass + : TestVerdict.Fail; + } } }; @@ -903,6 +931,7 @@ public static IEnumerable GetTestCases() Id = "SMUG-CHUNKED-WITH-PARAMS", Description = "Transfer-Encoding: chunked;ext=val — parameters on chunked encoding", Category = TestCategory.Smuggling, + Scored = false, RfcReference = "RFC 9112 §7", PayloadFactory = ctx => MakeRequest( $"POST / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nTransfer-Encoding: chunked;ext=val\r\nContent-Length: 5\r\n\r\nhello"), @@ -1189,13 +1218,28 @@ public static IEnumerable GetTestCases() Id = "SMUG-TE-OBS-FOLD", Description = "Transfer-Encoding with obs-fold line wrapping must be rejected", Category = TestCategory.Smuggling, - RfcReference = "RFC 9112 §5.1", + RfcReference = "RFC 9112 §5.2", PayloadFactory = ctx => MakeRequest( $"POST / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nTransfer-Encoding:\r\n chunked\r\nContent-Length: 5\r\n\r\nhello"), BehavioralAnalyzer = AnalyzeTeWithClFallback, Expected = new ExpectedBehavior { - ExpectedStatus = StatusCodeRange.Exact(400) + Description = "400 or 2xx+close", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; + + if (response.StatusCode == 400) + return TestVerdict.Pass; + + // RFC 9112 §5.2 permits unfolding obs-fold; if unfolded to TE+CL, + // RFC 9112 §6.1 requires closing the connection after responding. + if (response.StatusCode is >= 200 and < 300) + return state == ConnectionState.ClosedByServer ? TestVerdict.Warn : TestVerdict.Fail; + + return TestVerdict.Fail; + } } };