diff --git a/docs/content/_index.md b/docs/content/_index.md index a56fe66..429ea83 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -1,5 +1,6 @@ --- title: Http11Probe +description: "Http11Probe tests HTTP/1.1 server implementations against RFC 9110 and RFC 9112, covering protocol compliance, request smuggling, and malformed input handling across frameworks." layout: hextra-home --- diff --git a/docs/content/add-a-framework/_index.md b/docs/content/add-a-framework/_index.md index f977b9f..41ba1d6 100644 --- a/docs/content/add-a-framework/_index.md +++ b/docs/content/add-a-framework/_index.md @@ -1,5 +1,6 @@ --- title: Add a Framework +description: "How to add an HTTP server to Http11Probe: implement the required endpoints, add a Dockerfile and probe.json, and get automatic RFC 9110/9112 compliance results." toc: true --- diff --git a/docs/content/add-a-test.md b/docs/content/add-a-test.md index b873bc8..71bba0d 100644 --- a/docs/content/add-a-test.md +++ b/docs/content/add-a-test.md @@ -1,5 +1,6 @@ --- title: Add a Test +description: "How to add a new HTTP/1.1 compliance, smuggling, or malformed-input test case to Http11Probe, including the test case definition, documentation page, and category index entry." --- A step-by-step guide to adding a new test to Http11Probe. Every test touches four places: the suite file, the docs URL map (sometimes), a documentation page, and the category index. @@ -94,6 +95,7 @@ Expected = new ExpectedBehavior - Set `AllowConnectionClose = true` only when connection close is an acceptable alternative to a status code. - Set `Scored = false` for MAY-level or informational tests. - Use `"RFC 9112 §5.1"` format for `RfcReference` (section sign, not "Section"). +- Give the doc page a readable `title` (e.g. `"My Test — HTTP/1.1 Compliance"`), not the raw test ID — it's rendered as the page heading and browser tab title. Write a `description` that's a specific, one-sentence summary of the request and its RFC basis, not a generic placeholder — it's used as the page's meta description for search results. ## 2. Add a docs URL mapping (if needed) @@ -117,8 +119,8 @@ Use this template: ```markdown --- -title: "MY-TEST" -description: "MY-TEST test documentation" +title: "My Test — HTTP/1.1 Compliance" +description: "One or two sentences describing the request and what makes it non-conforming, ideally ending with the RFC section it's tested against." weight: 1 --- diff --git a/docs/content/add-with-ai-agent.md b/docs/content/add-with-ai-agent.md index 413e7f9..44be734 100644 --- a/docs/content/add-with-ai-agent.md +++ b/docs/content/add-with-ai-agent.md @@ -1,5 +1,6 @@ --- title: Add with AI Agent +description: "Use an AI coding agent with Http11Probe's machine-readable AGENTS.md guide to add a new compliance test or HTTP server framework." --- Use an AI coding agent (Claude Code, Cursor, Copilot, etc.) to add a new test or framework to Http11Probe. The repository includes a machine-readable contribution guide at [`AGENTS.md`](https://github.com/MDA2AV/blob/main/AGENTS.md) designed specifically for LLM consumption. diff --git a/docs/content/caching/_index.md b/docs/content/caching/_index.md index 7783f4f..b5e2741 100644 --- a/docs/content/caching/_index.md +++ b/docs/content/caching/_index.md @@ -1,5 +1,6 @@ --- title: Caching +description: "Conditional request support across HTTP/1.1 servers, covering ETag, Last-Modified, If-None-Match precedence, and weak comparison behavior." layout: wide toc: false --- diff --git a/docs/content/compliance/_index.md b/docs/content/compliance/_index.md index ff301a9..a7e6f6b 100644 --- a/docs/content/compliance/_index.md +++ b/docs/content/compliance/_index.md @@ -1,5 +1,6 @@ --- title: Compliance +description: "RFC 9110/9112 compliance results for HTTP/1.1 servers, covering request-line parsing, header syntax, Host validation, and body framing requirements." layout: wide toc: false --- diff --git a/docs/content/cookies/_index.md b/docs/content/cookies/_index.md index caf1bbb..3205063 100644 --- a/docs/content/cookies/_index.md +++ b/docs/content/cookies/_index.md @@ -1,5 +1,6 @@ --- title: Cookies +description: "Cookie header parsing resilience across HTTP servers, testing oversized values, NUL bytes, control characters, and malformed pairs per RFC 6265." layout: wide toc: false --- diff --git a/docs/content/docs/_index.md b/docs/content/docs/_index.md index 75ab8c0..926a14c 100644 --- a/docs/content/docs/_index.md +++ b/docs/content/docs/_index.md @@ -1,6 +1,6 @@ --- title: Glossary -description: "Glossary — Http11Probe documentation" +description: "Reference documentation for every Http11Probe test, organized by topic, explaining the RFC requirement, request payload, expected response, and rationale." breadcrumbs: false sidebar: open: false diff --git a/docs/content/docs/baseline.md b/docs/content/docs/baseline.md index 6380453..9de6fd3 100644 --- a/docs/content/docs/baseline.md +++ b/docs/content/docs/baseline.md @@ -1,6 +1,6 @@ --- -title: "BASELINE" -description: "BASELINE test documentation" +title: "Baseline Reachability Test" +description: "The COMP-BASELINE sanity check that confirms a target HTTP/1.1 server is reachable and parses well-formed requests before running negative tests." weight: 4 --- diff --git a/docs/content/docs/body/_index.md b/docs/content/docs/body/_index.md index 9ea3a81..6e647e0 100644 --- a/docs/content/docs/body/_index.md +++ b/docs/content/docs/body/_index.md @@ -1,6 +1,6 @@ --- title: Body Handling -description: "Body Handling — Http11Probe documentation" +description: "How HTTP/1.1 servers must read and terminate request bodies using Content-Length and chunked Transfer-Encoding, per RFC 9112 Sections 6 and 7." weight: 9 sidebar: open: false diff --git a/docs/content/docs/body/chunked-body.md b/docs/content/docs/body/chunked-body.md index 59f4f52..76c3dac 100644 --- a/docs/content/docs/body/chunked-body.md +++ b/docs/content/docs/body/chunked-body.md @@ -1,94 +1,94 @@ ---- -title: "CHUNKED-BODY" -description: "CHUNKED-BODY test documentation" -weight: 6 ---- - -| | | -|---|---| -| **Test ID** | `COMP-CHUNKED-BODY` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MUST accept | -| **Expected** | `2xx` | - -## What it sends - -A valid chunked POST with a single 5-byte chunk followed by the zero terminator. - -```http -POST / HTTP/1.1\r\n -Host: localhost\r\n -Transfer-Encoding: chunked\r\n -\r\n -5\r\n -hello\r\n -0\r\n -\r\n -``` - -## What the RFC says - -> "The chunked transfer coding wraps content in order to transfer it as a series of chunks, each with its own size indicator, followed by an OPTIONAL trailer section containing trailer fields." — RFC 9112 Section 7.1 - -> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 Section 7.1 - -> "A recipient MUST be able to parse the chunked transfer coding because it plays a crucial role in framing messages when the content size is not known in advance." — RFC 9112 Section 6.1 - -A server that supports HTTP/1.1 must be able to decode chunked transfer encoding. This is a MUST-level requirement. - -## Why it matters - -Chunked encoding is fundamental to HTTP/1.1 — it enables streaming, server-sent data, and requests where the body size isn't known in advance. If a server can't decode a basic chunked body, it cannot fully participate in HTTP/1.1. - -## Deep Analysis - -### Relevant ABNF grammar - -From RFC 9112 Section 7.1: - -``` -chunked-body = *chunk - last-chunk - trailer-section - CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG -last-chunk = 1*("0") [ chunk-ext ] CRLF - -chunk-data = 1*OCTET ; a sequence of chunk-size octets -trailer-section = *( field-line CRLF ) -``` - -### Direct RFC quotes - -> "The chunked transfer coding wraps content in order to transfer it as a series of chunks, each with its own size indicator, followed by an OPTIONAL trailer section containing trailer fields." -- RFC 9112 Section 7.1 - -> "A recipient MUST be able to parse and decode the chunked transfer coding." -- RFC 9112 Section 7.1 - -> "A recipient MUST be able to parse the chunked transfer coding because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 Section 6.1 - -### Chain of reasoning - -1. The test sends `Transfer-Encoding: chunked`, which triggers chunked body parsing per RFC 9112 Section 6.1. -2. Per the ABNF, the server must parse the chunk-size `5` (1*HEXDIG = "5"), read the CRLF, then read exactly 5 octets of chunk-data (`hello`), then read the trailing CRLF. -3. The next line is `0\r\n`, which matches `last-chunk = 1*("0") [ chunk-ext ] CRLF` -- this signals the end of chunked data. -4. The final `\r\n` satisfies the trailing CRLF in the `chunked-body` production. -5. The entire message is syntactically valid against the ABNF grammar. The server has no grounds to reject it. -6. RFC 9112 Section 7.1 uses "MUST be able to parse and decode" -- the strongest normative keyword. Failure to accept this request is a protocol violation. - -### Scored / Unscored justification - -**Scored.** The requirement uses MUST ("A recipient MUST be able to parse and decode the chunked transfer coding"). This is a non-negotiable RFC requirement. Any server claiming HTTP/1.1 support that rejects a syntactically valid single-chunk body is non-compliant. The test expects `2xx` with no fallback to `400` because there is no ambiguity in the grammar or the requirement level. - -### Edge cases - -- Some servers reject chunked encoding on POST if they expect `Content-Length` only -- this violates RFC 9112 Section 6.1 which mandates chunked parsing support. -- Servers behind load balancers may never see chunked requests if the LB de-chunks first, but the server itself must still support it. -- A few lightweight embedded HTTP servers omit chunked support entirely, treating it as an HTTP/1.0-only implementation. This test correctly flags that deficiency. - -## Sources - -- [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunked Body — HTTP/1.1 Compliance" +description: "A valid chunked POST with a single 5-byte chunk followed by the zero terminator. Tested against RFC 9112 Section 7.1." +weight: 6 +--- + +| | | +|---|---| +| **Test ID** | `COMP-CHUNKED-BODY` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MUST accept | +| **Expected** | `2xx` | + +## What it sends + +A valid chunked POST with a single 5-byte chunk followed by the zero terminator. + +```http +POST / HTTP/1.1\r\n +Host: localhost\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n +0\r\n +\r\n +``` + +## What the RFC says + +> "The chunked transfer coding wraps content in order to transfer it as a series of chunks, each with its own size indicator, followed by an OPTIONAL trailer section containing trailer fields." — RFC 9112 Section 7.1 + +> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 Section 7.1 + +> "A recipient MUST be able to parse the chunked transfer coding because it plays a crucial role in framing messages when the content size is not known in advance." — RFC 9112 Section 6.1 + +A server that supports HTTP/1.1 must be able to decode chunked transfer encoding. This is a MUST-level requirement. + +## Why it matters + +Chunked encoding is fundamental to HTTP/1.1 — it enables streaming, server-sent data, and requests where the body size isn't known in advance. If a server can't decode a basic chunked body, it cannot fully participate in HTTP/1.1. + +## Deep Analysis + +### Relevant ABNF grammar + +From RFC 9112 Section 7.1: + +``` +chunked-body = *chunk + last-chunk + trailer-section + CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG +last-chunk = 1*("0") [ chunk-ext ] CRLF + +chunk-data = 1*OCTET ; a sequence of chunk-size octets +trailer-section = *( field-line CRLF ) +``` + +### Direct RFC quotes + +> "The chunked transfer coding wraps content in order to transfer it as a series of chunks, each with its own size indicator, followed by an OPTIONAL trailer section containing trailer fields." -- RFC 9112 Section 7.1 + +> "A recipient MUST be able to parse and decode the chunked transfer coding." -- RFC 9112 Section 7.1 + +> "A recipient MUST be able to parse the chunked transfer coding because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 Section 6.1 + +### Chain of reasoning + +1. The test sends `Transfer-Encoding: chunked`, which triggers chunked body parsing per RFC 9112 Section 6.1. +2. Per the ABNF, the server must parse the chunk-size `5` (1*HEXDIG = "5"), read the CRLF, then read exactly 5 octets of chunk-data (`hello`), then read the trailing CRLF. +3. The next line is `0\r\n`, which matches `last-chunk = 1*("0") [ chunk-ext ] CRLF` -- this signals the end of chunked data. +4. The final `\r\n` satisfies the trailing CRLF in the `chunked-body` production. +5. The entire message is syntactically valid against the ABNF grammar. The server has no grounds to reject it. +6. RFC 9112 Section 7.1 uses "MUST be able to parse and decode" -- the strongest normative keyword. Failure to accept this request is a protocol violation. + +### Scored / Unscored justification + +**Scored.** The requirement uses MUST ("A recipient MUST be able to parse and decode the chunked transfer coding"). This is a non-negotiable RFC requirement. Any server claiming HTTP/1.1 support that rejects a syntactically valid single-chunk body is non-compliant. The test expects `2xx` with no fallback to `400` because there is no ambiguity in the grammar or the requirement level. + +### Edge cases + +- Some servers reject chunked encoding on POST if they expect `Content-Length` only -- this violates RFC 9112 Section 6.1 which mandates chunked parsing support. +- Servers behind load balancers may never see chunked requests if the LB de-chunks first, but the server itself must still support it. +- A few lightweight embedded HTTP servers omit chunked support entirely, treating it as an HTTP/1.0-only implementation. This test correctly flags that deficiency. + +## Sources + +- [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/body/chunked-empty.md b/docs/content/docs/body/chunked-empty.md index 17f5aed..3732b46 100644 --- a/docs/content/docs/body/chunked-empty.md +++ b/docs/content/docs/body/chunked-empty.md @@ -1,92 +1,92 @@ ---- -title: "CHUNKED-EMPTY" -description: "CHUNKED-EMPTY test documentation" -weight: 8 ---- - -| | | -|---|---| -| **Test ID** | `COMP-CHUNKED-EMPTY` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MUST accept | -| **Expected** | `2xx` or close | - -## What it sends - -A chunked POST with only the zero terminator — a zero-length body. - -```http -POST / HTTP/1.1\r\n -Host: localhost\r\n -Transfer-Encoding: chunked\r\n -\r\n -0\r\n -\r\n -``` - -## What the RFC says - -> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 Section 7.1 - -The chunked grammar defines `last-chunk = 1*("0") [ chunk-ext ] CRLF`. A zero-size first chunk is the terminator and indicates an empty body. The server must recognize it and not block waiting for additional data. - -The grammar allows `*chunk` (zero or more data chunks) before the `last-chunk`, so a chunked body containing only the zero terminator is syntactically valid. - -## Why it matters - -Empty chunked bodies occur when a client starts a chunked transfer but has nothing to send, or when a proxy rewrites a zero-length CL body into chunked encoding. The server must handle this edge case cleanly. - -## Deep Analysis - -### Relevant ABNF grammar - -From RFC 9112 Section 7.1: - -``` -chunked-body = *chunk - last-chunk - trailer-section - CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG -last-chunk = 1*("0") [ chunk-ext ] CRLF - -chunk-data = 1*OCTET ; a sequence of chunk-size octets -trailer-section = *( field-line CRLF ) -``` - -### Direct RFC quotes - -> "A recipient MUST be able to parse and decode the chunked transfer coding." -- RFC 9112 Section 7.1 - -> "A recipient MUST be able to parse the chunked transfer coding because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 Section 6.1 - -> "The chunked transfer coding wraps content in order to transfer it as a series of chunks, each with its own size indicator, followed by an OPTIONAL trailer section containing trailer fields." -- RFC 9112 Section 7.1 - -### Chain of reasoning - -1. The test sends `Transfer-Encoding: chunked`, activating chunked body parsing per RFC 9112 Section 6.1. -2. The ABNF production `chunked-body = *chunk last-chunk trailer-section CRLF` uses `*chunk`, meaning **zero or more** data chunks are valid before the `last-chunk`. -3. The first (and only) line of the body is `0\r\n`, which matches `last-chunk = 1*("0") [ chunk-ext ] CRLF`. This is the zero-length terminator with no preceding data chunks. -4. The `trailer-section` production is `*( field-line CRLF )` -- zero or more trailer fields. In this test, there are none. -5. The final `\r\n` satisfies the trailing CRLF in the `chunked-body` production. -6. The complete body `0\r\n\r\n` is a valid instance of `chunked-body` with zero data chunks, zero trailer fields. The grammar explicitly permits this. -7. A server that blocks waiting for additional data after seeing the zero-length chunk has failed to correctly implement the chunked decoder. - -### Scored / Unscored justification - -**Scored.** The requirement uses MUST ("A recipient MUST be able to parse and decode the chunked transfer coding"). The `*chunk` production (zero or more) explicitly allows an empty body. The server must accept this and respond with `2xx` or close the connection cleanly. The `AllowConnectionClose` flag is set because some servers may close the connection after processing a zero-length chunked body, which is acceptable behavior. - -### Edge cases - -- Some servers interpret a zero-length chunked body as "no body at all" and respond with `411 Length Required`, which is incorrect because the framing headers (Transfer-Encoding: chunked) are present and well-formed. -- Proxies may rewrite `Content-Length: 0` into chunked encoding, producing exactly this payload. Servers must handle it. -- A server that hangs waiting for data after the `0\r\n\r\n` terminator has a bug in its chunked state machine -- it is not recognizing the last-chunk production. -- Some implementations require at least one non-zero chunk before the terminator, which contradicts the `*chunk` (zero-or-more) ABNF. - -## Sources - -- [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunked Empty — HTTP/1.1 Compliance" +description: "A chunked POST with only the zero terminator — a zero-length body. Tested against RFC 9112 Section 7.1." +weight: 8 +--- + +| | | +|---|---| +| **Test ID** | `COMP-CHUNKED-EMPTY` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MUST accept | +| **Expected** | `2xx` or close | + +## What it sends + +A chunked POST with only the zero terminator — a zero-length body. + +```http +POST / HTTP/1.1\r\n +Host: localhost\r\n +Transfer-Encoding: chunked\r\n +\r\n +0\r\n +\r\n +``` + +## What the RFC says + +> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 Section 7.1 + +The chunked grammar defines `last-chunk = 1*("0") [ chunk-ext ] CRLF`. A zero-size first chunk is the terminator and indicates an empty body. The server must recognize it and not block waiting for additional data. + +The grammar allows `*chunk` (zero or more data chunks) before the `last-chunk`, so a chunked body containing only the zero terminator is syntactically valid. + +## Why it matters + +Empty chunked bodies occur when a client starts a chunked transfer but has nothing to send, or when a proxy rewrites a zero-length CL body into chunked encoding. The server must handle this edge case cleanly. + +## Deep Analysis + +### Relevant ABNF grammar + +From RFC 9112 Section 7.1: + +``` +chunked-body = *chunk + last-chunk + trailer-section + CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG +last-chunk = 1*("0") [ chunk-ext ] CRLF + +chunk-data = 1*OCTET ; a sequence of chunk-size octets +trailer-section = *( field-line CRLF ) +``` + +### Direct RFC quotes + +> "A recipient MUST be able to parse and decode the chunked transfer coding." -- RFC 9112 Section 7.1 + +> "A recipient MUST be able to parse the chunked transfer coding because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 Section 6.1 + +> "The chunked transfer coding wraps content in order to transfer it as a series of chunks, each with its own size indicator, followed by an OPTIONAL trailer section containing trailer fields." -- RFC 9112 Section 7.1 + +### Chain of reasoning + +1. The test sends `Transfer-Encoding: chunked`, activating chunked body parsing per RFC 9112 Section 6.1. +2. The ABNF production `chunked-body = *chunk last-chunk trailer-section CRLF` uses `*chunk`, meaning **zero or more** data chunks are valid before the `last-chunk`. +3. The first (and only) line of the body is `0\r\n`, which matches `last-chunk = 1*("0") [ chunk-ext ] CRLF`. This is the zero-length terminator with no preceding data chunks. +4. The `trailer-section` production is `*( field-line CRLF )` -- zero or more trailer fields. In this test, there are none. +5. The final `\r\n` satisfies the trailing CRLF in the `chunked-body` production. +6. The complete body `0\r\n\r\n` is a valid instance of `chunked-body` with zero data chunks, zero trailer fields. The grammar explicitly permits this. +7. A server that blocks waiting for additional data after seeing the zero-length chunk has failed to correctly implement the chunked decoder. + +### Scored / Unscored justification + +**Scored.** The requirement uses MUST ("A recipient MUST be able to parse and decode the chunked transfer coding"). The `*chunk` production (zero or more) explicitly allows an empty body. The server must accept this and respond with `2xx` or close the connection cleanly. The `AllowConnectionClose` flag is set because some servers may close the connection after processing a zero-length chunked body, which is acceptable behavior. + +### Edge cases + +- Some servers interpret a zero-length chunked body as "no body at all" and respond with `411 Length Required`, which is incorrect because the framing headers (Transfer-Encoding: chunked) are present and well-formed. +- Proxies may rewrite `Content-Length: 0` into chunked encoding, producing exactly this payload. Servers must handle it. +- A server that hangs waiting for data after the `0\r\n\r\n` terminator has a bug in its chunked state machine -- it is not recognizing the last-chunk production. +- Some implementations require at least one non-zero chunk before the terminator, which contradicts the `*chunk` (zero-or-more) ABNF. + +## Sources + +- [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/body/chunked-extension.md b/docs/content/docs/body/chunked-extension.md index b77c5b5..2d0d06e 100644 --- a/docs/content/docs/body/chunked-extension.md +++ b/docs/content/docs/body/chunked-extension.md @@ -1,93 +1,93 @@ ---- -title: "CHUNKED-EXTENSION" -description: "CHUNKED-EXTENSION test documentation" -weight: 10 ---- - -| | | -|---|---| -| **Test ID** | `COMP-CHUNKED-EXTENSION` | -| **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` = Pass, `400` = Warn | - -## What it sends - -A chunked POST where the chunk size line includes a valid extension: `5;ext=value`. - -```http -POST / HTTP/1.1\r\n -Host: localhost\r\n -Transfer-Encoding: chunked\r\n -\r\n -5;ext=value\r\n -hello\r\n -0\r\n -\r\n -``` - -## What the RFC says - -> "The chunked coding allows each chunk to include zero or more chunk extensions, immediately following the chunk-size, for the sake of supplying per-chunk metadata (such as a signature or hash), mid-message control information, or randomization of message body size." — RFC 9112 Section 7.1.1 - -> "A recipient MUST ignore unrecognized chunk extensions." — RFC 9112 Section 7.1.1 - -> "A server ought to limit the total length of chunk extensions received in a request to an amount reasonable for the services provided, in the same way that it applies length limitations and timeouts for other parts of a message, and generate an appropriate 4xx (Client Error) response if that amount is exceeded." — RFC 9112 Section 7.1.1 - -Chunk extensions are part of the chunked encoding grammar. A compliant parser must ignore unrecognized extensions and process the chunk data normally. - -## Why it matters - -While chunk extensions are rarely used in practice, they are syntactically valid. A server that rejects them has an overly strict chunk parser that may break with legitimate clients or proxies that add extensions for metadata (e.g., checksums, signatures). - -## Deep Analysis - -### Relevant ABNF grammar - -From RFC 9112 Section 7.1 and 7.1.1: - -``` -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG - -chunk-ext = *( BWS ";" BWS chunk-ext-name - [ BWS "=" BWS chunk-ext-val ] ) - -chunk-ext-name = token -chunk-ext-val = token / quoted-string -``` - -### Direct RFC quotes - -> "The chunked coding allows each chunk to include zero or more chunk extensions, immediately following the chunk-size, for the sake of supplying per-chunk metadata (such as a signature or hash), mid-message control information, or randomization of message body size." -- RFC 9112 Section 7.1.1 - -> "A recipient MUST ignore unrecognized chunk extensions." -- RFC 9112 Section 7.1.1 - -> "A server ought to limit the total length of chunk extensions received in a request to an amount reasonable for the services provided, in the same way that it applies length limitations and timeouts for other parts of a message, and generate an appropriate 4xx (Client Error) response if that amount is exceeded." -- RFC 9112 Section 7.1.1 - -### Chain of reasoning - -1. The test sends chunk-size line `5;ext=value\r\n`. Parsing this against the ABNF: `chunk-size` matches `5`, then `chunk-ext` matches `;ext=value` where `ext` is the `chunk-ext-name` (a token) and `value` is the `chunk-ext-val` (also a token). -2. The `chunk` production explicitly includes `[ chunk-ext ]` -- chunk extensions are an optional but grammatically valid part of every chunk. -3. RFC 9112 Section 7.1.1 states recipients "MUST ignore unrecognized chunk extensions". The word "ignore" means the server must parse past them and process the chunk-data normally. -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. - -### Scoring justification - -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 - -- Some servers strip chunk extensions before passing data to the application layer -- this is correct behavior per "MUST ignore unrecognized chunk extensions." -- A few servers fail to parse the semicolon delimiter and treat `5;ext=value` as an invalid chunk-size, returning `400`. This is a parser bug, not a policy decision. -- Chunk extensions with quoted-string values (e.g., `5;ext="hello world"`) are also valid per the ABNF but may trigger additional parser failures in implementations that only handle token values. -- The BWS (bad whitespace) allowance means `5 ; ext = value` is also technically valid, though rarely seen in practice. - -## Sources - -- [RFC 9112 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) +--- +title: "Chunked Extension — HTTP/1.1 Compliance" +description: "A chunked POST where the chunk size line includes a valid extension: 5;ext=value. Tested against RFC 9112 Section 7.1.1." +weight: 10 +--- + +| | | +|---|---| +| **Test ID** | `COMP-CHUNKED-EXTENSION` | +| **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` = Pass, `400` = Warn | + +## What it sends + +A chunked POST where the chunk size line includes a valid extension: `5;ext=value`. + +```http +POST / HTTP/1.1\r\n +Host: localhost\r\n +Transfer-Encoding: chunked\r\n +\r\n +5;ext=value\r\n +hello\r\n +0\r\n +\r\n +``` + +## What the RFC says + +> "The chunked coding allows each chunk to include zero or more chunk extensions, immediately following the chunk-size, for the sake of supplying per-chunk metadata (such as a signature or hash), mid-message control information, or randomization of message body size." — RFC 9112 Section 7.1.1 + +> "A recipient MUST ignore unrecognized chunk extensions." — RFC 9112 Section 7.1.1 + +> "A server ought to limit the total length of chunk extensions received in a request to an amount reasonable for the services provided, in the same way that it applies length limitations and timeouts for other parts of a message, and generate an appropriate 4xx (Client Error) response if that amount is exceeded." — RFC 9112 Section 7.1.1 + +Chunk extensions are part of the chunked encoding grammar. A compliant parser must ignore unrecognized extensions and process the chunk data normally. + +## Why it matters + +While chunk extensions are rarely used in practice, they are syntactically valid. A server that rejects them has an overly strict chunk parser that may break with legitimate clients or proxies that add extensions for metadata (e.g., checksums, signatures). + +## Deep Analysis + +### Relevant ABNF grammar + +From RFC 9112 Section 7.1 and 7.1.1: + +``` +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG + +chunk-ext = *( BWS ";" BWS chunk-ext-name + [ BWS "=" BWS chunk-ext-val ] ) + +chunk-ext-name = token +chunk-ext-val = token / quoted-string +``` + +### Direct RFC quotes + +> "The chunked coding allows each chunk to include zero or more chunk extensions, immediately following the chunk-size, for the sake of supplying per-chunk metadata (such as a signature or hash), mid-message control information, or randomization of message body size." -- RFC 9112 Section 7.1.1 + +> "A recipient MUST ignore unrecognized chunk extensions." -- RFC 9112 Section 7.1.1 + +> "A server ought to limit the total length of chunk extensions received in a request to an amount reasonable for the services provided, in the same way that it applies length limitations and timeouts for other parts of a message, and generate an appropriate 4xx (Client Error) response if that amount is exceeded." -- RFC 9112 Section 7.1.1 + +### Chain of reasoning + +1. The test sends chunk-size line `5;ext=value\r\n`. Parsing this against the ABNF: `chunk-size` matches `5`, then `chunk-ext` matches `;ext=value` where `ext` is the `chunk-ext-name` (a token) and `value` is the `chunk-ext-val` (also a token). +2. The `chunk` production explicitly includes `[ chunk-ext ]` -- chunk extensions are an optional but grammatically valid part of every chunk. +3. RFC 9112 Section 7.1.1 states recipients "MUST ignore unrecognized chunk extensions". The word "ignore" means the server must parse past them and process the chunk-data normally. +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. + +### Scoring justification + +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 + +- Some servers strip chunk extensions before passing data to the application layer -- this is correct behavior per "MUST ignore unrecognized chunk extensions." +- A few servers fail to parse the semicolon delimiter and treat `5;ext=value` as an invalid chunk-size, returning `400`. This is a parser bug, not a policy decision. +- Chunk extensions with quoted-string values (e.g., `5;ext="hello world"`) are also valid per the ABNF but may trigger additional parser failures in implementations that only handle token values. +- The BWS (bad whitespace) allowance means `5 ; ext = value` is also technically valid, though rarely seen in practice. + +## Sources + +- [RFC 9112 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) diff --git a/docs/content/docs/body/chunked-hex-uppercase.md b/docs/content/docs/body/chunked-hex-uppercase.md index f99d76c..da07012 100644 --- a/docs/content/docs/body/chunked-hex-uppercase.md +++ b/docs/content/docs/body/chunked-hex-uppercase.md @@ -1,94 +1,94 @@ ---- -title: "CHUNKED-HEX-UPPERCASE" -description: "CHUNKED-HEX-UPPERCASE test documentation" -weight: 12 ---- - -| | | -|---|---| -| **Test ID** | `COMP-CHUNKED-HEX-UPPERCASE` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MUST accept | -| **Expected** | `2xx` | - -## What it sends - -A valid chunked POST where the chunk size is expressed using an uppercase hexadecimal digit: `A` (which equals 10 in decimal), followed by exactly 10 bytes of data. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -\r\n -A\r\n -helloworld\r\n -0\r\n -\r\n -``` - -The chunk size `A` is uppercase hex for 10. The chunk data `helloworld` is exactly 10 bytes. - -## What the RFC says - -> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 Section 7.1 - -The chunked grammar defines `chunk-size = 1*HEXDIG`. `HEXDIG` is defined in RFC 5234 (ABNF) as `DIGIT / "A" / "B" / "C" / "D" / "E" / "F"`, and ABNF string matching is case-insensitive by definition. Both `a` and `A` represent the decimal value 10. A compliant chunked parser must accept hex digits in any case. - -> "Recipients MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer representation." — RFC 9112 Section 7.1 - -## Why it matters - -While most chunk sizes in practice are small decimal numbers (like `5` or `1a`), the grammar allows any combination of uppercase and lowercase hex digits. A parser that only handles lowercase hex, or only decimal digits, will fail on legitimate chunked bodies. This is a basic interoperability requirement for any HTTP/1.1 implementation. - -## Deep Analysis - -### Relevant ABNF grammar - -From RFC 9112 Section 7.1: - -``` -chunk-size = 1*HEXDIG -``` - -From RFC 5234 Appendix B.1 (Core ABNF): - -``` -HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" -DIGIT = %x30-39 ; 0-9 -``` - -Note: RFC 5234 Section 2.3 states that ABNF strings are case-insensitive. The HEXDIG definition listing uppercase `"A"` through `"F"` implicitly includes `"a"` through `"f"`. - -### Direct RFC quotes - -> "A recipient MUST be able to parse and decode the chunked transfer coding." -- RFC 9112 Section 7.1 - -> "Recipients MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer representation." -- RFC 9112 Section 7.1 - -> "HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"" -- RFC 5234 Appendix B.1 - -### Chain of reasoning - -1. The test sends chunk-size `A\r\n` followed by exactly 10 bytes of data (`helloworld`). -2. The `chunk-size` ABNF production is `1*HEXDIG`, requiring one or more hexadecimal digits. -3. `HEXDIG` is defined in RFC 5234 as `DIGIT / "A" / "B" / "C" / "D" / "E" / "F"`. Per RFC 5234 Section 2.3, ABNF string comparison is case-insensitive, so both `A` and `a` are valid HEXDIG values. -4. `A` in hexadecimal equals 10 in decimal. The test provides exactly 10 bytes of chunk-data, satisfying the `chunk-data = 1*OCTET` production with the correct length. -5. The `0\r\n\r\n` terminator satisfies `last-chunk` and the trailing CRLF. -6. The entire message is a valid `chunked-body`. The MUST requirement to "parse and decode" chunked encoding necessarily includes correctly interpreting hex digits of any case. - -### Scored / Unscored justification - -**Scored.** The MUST requirement ("A recipient MUST be able to parse and decode the chunked transfer coding") encompasses correct hex parsing. Since `chunk-size = 1*HEXDIG` and HEXDIG is case-insensitive by ABNF rules, rejecting uppercase hex is a failure to parse valid chunked encoding. There is no SHOULD or MAY ambiguity -- the grammar is unambiguous and the requirement is MUST-level. - -### Edge cases - -- Some implementations use `strtol()` or equivalent with base 16, which naturally handles both cases. These pass without issue. -- Implementations that use a hand-rolled hex parser with only `0-9` and `a-f` ranges (missing `A-F`) will fail this test. This is a common bug in minimal HTTP parsers. -- Mixed-case chunk sizes like `1a`, `1A`, `1b3F` are all equally valid per HEXDIG case-insensitivity. This test uses pure uppercase to catch the most common parser limitation. -- The RFC also warns about large hex numerals causing integer overflow. While this test uses a small value (`A` = 10), the parser must be robust against both case variation and large values. - -## Sources - -- [RFC 9112 §7.1 -- Chunked Transfer Coding](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) -- [RFC 5234 -- ABNF (HEXDIG definition)](https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1) +--- +title: "Chunked Hex Uppercase — HTTP/1.1 Compliance" +description: "A valid chunked POST where the chunk size is expressed using an uppercase hexadecimal digit: A (which equals 10 in decimal), followed by exactly 10 bytes of data. Tested against RFC 9112 §7.1." +weight: 12 +--- + +| | | +|---|---| +| **Test ID** | `COMP-CHUNKED-HEX-UPPERCASE` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MUST accept | +| **Expected** | `2xx` | + +## What it sends + +A valid chunked POST where the chunk size is expressed using an uppercase hexadecimal digit: `A` (which equals 10 in decimal), followed by exactly 10 bytes of data. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +A\r\n +helloworld\r\n +0\r\n +\r\n +``` + +The chunk size `A` is uppercase hex for 10. The chunk data `helloworld` is exactly 10 bytes. + +## What the RFC says + +> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 Section 7.1 + +The chunked grammar defines `chunk-size = 1*HEXDIG`. `HEXDIG` is defined in RFC 5234 (ABNF) as `DIGIT / "A" / "B" / "C" / "D" / "E" / "F"`, and ABNF string matching is case-insensitive by definition. Both `a` and `A` represent the decimal value 10. A compliant chunked parser must accept hex digits in any case. + +> "Recipients MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer representation." — RFC 9112 Section 7.1 + +## Why it matters + +While most chunk sizes in practice are small decimal numbers (like `5` or `1a`), the grammar allows any combination of uppercase and lowercase hex digits. A parser that only handles lowercase hex, or only decimal digits, will fail on legitimate chunked bodies. This is a basic interoperability requirement for any HTTP/1.1 implementation. + +## Deep Analysis + +### Relevant ABNF grammar + +From RFC 9112 Section 7.1: + +``` +chunk-size = 1*HEXDIG +``` + +From RFC 5234 Appendix B.1 (Core ABNF): + +``` +HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" +DIGIT = %x30-39 ; 0-9 +``` + +Note: RFC 5234 Section 2.3 states that ABNF strings are case-insensitive. The HEXDIG definition listing uppercase `"A"` through `"F"` implicitly includes `"a"` through `"f"`. + +### Direct RFC quotes + +> "A recipient MUST be able to parse and decode the chunked transfer coding." -- RFC 9112 Section 7.1 + +> "Recipients MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer representation." -- RFC 9112 Section 7.1 + +> "HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"" -- RFC 5234 Appendix B.1 + +### Chain of reasoning + +1. The test sends chunk-size `A\r\n` followed by exactly 10 bytes of data (`helloworld`). +2. The `chunk-size` ABNF production is `1*HEXDIG`, requiring one or more hexadecimal digits. +3. `HEXDIG` is defined in RFC 5234 as `DIGIT / "A" / "B" / "C" / "D" / "E" / "F"`. Per RFC 5234 Section 2.3, ABNF string comparison is case-insensitive, so both `A` and `a` are valid HEXDIG values. +4. `A` in hexadecimal equals 10 in decimal. The test provides exactly 10 bytes of chunk-data, satisfying the `chunk-data = 1*OCTET` production with the correct length. +5. The `0\r\n\r\n` terminator satisfies `last-chunk` and the trailing CRLF. +6. The entire message is a valid `chunked-body`. The MUST requirement to "parse and decode" chunked encoding necessarily includes correctly interpreting hex digits of any case. + +### Scored / Unscored justification + +**Scored.** The MUST requirement ("A recipient MUST be able to parse and decode the chunked transfer coding") encompasses correct hex parsing. Since `chunk-size = 1*HEXDIG` and HEXDIG is case-insensitive by ABNF rules, rejecting uppercase hex is a failure to parse valid chunked encoding. There is no SHOULD or MAY ambiguity -- the grammar is unambiguous and the requirement is MUST-level. + +### Edge cases + +- Some implementations use `strtol()` or equivalent with base 16, which naturally handles both cases. These pass without issue. +- Implementations that use a hand-rolled hex parser with only `0-9` and `a-f` ranges (missing `A-F`) will fail this test. This is a common bug in minimal HTTP parsers. +- Mixed-case chunk sizes like `1a`, `1A`, `1b3F` are all equally valid per HEXDIG case-insensitivity. This test uses pure uppercase to catch the most common parser limitation. +- The RFC also warns about large hex numerals causing integer overflow. While this test uses a small value (`A` = 10), the parser must be robust against both case variation and large values. + +## Sources + +- [RFC 9112 §7.1 -- Chunked Transfer Coding](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +- [RFC 5234 -- ABNF (HEXDIG definition)](https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1) diff --git a/docs/content/docs/body/chunked-multi.md b/docs/content/docs/body/chunked-multi.md index e916b6d..063acd0 100644 --- a/docs/content/docs/body/chunked-multi.md +++ b/docs/content/docs/body/chunked-multi.md @@ -1,108 +1,108 @@ ---- -title: "CHUNKED-MULTI" -description: "CHUNKED-MULTI test documentation" -weight: 7 ---- - -| | | -|---|---| -| **Test ID** | `COMP-CHUNKED-MULTI` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MUST accept | -| **Expected** | `2xx` | - -## What it sends - -A valid chunked POST with two data chunks (5 bytes + 6 bytes) followed by the zero terminator. - -```http -POST / HTTP/1.1\r\n -Host: localhost\r\n -Transfer-Encoding: chunked\r\n -\r\n -5\r\n -hello\r\n -6\r\n - world\r\n -0\r\n -\r\n -``` - -## What the RFC says - -> "The chunked transfer coding wraps content in order to transfer it as a series of chunks, each with its own size indicator." — RFC 9112 Section 7.1 - -> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 Section 7.1 - -The chunked grammar defines the body as `*chunk last-chunk trailer-section CRLF` — zero or more data chunks followed by the zero-length terminator. The server must concatenate all chunks to reconstruct the full body. This tests that the chunk parser correctly handles multiple consecutive data chunks before the terminator. - -## Why it matters - -Multi-chunk bodies are the norm in real-world HTTP — streaming uploads, large form submissions, and proxied requests all use multiple chunks. A server that only handles single-chunk bodies has an incomplete chunked decoder. - -## Deep Analysis - -### Relevant ABNF grammar - -From RFC 9112 Section 7.1: - -``` -chunked-body = *chunk - last-chunk - trailer-section - CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG -last-chunk = 1*("0") [ chunk-ext ] CRLF - -chunk-data = 1*OCTET ; a sequence of chunk-size octets -trailer-section = *( field-line CRLF ) -``` - -From RFC 9112 Section 7.1.3 (Decoding Chunked), the pseudocode algorithm: - -``` -length := 0 -read chunk-size, chunk-ext (if any), and CRLF -while (chunk-size > 0) { - read chunk-data and CRLF - append chunk-data to content - length := length + chunk-size - read chunk-size, chunk-ext (if any), and CRLF -} -``` - -### Direct RFC quotes - -> "The chunked transfer coding wraps content in order to transfer it as a series of chunks, each with its own size indicator." -- RFC 9112 Section 7.1 - -> "A recipient MUST be able to parse and decode the chunked transfer coding." -- RFC 9112 Section 7.1 - -> "A recipient MUST be able to parse the chunked transfer coding because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 Section 6.1 - -### Chain of reasoning - -1. The test sends `Transfer-Encoding: chunked` with two data chunks: `5\r\nhello\r\n` (5 bytes) and `6\r\n world\r\n` (6 bytes), followed by the `0\r\n\r\n` terminator. -2. The ABNF production `chunked-body = *chunk last-chunk ...` uses `*chunk`, meaning zero or more data chunks. This test exercises the "more than one" case. -3. Per the decoding pseudocode in RFC 9112 Section 7.1.3, the server enters the while loop, reads chunk 1 (size=5, data="hello"), appends it, then reads chunk 2 (size=6, data=" world"), appends it, then reads chunk-size=0 and exits the loop. -4. The reconstructed content is "hello world" (11 bytes). The server must concatenate all chunk-data segments in order. -5. Both chunks individually conform to the `chunk` production: each has a valid `chunk-size`, followed by CRLF, followed by exactly `chunk-size` octets of `chunk-data`, followed by CRLF. -6. The entire message is a valid `chunked-body`. The MUST requirement to "parse and decode" applies to multi-chunk bodies just as it does to single-chunk bodies. - -### Scored / Unscored justification - -**Scored.** The MUST requirement ("A recipient MUST be able to parse and decode the chunked transfer coding") applies to the full generality of the chunked ABNF, including the `*chunk` repetition. A server that can only handle a single chunk has not fully implemented the chunked decoder. The `*chunk` production explicitly models the multi-chunk case, making this a MUST-level compliance test. - -### Edge cases - -- Some servers read only the first chunk and treat chunk-size=0 in the *next* read as a new request, causing desynchronization. This is a critical parsing bug. -- The second chunk begins with a space character (` world`), which is valid chunk-data. Servers must not trim or interpret chunk-data content. -- Servers that buffer the entire body before processing should concatenate to "hello world" (11 bytes). Servers that stream should emit chunks in order. -- The maximum number of chunks is unbounded by the grammar (`*chunk`). This test uses just two, but a compliant server must handle arbitrarily many. - -## Sources - -- [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunked Multi — HTTP/1.1 Compliance" +description: "A valid chunked POST with two data chunks (5 bytes + 6 bytes) followed by the zero terminator. Tested against RFC 9112 Section 7.1." +weight: 7 +--- + +| | | +|---|---| +| **Test ID** | `COMP-CHUNKED-MULTI` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MUST accept | +| **Expected** | `2xx` | + +## What it sends + +A valid chunked POST with two data chunks (5 bytes + 6 bytes) followed by the zero terminator. + +```http +POST / HTTP/1.1\r\n +Host: localhost\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n +6\r\n + world\r\n +0\r\n +\r\n +``` + +## What the RFC says + +> "The chunked transfer coding wraps content in order to transfer it as a series of chunks, each with its own size indicator." — RFC 9112 Section 7.1 + +> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 Section 7.1 + +The chunked grammar defines the body as `*chunk last-chunk trailer-section CRLF` — zero or more data chunks followed by the zero-length terminator. The server must concatenate all chunks to reconstruct the full body. This tests that the chunk parser correctly handles multiple consecutive data chunks before the terminator. + +## Why it matters + +Multi-chunk bodies are the norm in real-world HTTP — streaming uploads, large form submissions, and proxied requests all use multiple chunks. A server that only handles single-chunk bodies has an incomplete chunked decoder. + +## Deep Analysis + +### Relevant ABNF grammar + +From RFC 9112 Section 7.1: + +``` +chunked-body = *chunk + last-chunk + trailer-section + CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG +last-chunk = 1*("0") [ chunk-ext ] CRLF + +chunk-data = 1*OCTET ; a sequence of chunk-size octets +trailer-section = *( field-line CRLF ) +``` + +From RFC 9112 Section 7.1.3 (Decoding Chunked), the pseudocode algorithm: + +``` +length := 0 +read chunk-size, chunk-ext (if any), and CRLF +while (chunk-size > 0) { + read chunk-data and CRLF + append chunk-data to content + length := length + chunk-size + read chunk-size, chunk-ext (if any), and CRLF +} +``` + +### Direct RFC quotes + +> "The chunked transfer coding wraps content in order to transfer it as a series of chunks, each with its own size indicator." -- RFC 9112 Section 7.1 + +> "A recipient MUST be able to parse and decode the chunked transfer coding." -- RFC 9112 Section 7.1 + +> "A recipient MUST be able to parse the chunked transfer coding because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 Section 6.1 + +### Chain of reasoning + +1. The test sends `Transfer-Encoding: chunked` with two data chunks: `5\r\nhello\r\n` (5 bytes) and `6\r\n world\r\n` (6 bytes), followed by the `0\r\n\r\n` terminator. +2. The ABNF production `chunked-body = *chunk last-chunk ...` uses `*chunk`, meaning zero or more data chunks. This test exercises the "more than one" case. +3. Per the decoding pseudocode in RFC 9112 Section 7.1.3, the server enters the while loop, reads chunk 1 (size=5, data="hello"), appends it, then reads chunk 2 (size=6, data=" world"), appends it, then reads chunk-size=0 and exits the loop. +4. The reconstructed content is "hello world" (11 bytes). The server must concatenate all chunk-data segments in order. +5. Both chunks individually conform to the `chunk` production: each has a valid `chunk-size`, followed by CRLF, followed by exactly `chunk-size` octets of `chunk-data`, followed by CRLF. +6. The entire message is a valid `chunked-body`. The MUST requirement to "parse and decode" applies to multi-chunk bodies just as it does to single-chunk bodies. + +### Scored / Unscored justification + +**Scored.** The MUST requirement ("A recipient MUST be able to parse and decode the chunked transfer coding") applies to the full generality of the chunked ABNF, including the `*chunk` repetition. A server that can only handle a single chunk has not fully implemented the chunked decoder. The `*chunk` production explicitly models the multi-chunk case, making this a MUST-level compliance test. + +### Edge cases + +- Some servers read only the first chunk and treat chunk-size=0 in the *next* read as a new request, causing desynchronization. This is a critical parsing bug. +- The second chunk begins with a space character (` world`), which is valid chunk-data. Servers must not trim or interpret chunk-data content. +- Servers that buffer the entire body before processing should concatenate to "hello world" (11 bytes). Servers that stream should emit chunks in order. +- The maximum number of chunks is unbounded by the grammar (`*chunk`). This test uses just two, but a compliant server must handle arbitrarily many. + +## Sources + +- [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/body/chunked-no-final.md b/docs/content/docs/body/chunked-no-final.md index 84d5093..a4eddb4 100644 --- a/docs/content/docs/body/chunked-no-final.md +++ b/docs/content/docs/body/chunked-no-final.md @@ -1,91 +1,91 @@ ---- -title: "CHUNKED-NO-FINAL" -description: "CHUNKED-NO-FINAL test documentation" -weight: 9 ---- - -| | | -|---|---| -| **Test ID** | `COMP-CHUNKED-NO-FINAL` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MUST read until terminator | -| **Expected** | `400`, close, or timeout | - -## What it sends - -A chunked POST with one data chunk but no zero terminator. The connection then goes silent. - -```http -POST / HTTP/1.1\r\n -Host: localhost\r\n -Transfer-Encoding: chunked\r\n -\r\n -5\r\n -hello\r\n -``` - -## What the RFC says - -> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 Section 7.1 - -The chunked grammar requires a `last-chunk` (zero-size chunk) to signal the end of the body: `chunked-body = *chunk last-chunk trailer-section CRLF`. Without the `0\r\n\r\n` terminator, the transfer is incomplete. The server must continue waiting for more chunks until its read timeout fires, because the chunked framing has not been satisfied. - -## Why it matters - -A server that responds before seeing the zero terminator risks connection desynchronization — subsequent requests on the same connection could be misinterpreted. This is analogous to the Content-Length undersend scenario but for chunked encoding. - -## Deep Analysis - -### Relevant ABNF grammar - -From RFC 9112 Section 7.1: - -``` -chunked-body = *chunk - last-chunk - trailer-section - CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG -last-chunk = 1*("0") [ chunk-ext ] CRLF -``` - -From RFC 9112 Section 6.3 (rule 4 for chunked): - -> "the message body length is determined by reading and decoding the chunked data until the transfer coding indicates the data is complete." - -### Direct RFC quotes - -> "A recipient MUST be able to parse and decode the chunked transfer coding." -- RFC 9112 Section 7.1 - -> "The message body length is determined by reading and decoding the chunked data until the transfer coding indicates the data is complete." -- RFC 9112 Section 6.3 - -> "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." -- RFC 9112 Section 6.3 - -### Chain of reasoning - -1. The test sends `Transfer-Encoding: chunked`, triggering chunked body parsing. -2. The server reads chunk-size `5`, CRLF, 5 bytes of chunk-data (`hello`), and the trailing CRLF. This satisfies one `chunk` production. -3. Per the ABNF, `chunked-body` requires `last-chunk` after `*chunk`. The server must now attempt to read the next chunk-size to determine if it is a data chunk or the `last-chunk` (zero terminator). -4. The test sends **no more data**. The connection goes silent. -5. The server is now blocked reading the next chunk-size. Per RFC 9112 Section 6.3, the message body length for chunked encoding is "determined by reading and decoding the chunked data until the transfer coding indicates the data is complete." The data is NOT complete because no `last-chunk` has been received. -6. The server must either: (a) wait until its read timeout fires and then close the connection, (b) return `400` after detecting the incomplete transfer, or (c) close the connection immediately upon detecting the stall. -7. A `2xx` response at this point would mean the server processed the request before the chunked body was complete, which desynchronizes the connection. - -### Scored / Unscored justification - -**Scored.** The MUST requirement to "parse and decode the chunked transfer coding" implicitly requires the server to follow the complete `chunked-body` grammar, which includes the mandatory `last-chunk`. The server MUST NOT treat a partial chunked body as complete. Responding with `2xx` before seeing the zero terminator is a protocol violation because it means the server did not fully consume the chunked body, leaving bytes on the connection that could be misinterpreted as a subsequent request. - -### Edge cases - -- Some servers set aggressive read timeouts (e.g., 5 seconds) and close the connection quickly. This is acceptable -- the test allows `400`, close, or timeout. -- A server that responds `2xx` immediately after the first chunk (without waiting for the terminator) has a critical desynchronization bug. On a persistent connection, the leftover `0\r\n\r\n` of a subsequent proper request could be misinterpreted. -- Some servers attempt to detect incomplete chunked bodies and return `400 Bad Request` -- this is the cleanest error-handling approach. -- Reverse proxies may impose their own chunked read timeouts, which could mask this behavior from the origin server. - -## Sources - -- [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunked No Final — HTTP/1.1 Compliance" +description: "A chunked POST with one data chunk but no zero terminator. The connection then goes silent. Tested against RFC 9112 Section 7.1." +weight: 9 +--- + +| | | +|---|---| +| **Test ID** | `COMP-CHUNKED-NO-FINAL` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MUST read until terminator | +| **Expected** | `400`, close, or timeout | + +## What it sends + +A chunked POST with one data chunk but no zero terminator. The connection then goes silent. + +```http +POST / HTTP/1.1\r\n +Host: localhost\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n +``` + +## What the RFC says + +> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 Section 7.1 + +The chunked grammar requires a `last-chunk` (zero-size chunk) to signal the end of the body: `chunked-body = *chunk last-chunk trailer-section CRLF`. Without the `0\r\n\r\n` terminator, the transfer is incomplete. The server must continue waiting for more chunks until its read timeout fires, because the chunked framing has not been satisfied. + +## Why it matters + +A server that responds before seeing the zero terminator risks connection desynchronization — subsequent requests on the same connection could be misinterpreted. This is analogous to the Content-Length undersend scenario but for chunked encoding. + +## Deep Analysis + +### Relevant ABNF grammar + +From RFC 9112 Section 7.1: + +``` +chunked-body = *chunk + last-chunk + trailer-section + CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG +last-chunk = 1*("0") [ chunk-ext ] CRLF +``` + +From RFC 9112 Section 6.3 (rule 4 for chunked): + +> "the message body length is determined by reading and decoding the chunked data until the transfer coding indicates the data is complete." + +### Direct RFC quotes + +> "A recipient MUST be able to parse and decode the chunked transfer coding." -- RFC 9112 Section 7.1 + +> "The message body length is determined by reading and decoding the chunked data until the transfer coding indicates the data is complete." -- RFC 9112 Section 6.3 + +> "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." -- RFC 9112 Section 6.3 + +### Chain of reasoning + +1. The test sends `Transfer-Encoding: chunked`, triggering chunked body parsing. +2. The server reads chunk-size `5`, CRLF, 5 bytes of chunk-data (`hello`), and the trailing CRLF. This satisfies one `chunk` production. +3. Per the ABNF, `chunked-body` requires `last-chunk` after `*chunk`. The server must now attempt to read the next chunk-size to determine if it is a data chunk or the `last-chunk` (zero terminator). +4. The test sends **no more data**. The connection goes silent. +5. The server is now blocked reading the next chunk-size. Per RFC 9112 Section 6.3, the message body length for chunked encoding is "determined by reading and decoding the chunked data until the transfer coding indicates the data is complete." The data is NOT complete because no `last-chunk` has been received. +6. The server must either: (a) wait until its read timeout fires and then close the connection, (b) return `400` after detecting the incomplete transfer, or (c) close the connection immediately upon detecting the stall. +7. A `2xx` response at this point would mean the server processed the request before the chunked body was complete, which desynchronizes the connection. + +### Scored / Unscored justification + +**Scored.** The MUST requirement to "parse and decode the chunked transfer coding" implicitly requires the server to follow the complete `chunked-body` grammar, which includes the mandatory `last-chunk`. The server MUST NOT treat a partial chunked body as complete. Responding with `2xx` before seeing the zero terminator is a protocol violation because it means the server did not fully consume the chunked body, leaving bytes on the connection that could be misinterpreted as a subsequent request. + +### Edge cases + +- Some servers set aggressive read timeouts (e.g., 5 seconds) and close the connection quickly. This is acceptable -- the test allows `400`, close, or timeout. +- A server that responds `2xx` immediately after the first chunk (without waiting for the terminator) has a critical desynchronization bug. On a persistent connection, the leftover `0\r\n\r\n` of a subsequent proper request could be misinterpreted. +- Some servers attempt to detect incomplete chunked bodies and return `400 Bad Request` -- this is the cleanest error-handling approach. +- Reverse proxies may impose their own chunked read timeouts, which could mask this behavior from the origin server. + +## Sources + +- [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/body/chunked-trailer-valid.md b/docs/content/docs/body/chunked-trailer-valid.md index ebb527a..3eaf5ea 100644 --- a/docs/content/docs/body/chunked-trailer-valid.md +++ b/docs/content/docs/body/chunked-trailer-valid.md @@ -1,100 +1,100 @@ ---- -title: "CHUNKED-TRAILER-VALID" -description: "CHUNKED-TRAILER-VALID test documentation" -weight: 11 ---- - -| | | -|---|---| -| **Test ID** | `COMP-CHUNKED-TRAILER-VALID` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 §7.1.2](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.2) | -| **Requirement** | MUST accept | -| **Expected** | `2xx` | - -## What it sends - -A valid chunked POST with a single 5-byte chunk, a zero terminator, and a trailer field (`X-Checksum: abc`) after the final chunk. - -```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 -X-Checksum: abc\r\n -\r\n -``` - -The trailer section appears between the zero-length terminating chunk and the final empty line. - -## What the RFC says - -> "A trailer section allows the sender to include additional fields at the end of a chunked message in order to supply metadata that might be dynamically generated while the content is sent, such as a message integrity check, digital signature, or post-processing status." — RFC 9112 Section 7.1.2 - -> "A recipient that removes the chunked coding from a message MAY selectively retain or discard the received trailer fields." — RFC 9112 Section 7.1.2 - -The chunked encoding grammar explicitly includes an optional trailer section: - -``` -chunked-body = *chunk last-chunk trailer-section CRLF -trailer-section = *( field-line CRLF ) -``` - -Trailer fields are valid metadata that follow the zero-length terminating chunk. A compliant HTTP/1.1 server must be able to parse them as part of the chunked body, even if it chooses to discard them. - -## Why it matters - -Trailer fields are used in practice for checksums, signatures, and streaming metadata that cannot be known until the body has been fully generated. A server that rejects a valid chunked body just because it contains a trailer section has an incomplete chunked encoding parser. This can break interoperability with legitimate clients and proxies that use trailers. - -## Deep Analysis - -### Relevant ABNF grammar - -From RFC 9112 Section 7.1: - -``` -chunked-body = *chunk - last-chunk - trailer-section - CRLF - -last-chunk = 1*("0") [ chunk-ext ] CRLF -trailer-section = *( field-line CRLF ) -``` - -The `trailer-section` is a mandatory part of the `chunked-body` grammar (not optional in brackets), but its content is `*( field-line CRLF )` -- zero or more field lines. So the trailer section is always present syntactically; it just may contain zero fields. - -### Direct RFC quotes - -> "A trailer section allows the sender to include additional fields at the end of a chunked message in order to supply metadata that might be dynamically generated while the content is sent, such as a message integrity check, digital signature, or post-processing status." -- RFC 9112 Section 7.1.2 - -> "A recipient that removes the chunked coding from a message MAY selectively retain or discard the received trailer fields." -- RFC 9112 Section 7.1.2 - -> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." -- RFC 9112 Section 7.1.2 - -### Chain of reasoning - -1. The test sends a valid chunked body with one data chunk (`5\r\nhello\r\n`), the zero terminator (`0\r\n`), one trailer field (`X-Checksum: abc\r\n`), and the final empty line (`\r\n`). -2. Parsing against the ABNF: `*chunk` matches the one data chunk, `last-chunk` matches `0\r\n`, `trailer-section` matches `X-Checksum: abc\r\n` (one `field-line CRLF`), and the final CRLF completes the `chunked-body`. -3. The `trailer-section` is part of the `chunked-body` grammar. A server that implements chunked decoding MUST parse through the trailer section to reach the end of the message. Stopping at the `last-chunk` without consuming the trailer and final CRLF would leave data on the connection. -4. RFC 9112 Section 7.1.2 says recipients "MAY selectively retain or discard the received trailer fields." The MAY applies to what the server *does* with the trailers, not whether it parses them. The server must parse them to complete the chunked body. -5. The decoding pseudocode in RFC 9112 Section 7.1.3 explicitly includes reading trailer fields after the zero-size chunk, confirming that trailer parsing is part of the chunked decoding algorithm. - -### Scored / Unscored justification - -**Scored.** The MUST requirement to "parse and decode the chunked transfer coding" (RFC 9112 Section 7.1) encompasses the entire `chunked-body` grammar, including the `trailer-section`. A server that rejects a message because it has trailer fields has failed to implement the full chunked decoder. The server is free to discard the trailer values (MAY retain or discard), but it MUST parse past them to correctly delimit the message. - -### Edge cases - -- Some servers treat any data after the `0\r\n` terminator as the start of a new request, causing desynchronization when trailers are present. This is a critical bug. -- The `X-Checksum` trailer is an unregistered extension field. Servers should not reject unknown trailer field names -- they should discard or store them per RFC 9112 Section 7.1.2. -- Servers that implement HTTP/2 or HTTP/3 origin handling may have different trailer semantics, but HTTP/1.1 chunked trailers must still be parsed correctly on HTTP/1.1 connections. -- A trailer field whose name matches a header field (e.g., `Content-MD5`) must not be merged into the header section unless that field's definition explicitly allows it (per the MUST NOT merge rule). - -## Sources - -- [RFC 9112 §7.1.2 -- Chunked Trailer Section](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.2) -- [RFC 9110 Section 6.5 -- Trailer Fields](https://www.rfc-editor.org/rfc/rfc9110#section-6.5) +--- +title: "Chunked Trailer Valid — HTTP/1.1 Compliance" +description: "A valid chunked POST with a single 5-byte chunk, a zero terminator, and a trailer field (X-Checksum: abc) after the final chunk. Tested against RFC 9112 §7.1.2." +weight: 11 +--- + +| | | +|---|---| +| **Test ID** | `COMP-CHUNKED-TRAILER-VALID` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 §7.1.2](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.2) | +| **Requirement** | MUST accept | +| **Expected** | `2xx` | + +## What it sends + +A valid chunked POST with a single 5-byte chunk, a zero terminator, and a trailer field (`X-Checksum: abc`) after the final chunk. + +```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 +X-Checksum: abc\r\n +\r\n +``` + +The trailer section appears between the zero-length terminating chunk and the final empty line. + +## What the RFC says + +> "A trailer section allows the sender to include additional fields at the end of a chunked message in order to supply metadata that might be dynamically generated while the content is sent, such as a message integrity check, digital signature, or post-processing status." — RFC 9112 Section 7.1.2 + +> "A recipient that removes the chunked coding from a message MAY selectively retain or discard the received trailer fields." — RFC 9112 Section 7.1.2 + +The chunked encoding grammar explicitly includes an optional trailer section: + +``` +chunked-body = *chunk last-chunk trailer-section CRLF +trailer-section = *( field-line CRLF ) +``` + +Trailer fields are valid metadata that follow the zero-length terminating chunk. A compliant HTTP/1.1 server must be able to parse them as part of the chunked body, even if it chooses to discard them. + +## Why it matters + +Trailer fields are used in practice for checksums, signatures, and streaming metadata that cannot be known until the body has been fully generated. A server that rejects a valid chunked body just because it contains a trailer section has an incomplete chunked encoding parser. This can break interoperability with legitimate clients and proxies that use trailers. + +## Deep Analysis + +### Relevant ABNF grammar + +From RFC 9112 Section 7.1: + +``` +chunked-body = *chunk + last-chunk + trailer-section + CRLF + +last-chunk = 1*("0") [ chunk-ext ] CRLF +trailer-section = *( field-line CRLF ) +``` + +The `trailer-section` is a mandatory part of the `chunked-body` grammar (not optional in brackets), but its content is `*( field-line CRLF )` -- zero or more field lines. So the trailer section is always present syntactically; it just may contain zero fields. + +### Direct RFC quotes + +> "A trailer section allows the sender to include additional fields at the end of a chunked message in order to supply metadata that might be dynamically generated while the content is sent, such as a message integrity check, digital signature, or post-processing status." -- RFC 9112 Section 7.1.2 + +> "A recipient that removes the chunked coding from a message MAY selectively retain or discard the received trailer fields." -- RFC 9112 Section 7.1.2 + +> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." -- RFC 9112 Section 7.1.2 + +### Chain of reasoning + +1. The test sends a valid chunked body with one data chunk (`5\r\nhello\r\n`), the zero terminator (`0\r\n`), one trailer field (`X-Checksum: abc\r\n`), and the final empty line (`\r\n`). +2. Parsing against the ABNF: `*chunk` matches the one data chunk, `last-chunk` matches `0\r\n`, `trailer-section` matches `X-Checksum: abc\r\n` (one `field-line CRLF`), and the final CRLF completes the `chunked-body`. +3. The `trailer-section` is part of the `chunked-body` grammar. A server that implements chunked decoding MUST parse through the trailer section to reach the end of the message. Stopping at the `last-chunk` without consuming the trailer and final CRLF would leave data on the connection. +4. RFC 9112 Section 7.1.2 says recipients "MAY selectively retain or discard the received trailer fields." The MAY applies to what the server *does* with the trailers, not whether it parses them. The server must parse them to complete the chunked body. +5. The decoding pseudocode in RFC 9112 Section 7.1.3 explicitly includes reading trailer fields after the zero-size chunk, confirming that trailer parsing is part of the chunked decoding algorithm. + +### Scored / Unscored justification + +**Scored.** The MUST requirement to "parse and decode the chunked transfer coding" (RFC 9112 Section 7.1) encompasses the entire `chunked-body` grammar, including the `trailer-section`. A server that rejects a message because it has trailer fields has failed to implement the full chunked decoder. The server is free to discard the trailer values (MAY retain or discard), but it MUST parse past them to correctly delimit the message. + +### Edge cases + +- Some servers treat any data after the `0\r\n` terminator as the start of a new request, causing desynchronization when trailers are present. This is a critical bug. +- The `X-Checksum` trailer is an unregistered extension field. Servers should not reject unknown trailer field names -- they should discard or store them per RFC 9112 Section 7.1.2. +- Servers that implement HTTP/2 or HTTP/3 origin handling may have different trailer semantics, but HTTP/1.1 chunked trailers must still be parsed correctly on HTTP/1.1 connections. +- A trailer field whose name matches a header field (e.g., `Content-MD5`) must not be merged into the header section unless that field's definition explicitly allows it (per the MUST NOT merge rule). + +## Sources + +- [RFC 9112 §7.1.2 -- Chunked Trailer Section](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.2) +- [RFC 9110 Section 6.5 -- Trailer Fields](https://www.rfc-editor.org/rfc/rfc9110#section-6.5) diff --git a/docs/content/docs/body/get-with-cl-body.md b/docs/content/docs/body/get-with-cl-body.md index bf26d63..51ceab5 100644 --- a/docs/content/docs/body/get-with-cl-body.md +++ b/docs/content/docs/body/get-with-cl-body.md @@ -1,96 +1,96 @@ ---- -title: "GET-WITH-CL-BODY" -description: "GET-WITH-CL-BODY test documentation" -weight: 5 ---- - -| | | -|---|---| -| **Test ID** | `COMP-GET-WITH-CL-BODY` | -| **Category** | Compliance | -| **RFC** | [RFC 9110 Section 9.3.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.1) | -| **Requirement** | MAY reject | -| **Expected** | `400` = Pass; `2xx` = Warn | - -## What it sends - -A GET request with `Content-Length: 5` and a body (`hello`). - -```http -GET / HTTP/1.1\r\n -Host: localhost\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -## What the RFC says - -> "Although request message framing is independent of method semantics, content received in a GET request has no generally defined semantics, cannot alter the meaning or target of the request, and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack." — RFC 9110 Section 9.3.1 - -> "A client SHOULD NOT generate content in a GET request unless it is made directly to an origin server that has previously indicated, in or out of band, that such a request has a purpose and will be adequately supported." — RFC 9110 Section 9.3.1 - -A body on GET is unusual and has no defined semantics. Rejecting it is stricter and safer. - -## Pass / Warn explanation - -| Response | Verdict | Reasoning | -|---|---|---| -| `400` | Pass | Rejecting a GET body is the safer choice and eliminates a smuggling vector | -| `2xx` | Warn | The RFC permits servers to accept GET bodies, but doing so carries risk | - -## Why this test is unscored - -The RFC uses deliberately permissive language ("has no generally defined semantics", "might lead some implementations to reject"). There is no MUST or SHOULD — the server is free to accept or reject a GET body. Because both behaviors are RFC-compliant, this test is unscored. A `400` is preferred (Pass) because it is the safer posture, while `2xx` earns a Warn to flag the potential smuggling surface. - -## Why it matters - -GET-with-body is a known smuggling vector. If a front-end proxy strips the body but a back-end server reads it, the leftover bytes desynchronize the connection. Rejecting GET bodies at the server level eliminates this attack surface. - -## Deep Analysis - -### Relevant ABNF grammar - -From RFC 9112 Section 6 (message body): - -``` -message-body = *OCTET -``` - -From RFC 9112 Section 6.3 (message body length, rule 6): - -> "If a valid Content-Length header field is present without Transfer-Encoding, its decimal value defines the expected message body length in octets." - -Message body framing is method-independent. The `Content-Length: 5` header defines the body length regardless of the GET method. - -### Direct RFC quotes - -> "Although request message framing is independent of method semantics, content received in a GET request has no generally defined semantics, cannot alter the meaning or target of the request, and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack." -- RFC 9110 Section 9.3.1 - -> "A client SHOULD NOT generate content in a GET request unless it is made directly to an origin server that has previously indicated, in or out of band, that such a request has a purpose and will be adequately supported." -- RFC 9110 Section 9.3.1 - -> "If a valid Content-Length header field is present without Transfer-Encoding, its decimal value defines the expected message body length in octets." -- RFC 9112 Section 6.3 - -### Chain of reasoning - -1. The test sends a GET request with `Content-Length: 5` and a 5-byte body (`hello`). -2. Per RFC 9112 Section 6.3 rule 6, the server must read exactly 5 bytes of body data because `Content-Length: 5` is present without `Transfer-Encoding`. This framing requirement applies regardless of the HTTP method. -3. RFC 9110 Section 9.3.1 states that content in a GET request "has no generally defined semantics" and "cannot alter the meaning or target of the request." This means the body is meaningless for GET semantics. -4. The RFC further notes that this "might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack." The word "might" is informational, not normative. -5. The SHOULD NOT applies to the *client* ("A client SHOULD NOT generate content in a GET request"), not to the server. There is no corresponding MUST or SHOULD for the server to reject it. -6. Therefore, the server MAY accept the body (reading and ignoring it) or MAY reject it with `400`. Both are compliant. - -### Scored / Unscored justification - -**Unscored.** The RFC uses deliberately non-normative language for the server side: "has no generally defined semantics", "might lead some implementations to reject." There is no MUST or SHOULD directed at the server regarding GET bodies. The SHOULD NOT is directed at clients, not servers. Because both accepting and rejecting are compliant behaviors, the test cannot be scored. However, `400` is preferred (Pass) as the safer security posture because GET-with-body is a well-known smuggling vector, while `2xx` earns a Warn to flag the risk. - -### Edge cases - -- **Smuggling vector**: If a front-end proxy ignores the GET body but the back-end reads it, the extra bytes desynchronize the connection. This is the primary security concern motivating the `400`-preferred posture. -- Some popular servers (nginx, Apache) accept GET bodies by default. Others (certain WAFs) reject them. Both are compliant. -- Elasticsearch historically used GET-with-body for search queries (`GET /_search` with a JSON body), which is the most well-known legitimate use case. RFC 9110 acknowledges this by saying clients SHOULD NOT *unless* the server has indicated support. -- If the server accepts the GET body, it MUST still read exactly 5 bytes per the Content-Length framing. Failing to consume the body would desynchronize the connection on keep-alive. - -## Sources - -- [RFC 9110 Section 9.3.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.1) +--- +title: "GET With CL Body — HTTP/1.1 Compliance" +description: "A GET request with Content-Length: 5 and a body (hello). Tested against RFC 9110 Section 9.3.1." +weight: 5 +--- + +| | | +|---|---| +| **Test ID** | `COMP-GET-WITH-CL-BODY` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 Section 9.3.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.1) | +| **Requirement** | MAY reject | +| **Expected** | `400` = Pass; `2xx` = Warn | + +## What it sends + +A GET request with `Content-Length: 5` and a body (`hello`). + +```http +GET / HTTP/1.1\r\n +Host: localhost\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +## What the RFC says + +> "Although request message framing is independent of method semantics, content received in a GET request has no generally defined semantics, cannot alter the meaning or target of the request, and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack." — RFC 9110 Section 9.3.1 + +> "A client SHOULD NOT generate content in a GET request unless it is made directly to an origin server that has previously indicated, in or out of band, that such a request has a purpose and will be adequately supported." — RFC 9110 Section 9.3.1 + +A body on GET is unusual and has no defined semantics. Rejecting it is stricter and safer. + +## Pass / Warn explanation + +| Response | Verdict | Reasoning | +|---|---|---| +| `400` | Pass | Rejecting a GET body is the safer choice and eliminates a smuggling vector | +| `2xx` | Warn | The RFC permits servers to accept GET bodies, but doing so carries risk | + +## Why this test is unscored + +The RFC uses deliberately permissive language ("has no generally defined semantics", "might lead some implementations to reject"). There is no MUST or SHOULD — the server is free to accept or reject a GET body. Because both behaviors are RFC-compliant, this test is unscored. A `400` is preferred (Pass) because it is the safer posture, while `2xx` earns a Warn to flag the potential smuggling surface. + +## Why it matters + +GET-with-body is a known smuggling vector. If a front-end proxy strips the body but a back-end server reads it, the leftover bytes desynchronize the connection. Rejecting GET bodies at the server level eliminates this attack surface. + +## Deep Analysis + +### Relevant ABNF grammar + +From RFC 9112 Section 6 (message body): + +``` +message-body = *OCTET +``` + +From RFC 9112 Section 6.3 (message body length, rule 6): + +> "If a valid Content-Length header field is present without Transfer-Encoding, its decimal value defines the expected message body length in octets." + +Message body framing is method-independent. The `Content-Length: 5` header defines the body length regardless of the GET method. + +### Direct RFC quotes + +> "Although request message framing is independent of method semantics, content received in a GET request has no generally defined semantics, cannot alter the meaning or target of the request, and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack." -- RFC 9110 Section 9.3.1 + +> "A client SHOULD NOT generate content in a GET request unless it is made directly to an origin server that has previously indicated, in or out of band, that such a request has a purpose and will be adequately supported." -- RFC 9110 Section 9.3.1 + +> "If a valid Content-Length header field is present without Transfer-Encoding, its decimal value defines the expected message body length in octets." -- RFC 9112 Section 6.3 + +### Chain of reasoning + +1. The test sends a GET request with `Content-Length: 5` and a 5-byte body (`hello`). +2. Per RFC 9112 Section 6.3 rule 6, the server must read exactly 5 bytes of body data because `Content-Length: 5` is present without `Transfer-Encoding`. This framing requirement applies regardless of the HTTP method. +3. RFC 9110 Section 9.3.1 states that content in a GET request "has no generally defined semantics" and "cannot alter the meaning or target of the request." This means the body is meaningless for GET semantics. +4. The RFC further notes that this "might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack." The word "might" is informational, not normative. +5. The SHOULD NOT applies to the *client* ("A client SHOULD NOT generate content in a GET request"), not to the server. There is no corresponding MUST or SHOULD for the server to reject it. +6. Therefore, the server MAY accept the body (reading and ignoring it) or MAY reject it with `400`. Both are compliant. + +### Scored / Unscored justification + +**Unscored.** The RFC uses deliberately non-normative language for the server side: "has no generally defined semantics", "might lead some implementations to reject." There is no MUST or SHOULD directed at the server regarding GET bodies. The SHOULD NOT is directed at clients, not servers. Because both accepting and rejecting are compliant behaviors, the test cannot be scored. However, `400` is preferred (Pass) as the safer security posture because GET-with-body is a well-known smuggling vector, while `2xx` earns a Warn to flag the risk. + +### Edge cases + +- **Smuggling vector**: If a front-end proxy ignores the GET body but the back-end reads it, the extra bytes desynchronize the connection. This is the primary security concern motivating the `400`-preferred posture. +- Some popular servers (nginx, Apache) accept GET bodies by default. Others (certain WAFs) reject them. Both are compliant. +- Elasticsearch historically used GET-with-body for search queries (`GET /_search` with a JSON body), which is the most well-known legitimate use case. RFC 9110 acknowledges this by saying clients SHOULD NOT *unless* the server has indicated support. +- If the server accepts the GET body, it MUST still read exactly 5 bytes per the Content-Length framing. Failing to consume the body would desynchronize the connection on keep-alive. + +## Sources + +- [RFC 9110 Section 9.3.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.1) diff --git a/docs/content/docs/body/post-cl-body.md b/docs/content/docs/body/post-cl-body.md index e4c9f73..fa91dd6 100644 --- a/docs/content/docs/body/post-cl-body.md +++ b/docs/content/docs/body/post-cl-body.md @@ -1,85 +1,85 @@ ---- -title: "POST-CL-BODY" -description: "POST-CL-BODY test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `COMP-POST-CL-BODY` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) | -| **Requirement** | MUST accept | -| **Expected** | `2xx` | - -## What it sends - -A valid POST with `Content-Length: 5` and exactly 5 bytes of body (`hello`). - -```http -POST / HTTP/1.1\r\n -Host: localhost\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -## What the RFC says - -> "When a message does not have a Transfer-Encoding header field, a Content-Length header field can provide the anticipated size, as a decimal number of octets, for potential content. For messages that do include content, the Content-Length field value provides the framing information necessary for determining where the data (and message) ends." — RFC 9112 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." — RFC 9112 Section 6.3 - -The server must read exactly 5 bytes from the connection after the header section ends, then process the request normally. - -## Why it matters - -This is the most basic body consumption test. If a server cannot read a fixed-length POST body, it cannot handle form submissions, API calls, or file uploads — the foundation of any interactive web application. - -## Deep Analysis - -### Relevant ABNF grammar - -From RFC 9112 Section 6: - -``` -message-body = *OCTET -``` - -From RFC 9110 Section 8.6 (Content-Length): - -``` -Content-Length = 1*DIGIT -``` - -### Direct RFC quotes - -> "When a message does not have a Transfer-Encoding header field, a Content-Length header field can provide the anticipated size, as a decimal number of octets, for potential content. For messages that do include content, the Content-Length field value provides the framing information necessary for determining where the data (and message) ends." -- RFC 9112 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." -- RFC 9112 Section 6.3 - -> "A sender MUST NOT send a Content-Length header field in any message that contains a Transfer-Encoding header field." -- RFC 9112 Section 6.2 - -### Chain of reasoning - -1. The test sends a POST request with `Content-Length: 5` and no `Transfer-Encoding` header. -2. Per RFC 9112 Section 6.3 rule 6, when a valid Content-Length is present without Transfer-Encoding, "its decimal value defines the expected message body length in octets." The server must read exactly 5 bytes. -3. The test sends exactly 5 bytes (`hello`) after the header section. This satisfies the declared Content-Length precisely. -4. POST is a method that inherently expects content (RFC 9110 Section 9.3.3). There is no ambiguity about whether a body is appropriate. -5. The Content-Length value `5` is a valid `1*DIGIT` production, the body length matches the declared value, and there is no Transfer-Encoding conflict. The request is fully well-formed. -6. The server has no grounds to reject this request. Accepting a valid Content-Length-framed POST body is the most fundamental requirement for an HTTP/1.1 server. - -### Scored / Unscored justification - -**Scored.** RFC 9112 Section 6.3 establishes that Content-Length "defines the expected message body length" as a definitive framing mechanism. While the word MUST does not appear in the specific framing sentence, the entire message body length determination algorithm in Section 6.3 is normative, and a server that cannot read a Content-Length-framed body cannot function as an HTTP/1.1 server. This is the baseline body consumption test -- if this fails, the server is fundamentally broken. - -### Edge cases - -- Some servers impose upper limits on Content-Length and reject bodies exceeding a configured maximum with `413 Content Too Large`. This test uses only 5 bytes, well below any reasonable limit. -- A server that reads fewer than 5 bytes (e.g., reads 0 bytes and ignores the body) may appear to "work" but will desynchronize the connection on keep-alive because the unread body bytes will be interpreted as the next request. -- Servers that only accept GET (static file servers) may return `405 Method Not Allowed` for POST. This would be a legitimate response but indicates the server does not support POST at all, which is a separate concern from body parsing. -- The absence of a `Content-Type` header is deliberate -- the server must still read the body based on Content-Length framing regardless of whether the content type is specified. - -## Sources - -- [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) +--- +title: "POST CL Body — HTTP/1.1 Compliance" +description: "A valid POST with Content-Length: 5 and exactly 5 bytes of body (hello). Tested against RFC 9112 Section 6.2." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `COMP-POST-CL-BODY` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) | +| **Requirement** | MUST accept | +| **Expected** | `2xx` | + +## What it sends + +A valid POST with `Content-Length: 5` and exactly 5 bytes of body (`hello`). + +```http +POST / HTTP/1.1\r\n +Host: localhost\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +## What the RFC says + +> "When a message does not have a Transfer-Encoding header field, a Content-Length header field can provide the anticipated size, as a decimal number of octets, for potential content. For messages that do include content, the Content-Length field value provides the framing information necessary for determining where the data (and message) ends." — RFC 9112 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." — RFC 9112 Section 6.3 + +The server must read exactly 5 bytes from the connection after the header section ends, then process the request normally. + +## Why it matters + +This is the most basic body consumption test. If a server cannot read a fixed-length POST body, it cannot handle form submissions, API calls, or file uploads — the foundation of any interactive web application. + +## Deep Analysis + +### Relevant ABNF grammar + +From RFC 9112 Section 6: + +``` +message-body = *OCTET +``` + +From RFC 9110 Section 8.6 (Content-Length): + +``` +Content-Length = 1*DIGIT +``` + +### Direct RFC quotes + +> "When a message does not have a Transfer-Encoding header field, a Content-Length header field can provide the anticipated size, as a decimal number of octets, for potential content. For messages that do include content, the Content-Length field value provides the framing information necessary for determining where the data (and message) ends." -- RFC 9112 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." -- RFC 9112 Section 6.3 + +> "A sender MUST NOT send a Content-Length header field in any message that contains a Transfer-Encoding header field." -- RFC 9112 Section 6.2 + +### Chain of reasoning + +1. The test sends a POST request with `Content-Length: 5` and no `Transfer-Encoding` header. +2. Per RFC 9112 Section 6.3 rule 6, when a valid Content-Length is present without Transfer-Encoding, "its decimal value defines the expected message body length in octets." The server must read exactly 5 bytes. +3. The test sends exactly 5 bytes (`hello`) after the header section. This satisfies the declared Content-Length precisely. +4. POST is a method that inherently expects content (RFC 9110 Section 9.3.3). There is no ambiguity about whether a body is appropriate. +5. The Content-Length value `5` is a valid `1*DIGIT` production, the body length matches the declared value, and there is no Transfer-Encoding conflict. The request is fully well-formed. +6. The server has no grounds to reject this request. Accepting a valid Content-Length-framed POST body is the most fundamental requirement for an HTTP/1.1 server. + +### Scored / Unscored justification + +**Scored.** RFC 9112 Section 6.3 establishes that Content-Length "defines the expected message body length" as a definitive framing mechanism. While the word MUST does not appear in the specific framing sentence, the entire message body length determination algorithm in Section 6.3 is normative, and a server that cannot read a Content-Length-framed body cannot function as an HTTP/1.1 server. This is the baseline body consumption test -- if this fails, the server is fundamentally broken. + +### Edge cases + +- Some servers impose upper limits on Content-Length and reject bodies exceeding a configured maximum with `413 Content Too Large`. This test uses only 5 bytes, well below any reasonable limit. +- A server that reads fewer than 5 bytes (e.g., reads 0 bytes and ignores the body) may appear to "work" but will desynchronize the connection on keep-alive because the unread body bytes will be interpreted as the next request. +- Servers that only accept GET (static file servers) may return `405 Method Not Allowed` for POST. This would be a legitimate response but indicates the server does not support POST at all, which is a separate concern from body parsing. +- The absence of a `Content-Type` header is deliberate -- the server must still read the body based on Content-Length framing regardless of whether the content type is specified. + +## Sources + +- [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) diff --git a/docs/content/docs/body/post-cl-undersend.md b/docs/content/docs/body/post-cl-undersend.md index 3d56ee9..55e30fc 100644 --- a/docs/content/docs/body/post-cl-undersend.md +++ b/docs/content/docs/body/post-cl-undersend.md @@ -1,88 +1,88 @@ ---- -title: "POST-CL-UNDERSEND" -description: "POST-CL-UNDERSEND test documentation" -weight: 4 ---- - -| | | -|---|---| -| **Test ID** | `COMP-POST-CL-UNDERSEND` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) | -| **Requirement** | MUST read declared length | -| **Expected** | `400`, close, or timeout | - -## What it sends - -A POST declaring `Content-Length: 10` but sending only 5 bytes (`hello`). The connection then goes silent. - -```http -POST / HTTP/1.1\r\n -Host: localhost\r\n -Content-Length: 10\r\n -\r\n -hello -``` - -## What the RFC says - -> "If a valid Content-Length header field is present without Transfer-Encoding, its decimal value defines the expected message body length in octets." — RFC 9112 Section 6.3 - -> "For messages that do include content, the Content-Length field value provides the framing information necessary for determining where the data (and message) ends." — RFC 9112 Section 6.2 - -The server is obligated to read exactly 10 bytes as declared. Since only 5 arrive, the server must continue waiting for the remaining 5 bytes until its read timeout fires. Responding prematurely would leave leftover bytes on the connection. - -## Why it matters - -A server that responds before reading the full declared body risks desynchronizing the connection — leftover bytes from the incomplete body could be interpreted as the start of the next request, creating a smuggling vector. - -## Deep Analysis - -### Relevant ABNF grammar - -From RFC 9110 Section 8.6 (Content-Length): - -``` -Content-Length = 1*DIGIT -``` - -From RFC 9112 Section 6: - -``` -message-body = *OCTET -``` - -The message body length is determined by the Content-Length value (10 octets), but only 5 octets arrive before the connection stalls. - -### Direct RFC quotes - -> "If a valid Content-Length header field is present without Transfer-Encoding, its decimal value defines the expected message body length in octets." -- RFC 9112 Section 6.3 - -> "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." -- RFC 9112 Section 6.3 - -> "For messages that do include content, the Content-Length field value provides the framing information necessary for determining where the data (and message) ends." -- RFC 9112 Section 6.2 - -### Chain of reasoning - -1. The test sends `Content-Length: 10` but only 5 bytes of body data (`hello`), then the connection goes silent. -2. Per RFC 9112 Section 6.3 rule 6, the server must read exactly 10 bytes because the Content-Length "defines the expected message body length in octets." -3. After reading 5 bytes, the server has received only half the declared body. It must continue reading, waiting for the remaining 5 bytes. -4. No more data arrives. The server's read operation blocks. -5. RFC 9112 Section 6.3 explicitly addresses this scenario: "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." -6. The MUST keyword applies directly: the server MUST treat this as an incomplete message. A `2xx` response would violate this requirement because it implies the server processed the request successfully despite having an incomplete body. -7. Acceptable outcomes are: `400` (explicit rejection), connection close (per the MUST close requirement), or timeout (the server waited for the full 10 bytes, which never arrived). - -### Scored / Unscored justification - -**Scored.** RFC 9112 Section 6.3 uses MUST: "the recipient MUST consider the message to be incomplete and close the connection." This is a direct, unambiguous requirement. A server that responds `2xx` after reading only 5 of the declared 10 bytes has violated this MUST by treating an incomplete message as complete. The three acceptable outcomes (`400`, close, timeout) all represent correct handling of the incomplete message. - -### Edge cases - -- **Smuggling risk**: If the server responds `2xx` after reading only 5 bytes, the remaining 5 bytes of body data (when eventually sent or from a subsequent request) could be interpreted as a new HTTP request, enabling request smuggling. -- Some servers read the full Content-Length worth of data from the socket before processing, and thus correctly block when only 5 bytes are available. Others process in a streaming fashion and may prematurely respond. -- A server that sends `408 Request Timeout` instead of `400` is also acceptable -- it correctly identifies the incomplete body as a timeout condition. -- The connection closure requirement means keep-alive should not be used after an incomplete message. The server must not attempt to read a subsequent request on this connection. - -## Sources - -- [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) +--- +title: "POST CL Undersend — HTTP/1.1 Compliance" +description: "A POST declaring Content-Length: 10 but sending only 5 bytes (hello). The connection then goes silent. Tested against RFC 9112 Section 6.2." +weight: 4 +--- + +| | | +|---|---| +| **Test ID** | `COMP-POST-CL-UNDERSEND` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) | +| **Requirement** | MUST read declared length | +| **Expected** | `400`, close, or timeout | + +## What it sends + +A POST declaring `Content-Length: 10` but sending only 5 bytes (`hello`). The connection then goes silent. + +```http +POST / HTTP/1.1\r\n +Host: localhost\r\n +Content-Length: 10\r\n +\r\n +hello +``` + +## What the RFC says + +> "If a valid Content-Length header field is present without Transfer-Encoding, its decimal value defines the expected message body length in octets." — RFC 9112 Section 6.3 + +> "For messages that do include content, the Content-Length field value provides the framing information necessary for determining where the data (and message) ends." — RFC 9112 Section 6.2 + +The server is obligated to read exactly 10 bytes as declared. Since only 5 arrive, the server must continue waiting for the remaining 5 bytes until its read timeout fires. Responding prematurely would leave leftover bytes on the connection. + +## Why it matters + +A server that responds before reading the full declared body risks desynchronizing the connection — leftover bytes from the incomplete body could be interpreted as the start of the next request, creating a smuggling vector. + +## Deep Analysis + +### Relevant ABNF grammar + +From RFC 9110 Section 8.6 (Content-Length): + +``` +Content-Length = 1*DIGIT +``` + +From RFC 9112 Section 6: + +``` +message-body = *OCTET +``` + +The message body length is determined by the Content-Length value (10 octets), but only 5 octets arrive before the connection stalls. + +### Direct RFC quotes + +> "If a valid Content-Length header field is present without Transfer-Encoding, its decimal value defines the expected message body length in octets." -- RFC 9112 Section 6.3 + +> "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." -- RFC 9112 Section 6.3 + +> "For messages that do include content, the Content-Length field value provides the framing information necessary for determining where the data (and message) ends." -- RFC 9112 Section 6.2 + +### Chain of reasoning + +1. The test sends `Content-Length: 10` but only 5 bytes of body data (`hello`), then the connection goes silent. +2. Per RFC 9112 Section 6.3 rule 6, the server must read exactly 10 bytes because the Content-Length "defines the expected message body length in octets." +3. After reading 5 bytes, the server has received only half the declared body. It must continue reading, waiting for the remaining 5 bytes. +4. No more data arrives. The server's read operation blocks. +5. RFC 9112 Section 6.3 explicitly addresses this scenario: "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." +6. The MUST keyword applies directly: the server MUST treat this as an incomplete message. A `2xx` response would violate this requirement because it implies the server processed the request successfully despite having an incomplete body. +7. Acceptable outcomes are: `400` (explicit rejection), connection close (per the MUST close requirement), or timeout (the server waited for the full 10 bytes, which never arrived). + +### Scored / Unscored justification + +**Scored.** RFC 9112 Section 6.3 uses MUST: "the recipient MUST consider the message to be incomplete and close the connection." This is a direct, unambiguous requirement. A server that responds `2xx` after reading only 5 of the declared 10 bytes has violated this MUST by treating an incomplete message as complete. The three acceptable outcomes (`400`, close, timeout) all represent correct handling of the incomplete message. + +### Edge cases + +- **Smuggling risk**: If the server responds `2xx` after reading only 5 bytes, the remaining 5 bytes of body data (when eventually sent or from a subsequent request) could be interpreted as a new HTTP request, enabling request smuggling. +- Some servers read the full Content-Length worth of data from the socket before processing, and thus correctly block when only 5 bytes are available. Others process in a streaming fashion and may prematurely respond. +- A server that sends `408 Request Timeout` instead of `400` is also acceptable -- it correctly identifies the incomplete body as a timeout condition. +- The connection closure requirement means keep-alive should not be used after an incomplete message. The server must not attempt to read a subsequent request on this connection. + +## Sources + +- [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) diff --git a/docs/content/docs/body/post-cl-zero.md b/docs/content/docs/body/post-cl-zero.md index 2bea9ec..50f2e24 100644 --- a/docs/content/docs/body/post-cl-zero.md +++ b/docs/content/docs/body/post-cl-zero.md @@ -1,88 +1,88 @@ ---- -title: "POST-CL-ZERO" -description: "POST-CL-ZERO test documentation" -weight: 2 ---- - -| | | -|---|---| -| **Test ID** | `COMP-POST-CL-ZERO` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) | -| **Requirement** | MUST accept | -| **Expected** | `2xx` or close | - -## What it sends - -A POST with `Content-Length: 0` and no body bytes after the headers. - -```http -POST / HTTP/1.1\r\n -Host: localhost\r\n -Content-Length: 0\r\n -\r\n -``` - -## What the RFC says - -> "When a message does not have a Transfer-Encoding header field, a Content-Length header field can provide the anticipated size, as a decimal number of octets, for potential content." — RFC 9112 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." — RFC 9112 Section 6.3 - -A Content-Length of zero is explicitly valid — its decimal value (0) defines the body length as zero octets. The server must not block waiting for body bytes that will never arrive. - -## Why it matters - -Zero-length POSTs are common in APIs (e.g., triggering an action with no payload). A server that hangs waiting for a body on CL:0 will cause client timeouts and connection leaks. - -## Deep Analysis - -### Relevant ABNF grammar - -From RFC 9110 Section 8.6 (Content-Length): - -``` -Content-Length = 1*DIGIT -``` - -The value `0` is a valid `1*DIGIT` production (one digit, "0"). It defines a message body length of zero octets. - -From RFC 9112 Section 6: - -``` -message-body = *OCTET -``` - -A `*OCTET` production with zero repetitions (empty string) is valid. A zero-length message body is a valid instance of `*OCTET`. - -### Direct RFC quotes - -> "When a message does not have a Transfer-Encoding header field, a Content-Length header field can provide the anticipated size, as a decimal number of octets, for potential content." -- RFC 9112 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." -- RFC 9112 Section 6.3 - -> "For messages that do include content, the Content-Length field value provides the framing information necessary for determining where the data (and message) ends." -- RFC 9112 Section 6.2 - -### Chain of reasoning - -1. The test sends `Content-Length: 0` with no body bytes after the `\r\n\r\n` header terminator. -2. The Content-Length value `0` is a valid `1*DIGIT` production -- the digit "0" satisfies the one-or-more requirement. -3. Per RFC 9112 Section 6.3 rule 6, the server must read exactly 0 bytes of body data. This means the message ends immediately after the header section. -4. The server must not block waiting for body data. The Content-Length has explicitly declared that zero octets follow. -5. POST with a zero-length body is semantically valid. RFC 9110 Section 9.3.3 does not require POST to have a non-empty body. -6. The request is syntactically complete and well-formed. The server must process it and respond. - -### Scored / Unscored justification - -**Scored.** RFC 9112 Section 6.3 establishes that Content-Length "defines the expected message body length in octets." A value of 0 unambiguously means zero bytes. The server must process this as a complete request with an empty body. There is no ambiguity or discretionary latitude. The `AllowConnectionClose` flag is set because a server may legitimately close the connection after processing a POST (e.g., if it does not support keep-alive for POST), but it must still process the request. - -### Edge cases - -- A server that hangs waiting for body data after seeing `Content-Length: 0` has a framing bug -- it is ignoring the declared body length. -- Some servers treat `Content-Length: 0` differently from "no Content-Length" on POST. Both should result in a zero-length body (per Section 6.3 rules 6 and 7), but the presence of the header is more explicit. -- `Content-Length: 0` is commonly used in API calls that trigger actions without payload (e.g., `POST /api/restart` with no body). Servers must handle this pattern. -- A server that returns `411 Length Required` when Content-Length is already present and valid has a logic error in its header checking. - -## Sources - -- [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) +--- +title: "POST CL Zero — HTTP/1.1 Compliance" +description: "A POST with Content-Length: 0 and no body bytes after the headers. Tested against RFC 9112 Section 6.2." +weight: 2 +--- + +| | | +|---|---| +| **Test ID** | `COMP-POST-CL-ZERO` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) | +| **Requirement** | MUST accept | +| **Expected** | `2xx` or close | + +## What it sends + +A POST with `Content-Length: 0` and no body bytes after the headers. + +```http +POST / HTTP/1.1\r\n +Host: localhost\r\n +Content-Length: 0\r\n +\r\n +``` + +## What the RFC says + +> "When a message does not have a Transfer-Encoding header field, a Content-Length header field can provide the anticipated size, as a decimal number of octets, for potential content." — RFC 9112 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." — RFC 9112 Section 6.3 + +A Content-Length of zero is explicitly valid — its decimal value (0) defines the body length as zero octets. The server must not block waiting for body bytes that will never arrive. + +## Why it matters + +Zero-length POSTs are common in APIs (e.g., triggering an action with no payload). A server that hangs waiting for a body on CL:0 will cause client timeouts and connection leaks. + +## Deep Analysis + +### Relevant ABNF grammar + +From RFC 9110 Section 8.6 (Content-Length): + +``` +Content-Length = 1*DIGIT +``` + +The value `0` is a valid `1*DIGIT` production (one digit, "0"). It defines a message body length of zero octets. + +From RFC 9112 Section 6: + +``` +message-body = *OCTET +``` + +A `*OCTET` production with zero repetitions (empty string) is valid. A zero-length message body is a valid instance of `*OCTET`. + +### Direct RFC quotes + +> "When a message does not have a Transfer-Encoding header field, a Content-Length header field can provide the anticipated size, as a decimal number of octets, for potential content." -- RFC 9112 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." -- RFC 9112 Section 6.3 + +> "For messages that do include content, the Content-Length field value provides the framing information necessary for determining where the data (and message) ends." -- RFC 9112 Section 6.2 + +### Chain of reasoning + +1. The test sends `Content-Length: 0` with no body bytes after the `\r\n\r\n` header terminator. +2. The Content-Length value `0` is a valid `1*DIGIT` production -- the digit "0" satisfies the one-or-more requirement. +3. Per RFC 9112 Section 6.3 rule 6, the server must read exactly 0 bytes of body data. This means the message ends immediately after the header section. +4. The server must not block waiting for body data. The Content-Length has explicitly declared that zero octets follow. +5. POST with a zero-length body is semantically valid. RFC 9110 Section 9.3.3 does not require POST to have a non-empty body. +6. The request is syntactically complete and well-formed. The server must process it and respond. + +### Scored / Unscored justification + +**Scored.** RFC 9112 Section 6.3 establishes that Content-Length "defines the expected message body length in octets." A value of 0 unambiguously means zero bytes. The server must process this as a complete request with an empty body. There is no ambiguity or discretionary latitude. The `AllowConnectionClose` flag is set because a server may legitimately close the connection after processing a POST (e.g., if it does not support keep-alive for POST), but it must still process the request. + +### Edge cases + +- A server that hangs waiting for body data after seeing `Content-Length: 0` has a framing bug -- it is ignoring the declared body length. +- Some servers treat `Content-Length: 0` differently from "no Content-Length" on POST. Both should result in a zero-length body (per Section 6.3 rules 6 and 7), but the presence of the header is more explicit. +- `Content-Length: 0` is commonly used in API calls that trigger actions without payload (e.g., `POST /api/restart` with no body). Servers must handle this pattern. +- A server that returns `411 Length Required` when Content-Length is already present and valid has a logic error in its header checking. + +## Sources + +- [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) diff --git a/docs/content/docs/body/post-no-cl-no-te.md b/docs/content/docs/body/post-no-cl-no-te.md index 4c25b6e..1be6729 100644 --- a/docs/content/docs/body/post-no-cl-no-te.md +++ b/docs/content/docs/body/post-no-cl-no-te.md @@ -1,95 +1,95 @@ ---- -title: "POST-NO-CL-NO-TE" -description: "POST-NO-CL-NO-TE test documentation" -weight: 3 ---- - -| | | -|---|---| -| **Test ID** | `COMP-POST-NO-CL-NO-TE` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) | -| **Requirement** | MUST treat as zero-length | -| **Expected** | `2xx` or close | - -## What it sends - -A POST with neither Content-Length nor Transfer-Encoding headers — no body framing at all. - -```http -POST / HTTP/1.1\r\n -Host: localhost\r\n -\r\n -``` - -## What the RFC says - -RFC 9112 Section 6.3 defines a precedence list for determining message body length. After checking for Transfer-Encoding (rule 3/4) and Content-Length (rule 5/6), the final rule for requests states: - -> "If this is a request message and none of the above are true, then the message body length is zero (no message body is present)." — RFC 9112 Section 6.3 - -The RFC also notes that a server may choose to require Content-Length on requests that carry a body: - -> "A server MAY reject a request that contains a message body but not a Content-Length by responding with 411 (Length Required)." — RFC 9112 Section 6.3 - -When a request has no framing headers, the server must assume the body is empty and process the request immediately. - -## Why it matters - -Some servers hang waiting for a body when they see POST without Content-Length, causing connection timeouts. The RFC is clear: no framing headers means zero-length body. - -## Deep Analysis - -### Relevant ABNF grammar - -From RFC 9112 Section 6: - -``` -message-body = *OCTET -``` - -There is no Content-Length or Transfer-Encoding header, so the message body length determination falls through to rule 7 of RFC 9112 Section 6.3. - -### RFC 9112 Section 6.3 -- Message Body Length precedence rules - -The complete precedence list for determining message body length: - -1. Responses to HEAD, and 1xx/204/304 responses: no body. -2. 2xx responses to CONNECT: tunnel mode. -3. Transfer-Encoding present and overrides Content-Length. -4. Chunked as final coding: read chunked data. Non-chunked final coding on request: respond 400. -5. Invalid Content-Length without Transfer-Encoding: unrecoverable error. -6. Valid Content-Length without Transfer-Encoding: read that many octets. -7. **"If this is a request message and none of the above are true, then the message body length is zero (no message body is present)."** - -### Direct RFC quotes - -> "If this is a request message and none of the above are true, then the message body length is zero (no message body is present)." -- RFC 9112 Section 6.3 - -> "A server MAY reject a request that contains a message body but not a Content-Length by responding with 411 (Length Required)." -- RFC 9112 Section 6.3 - -> "A user agent that sends a request that contains a message body MUST send either a valid Content-Length header field or use the chunked transfer coding." -- RFC 9112 Section 6.3 - -### Chain of reasoning - -1. The test sends a POST request with no Content-Length and no Transfer-Encoding headers. -2. Walking through the RFC 9112 Section 6.3 precedence rules: rules 1-2 do not apply (this is a request, not a response). Rule 3/4: no Transfer-Encoding is present. Rule 5/6: no Content-Length is present. -3. We reach rule 7: "If this is a request message and none of the above are true, then the message body length is zero (no message body is present)." This is definitive. -4. The server MUST treat the body length as zero. It must not block waiting for body data that will never arrive. -5. The MAY reject with 411 clause applies when a request "contains a message body but not a Content-Length." In this case, rule 7 has already determined the body length is zero -- there *is* no message body. Therefore the 411 clause does not apply to this test case. -6. The server should process the request as a POST with an empty body and respond normally. - -### Scored / Unscored justification - -**Scored.** Rule 7 of RFC 9112 Section 6.3 definitively states the body length is zero when no framing headers are present. This is not a SHOULD or MAY -- it is a declarative statement of fact within the normative precedence algorithm. A server that hangs waiting for body data is violating the body-length determination algorithm. The `AllowConnectionClose` flag is set because a server may close the connection for its own reasons, but it must not stall indefinitely. - -### Edge cases - -- Some servers interpret POST without Content-Length as malformed and return `411 Length Required`. This is only correct if the server believes a body *was intended* but not framed. Since rule 7 says "no message body is present," returning 411 is technically incorrect for this scenario, but the test accepts close as an alternative. -- A server that blocks waiting for a body on this request has confused "POST method" with "POST must have a body." POST does not require a body. -- The third RFC quote ("A user agent that sends a request that contains a message body MUST send either a valid Content-Length header field or use the chunked transfer coding") places the MUST on the *client*. This test verifies the server correctly handles a client that has no body to send and therefore omits both headers. -- Some frameworks (e.g., Express.js, Flask) automatically set `Content-Length: 0` on empty POST bodies, so servers may rarely encounter this in practice. However, raw TCP clients and proxies can produce this pattern. - -## Sources - -- [RFC 9112 Section 6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "POST No CL No TE — HTTP/1.1 Compliance" +description: "A POST with neither Content-Length nor Transfer-Encoding headers — no body framing at all. Tested against RFC 9112 Section 6.3." +weight: 3 +--- + +| | | +|---|---| +| **Test ID** | `COMP-POST-NO-CL-NO-TE` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) | +| **Requirement** | MUST treat as zero-length | +| **Expected** | `2xx` or close | + +## What it sends + +A POST with neither Content-Length nor Transfer-Encoding headers — no body framing at all. + +```http +POST / HTTP/1.1\r\n +Host: localhost\r\n +\r\n +``` + +## What the RFC says + +RFC 9112 Section 6.3 defines a precedence list for determining message body length. After checking for Transfer-Encoding (rule 3/4) and Content-Length (rule 5/6), the final rule for requests states: + +> "If this is a request message and none of the above are true, then the message body length is zero (no message body is present)." — RFC 9112 Section 6.3 + +The RFC also notes that a server may choose to require Content-Length on requests that carry a body: + +> "A server MAY reject a request that contains a message body but not a Content-Length by responding with 411 (Length Required)." — RFC 9112 Section 6.3 + +When a request has no framing headers, the server must assume the body is empty and process the request immediately. + +## Why it matters + +Some servers hang waiting for a body when they see POST without Content-Length, causing connection timeouts. The RFC is clear: no framing headers means zero-length body. + +## Deep Analysis + +### Relevant ABNF grammar + +From RFC 9112 Section 6: + +``` +message-body = *OCTET +``` + +There is no Content-Length or Transfer-Encoding header, so the message body length determination falls through to rule 7 of RFC 9112 Section 6.3. + +### RFC 9112 Section 6.3 -- Message Body Length precedence rules + +The complete precedence list for determining message body length: + +1. Responses to HEAD, and 1xx/204/304 responses: no body. +2. 2xx responses to CONNECT: tunnel mode. +3. Transfer-Encoding present and overrides Content-Length. +4. Chunked as final coding: read chunked data. Non-chunked final coding on request: respond 400. +5. Invalid Content-Length without Transfer-Encoding: unrecoverable error. +6. Valid Content-Length without Transfer-Encoding: read that many octets. +7. **"If this is a request message and none of the above are true, then the message body length is zero (no message body is present)."** + +### Direct RFC quotes + +> "If this is a request message and none of the above are true, then the message body length is zero (no message body is present)." -- RFC 9112 Section 6.3 + +> "A server MAY reject a request that contains a message body but not a Content-Length by responding with 411 (Length Required)." -- RFC 9112 Section 6.3 + +> "A user agent that sends a request that contains a message body MUST send either a valid Content-Length header field or use the chunked transfer coding." -- RFC 9112 Section 6.3 + +### Chain of reasoning + +1. The test sends a POST request with no Content-Length and no Transfer-Encoding headers. +2. Walking through the RFC 9112 Section 6.3 precedence rules: rules 1-2 do not apply (this is a request, not a response). Rule 3/4: no Transfer-Encoding is present. Rule 5/6: no Content-Length is present. +3. We reach rule 7: "If this is a request message and none of the above are true, then the message body length is zero (no message body is present)." This is definitive. +4. The server MUST treat the body length as zero. It must not block waiting for body data that will never arrive. +5. The MAY reject with 411 clause applies when a request "contains a message body but not a Content-Length." In this case, rule 7 has already determined the body length is zero -- there *is* no message body. Therefore the 411 clause does not apply to this test case. +6. The server should process the request as a POST with an empty body and respond normally. + +### Scored / Unscored justification + +**Scored.** Rule 7 of RFC 9112 Section 6.3 definitively states the body length is zero when no framing headers are present. This is not a SHOULD or MAY -- it is a declarative statement of fact within the normative precedence algorithm. A server that hangs waiting for body data is violating the body-length determination algorithm. The `AllowConnectionClose` flag is set because a server may close the connection for its own reasons, but it must not stall indefinitely. + +### Edge cases + +- Some servers interpret POST without Content-Length as malformed and return `411 Length Required`. This is only correct if the server believes a body *was intended* but not framed. Since rule 7 says "no message body is present," returning 411 is technically incorrect for this scenario, but the test accepts close as an alternative. +- A server that blocks waiting for a body on this request has confused "POST method" with "POST must have a body." POST does not require a body. +- The third RFC quote ("A user agent that sends a request that contains a message body MUST send either a valid Content-Length header field or use the chunked transfer coding") places the MUST on the *client*. This test verifies the server correctly handles a client that has no body to send and therefore omits both headers. +- Some frameworks (e.g., Express.js, Flask) automatically set `Content-Length: 0` on empty POST bodies, so servers may rarely encounter this in practice. However, raw TCP clients and proxies can produce this pattern. + +## Sources + +- [RFC 9112 Section 6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/body/post-unsupported-ct.md b/docs/content/docs/body/post-unsupported-ct.md index 0db9c6e..c9a9809 100644 --- a/docs/content/docs/body/post-unsupported-ct.md +++ b/docs/content/docs/body/post-unsupported-ct.md @@ -1,46 +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) +--- +title: "POST Unsupported Content-Type — HTTP/1.1 Compliance" +description: "A POST request with an unrecognized Content-Type. Tested against RFC 9110 §15.5.16." +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 index f8e2e1c..0820258 100644 --- a/docs/content/docs/body/range-invalid.md +++ b/docs/content/docs/body/range-invalid.md @@ -1,48 +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) +--- +title: "Range Invalid — HTTP/1.1 Compliance" +description: "A GET request with a syntactically invalid Range header. Tested against RFC 9110 §14.2." +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/body/range-post.md b/docs/content/docs/body/range-post.md index 9ca39f6..22f0999 100644 --- a/docs/content/docs/body/range-post.md +++ b/docs/content/docs/body/range-post.md @@ -1,46 +1,46 @@ ---- -title: "RANGE-POST" -description: "RANGE-POST test documentation" -weight: 13 ---- - -| | | -|---|---| -| **Test ID** | `COMP-RANGE-POST` | -| **Category** | Compliance | -| **Scored** | Yes | -| **RFC** | [RFC 9110 §14.2](https://www.rfc-editor.org/rfc/rfc9110#section-14.2) | -| **RFC Level** | MUST | -| **Expected** | `2xx` (Range ignored) | - -## What it sends - -A POST request with a `Range` header. The Range mechanism only applies to GET requests. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5\r\n -Range: bytes=0-10\r\n -\r\n -hello -``` - -## What the RFC says - -> "A server MUST ignore a Range header field received with a request method that is unrecognized or for which range handling is not defined." — RFC 9110 §14.2 - -Range handling is only defined for GET (RFC 9110 §14.2). For all other methods, the server must ignore the Range header and process the request normally. - -## Why it matters - -If a server incorrectly applies Range semantics to a POST request (returning `206 Partial Content`), it could truncate the request body or cause unexpected behavior. The server should process the full POST body and return a normal `2xx` response. - -## Verdicts - -- **Pass** — Server returns `2xx` (correctly ignored Range for POST) -- **Fail** — Server returns `206` (incorrectly applied Range to POST) or any non-2xx response - -## Sources - -- [RFC 9110 §14.2](https://www.rfc-editor.org/rfc/rfc9110#section-14.2) +--- +title: "Range POST — HTTP/1.1 Compliance" +description: "A POST request with a Range header. The Range mechanism only applies to GET requests. Tested against RFC 9110 §14.2." +weight: 13 +--- + +| | | +|---|---| +| **Test ID** | `COMP-RANGE-POST` | +| **Category** | Compliance | +| **Scored** | Yes | +| **RFC** | [RFC 9110 §14.2](https://www.rfc-editor.org/rfc/rfc9110#section-14.2) | +| **RFC Level** | MUST | +| **Expected** | `2xx` (Range ignored) | + +## What it sends + +A POST request with a `Range` header. The Range mechanism only applies to GET requests. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +Range: bytes=0-10\r\n +\r\n +hello +``` + +## What the RFC says + +> "A server MUST ignore a Range header field received with a request method that is unrecognized or for which range handling is not defined." — RFC 9110 §14.2 + +Range handling is only defined for GET (RFC 9110 §14.2). For all other methods, the server must ignore the Range header and process the request normally. + +## Why it matters + +If a server incorrectly applies Range semantics to a POST request (returning `206 Partial Content`), it could truncate the request body or cause unexpected behavior. The server should process the full POST body and return a normal `2xx` response. + +## Verdicts + +- **Pass** — Server returns `2xx` (correctly ignored Range for POST) +- **Fail** — Server returns `206` (incorrectly applied Range to POST) or any non-2xx response + +## Sources + +- [RFC 9110 §14.2](https://www.rfc-editor.org/rfc/rfc9110#section-14.2) diff --git a/docs/content/docs/caching/_index.md b/docs/content/docs/caching/_index.md index 4fe9c5b..a3f2c9b 100644 --- a/docs/content/docs/caching/_index.md +++ b/docs/content/docs/caching/_index.md @@ -1,6 +1,6 @@ --- title: Caching -description: "Caching — Http11Probe documentation" +description: "Conditional request tests for ETag, Last-Modified, and If-None-Match handling, covering precedence, wildcard matching, and weak comparison." weight: 12 sidebar: open: false diff --git a/docs/content/docs/caching/etag-304.md b/docs/content/docs/caching/etag-304.md index 544b243..439b09b 100644 --- a/docs/content/docs/caching/etag-304.md +++ b/docs/content/docs/caching/etag-304.md @@ -1,62 +1,62 @@ ---- -title: "ETAG-304" -description: "CAP-ETAG-304 capability test documentation" -weight: 10 ---- - -| | | -|---|---| -| **Test ID** | `CAP-ETAG-304` | -| **Category** | Capabilities | -| **Type** | Sequence (2 steps) | -| **Scored** | No | -| **RFC** | [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) | -| **RFC Level** | SHOULD | -| **Expected** | `304` | - -## What it does - -This is a **sequence test** — it sends two requests on the same TCP connection to test ETag-based conditional request handling. - -### Step 1: Initial GET (capture ETag) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Connection: keep-alive\r\n -\r\n -``` - -Captures the `ETag` header from the response for use in step 2. - -### Step 2: Conditional GET (If-None-Match) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -If-None-Match: {ETag from step 1}\r\n -\r\n -``` - -Replays the `ETag` value captured from step 1 in an `If-None-Match` header. If the resource hasn't changed, the server should return `304 Not Modified`. If the server did not include an `ETag` header in step 1, the test reports Warn immediately. - -## What the RFC says - -> "An origin server MUST use the strong comparison function when comparing entity-tags for If-None-Match, because the client intends to use the cached representation." — RFC 9110 §13.1.2 - -> "If the field value is '*', the condition is false if the origin server has a current representation for the target resource." — RFC 9110 §13.1.2 - -## Why it matters - -ETag-based conditional requests are the most reliable caching mechanism in HTTP. They enable efficient revalidation without relying on timestamps, which can be unreliable across servers or after deployments. - -## Verdicts - -- **Pass** — Step 2 returns `304 Not Modified` -- **Warn** — Server does not include ETag in step 1, or returns `200` in step 2 (no conditional support) -- **Fail** — Unexpected error (non-2xx/304 response) - -## Sources - -- [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) -- [RFC 9110 §8.8.3](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3) +--- +title: "ETag 304 — Caching" +description: "This is a sequence test — it sends two requests on the same TCP connection to test ETag-based conditional request handling. Tested against RFC 9110 §13.1.2." +weight: 10 +--- + +| | | +|---|---| +| **Test ID** | `CAP-ETAG-304` | +| **Category** | Capabilities | +| **Type** | Sequence (2 steps) | +| **Scored** | No | +| **RFC** | [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) | +| **RFC Level** | SHOULD | +| **Expected** | `304` | + +## What it does + +This is a **sequence test** — it sends two requests on the same TCP connection to test ETag-based conditional request handling. + +### Step 1: Initial GET (capture ETag) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Connection: keep-alive\r\n +\r\n +``` + +Captures the `ETag` header from the response for use in step 2. + +### Step 2: Conditional GET (If-None-Match) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +If-None-Match: {ETag from step 1}\r\n +\r\n +``` + +Replays the `ETag` value captured from step 1 in an `If-None-Match` header. If the resource hasn't changed, the server should return `304 Not Modified`. If the server did not include an `ETag` header in step 1, the test reports Warn immediately. + +## What the RFC says + +> "An origin server MUST use the strong comparison function when comparing entity-tags for If-None-Match, because the client intends to use the cached representation." — RFC 9110 §13.1.2 + +> "If the field value is '*', the condition is false if the origin server has a current representation for the target resource." — RFC 9110 §13.1.2 + +## Why it matters + +ETag-based conditional requests are the most reliable caching mechanism in HTTP. They enable efficient revalidation without relying on timestamps, which can be unreliable across servers or after deployments. + +## Verdicts + +- **Pass** — Step 2 returns `304 Not Modified` +- **Warn** — Server does not include ETag in step 1, or returns `200` in step 2 (no conditional support) +- **Fail** — Unexpected error (non-2xx/304 response) + +## Sources + +- [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) +- [RFC 9110 §8.8.3](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3) diff --git a/docs/content/docs/caching/etag-in-304.md b/docs/content/docs/caching/etag-in-304.md index 6ca3304..b6acac6 100644 --- a/docs/content/docs/caching/etag-in-304.md +++ b/docs/content/docs/caching/etag-in-304.md @@ -1,60 +1,60 @@ ---- -title: "ETAG-IN-304" -description: "CAP-ETAG-IN-304 capability test documentation" -weight: 12 ---- - -| | | -|---|---| -| **Test ID** | `CAP-ETAG-IN-304` | -| **Category** | Capabilities | -| **Type** | Sequence (2 steps) | -| **Scored** | No | -| **RFC** | [RFC 9110 §15.4.5](https://www.rfc-editor.org/rfc/rfc9110#section-15.4.5) | -| **RFC Level** | SHOULD | -| **Expected** | `304` with ETag | - -## What it does - -This is a **sequence test** — it verifies that a `304 Not Modified` response includes the `ETag` header, allowing clients to update their cached validators. - -### Step 1: Initial GET (capture ETag) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Connection: keep-alive\r\n -\r\n -``` - -Captures the `ETag` header from the response. - -### Step 2: Conditional GET (If-None-Match) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -If-None-Match: {ETag from step 1}\r\n -\r\n -``` - -Replays the `ETag` value captured from step 1. If the server returns `304`, this test checks whether the `ETag` header is present in that response. - -## What the RFC says - -> "A server generating a 304 response MUST generate any of the following header fields that would have been sent in a 200 (OK) response to the same request: ... ETag" — RFC 9110 §15.4.5 - -## Why it matters - -Including the ETag in a `304` response lets clients confirm which representation they have cached and update their stored validator. Without it, clients may lose track of the ETag and fall back to unconditional requests. - -## Verdicts - -- **Pass** — Step 2 returns `304` with an ETag header -- **Warn** — Server does not support ETags, or returns `304` without an ETag header -- **Fail** — Unexpected error (non-2xx/304 response) - -## Sources - -- [RFC 9110 §15.4.5](https://www.rfc-editor.org/rfc/rfc9110#section-15.4.5) -- [RFC 9110 §8.8.3](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3) +--- +title: "ETag In 304 — Caching" +description: "This is a sequence test — it verifies that a 304 Not Modified response includes the ETag header, allowing clients to update their cached validators. Tested against RFC 9110 §15.4.5." +weight: 12 +--- + +| | | +|---|---| +| **Test ID** | `CAP-ETAG-IN-304` | +| **Category** | Capabilities | +| **Type** | Sequence (2 steps) | +| **Scored** | No | +| **RFC** | [RFC 9110 §15.4.5](https://www.rfc-editor.org/rfc/rfc9110#section-15.4.5) | +| **RFC Level** | SHOULD | +| **Expected** | `304` with ETag | + +## What it does + +This is a **sequence test** — it verifies that a `304 Not Modified` response includes the `ETag` header, allowing clients to update their cached validators. + +### Step 1: Initial GET (capture ETag) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Connection: keep-alive\r\n +\r\n +``` + +Captures the `ETag` header from the response. + +### Step 2: Conditional GET (If-None-Match) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +If-None-Match: {ETag from step 1}\r\n +\r\n +``` + +Replays the `ETag` value captured from step 1. If the server returns `304`, this test checks whether the `ETag` header is present in that response. + +## What the RFC says + +> "A server generating a 304 response MUST generate any of the following header fields that would have been sent in a 200 (OK) response to the same request: ... ETag" — RFC 9110 §15.4.5 + +## Why it matters + +Including the ETag in a `304` response lets clients confirm which representation they have cached and update their stored validator. Without it, clients may lose track of the ETag and fall back to unconditional requests. + +## Verdicts + +- **Pass** — Step 2 returns `304` with an ETag header +- **Warn** — Server does not support ETags, or returns `304` without an ETag header +- **Fail** — Unexpected error (non-2xx/304 response) + +## Sources + +- [RFC 9110 §15.4.5](https://www.rfc-editor.org/rfc/rfc9110#section-15.4.5) +- [RFC 9110 §8.8.3](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3) diff --git a/docs/content/docs/caching/etag-weak.md b/docs/content/docs/caching/etag-weak.md index 9538320..6665a23 100644 --- a/docs/content/docs/caching/etag-weak.md +++ b/docs/content/docs/caching/etag-weak.md @@ -1,62 +1,62 @@ ---- -title: "ETAG-WEAK" -description: "CAP-ETAG-WEAK capability test documentation" -weight: 18 ---- - -| | | -|---|---| -| **Test ID** | `CAP-ETAG-WEAK` | -| **Category** | Capabilities | -| **Type** | Sequence (2 steps) | -| **Scored** | No | -| **RFC** | [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) | -| **RFC Level** | SHOULD | -| **Expected** | `304` | - -## What it does - -This is a **sequence test** — it captures the server's ETag and resends it with a `W/` weak prefix in `If-None-Match` to test whether the server uses the weak comparison function for GET requests. - -### Step 1: Initial GET (capture ETag) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Connection: keep-alive\r\n -\r\n -``` - -Captures the `ETag` header from the response. If the ETag is strong (e.g., `"abc123"`), step 2 will prepend `W/` to make it weak (`W/"abc123"`). If already weak, it is sent as-is. - -### Step 2: Conditional GET (If-None-Match: W/etag) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -If-None-Match: {ETag from step 1, weak}\r\n -\r\n -``` - -The weak ETag should still match via the weak comparison function, which only compares the opaque-tag portion. - -## What the RFC says - -> "A recipient MUST use the weak comparison function when comparing entity-tags for If-None-Match." — RFC 9110 §13.1.2 - -The weak comparison function is defined as: "two entity-tags are equivalent if their opaque-tags match character-by-character, regardless of either or both being tagged as 'weak'." - -## Why it matters - -GET conditional requests must use weak comparison. A server that only does byte-for-byte matching of the full ETag string (including `W/` prefix) will fail to match weak ETags, causing unnecessary full responses for cacheable content. - -## Verdicts - -- **Pass** — Step 2 returns `304` (weak comparison matched) -- **Warn** — No ETag in step 1, or step 2 returns `200` (server didn't use weak comparison) -- **Fail** — Unexpected error (non-2xx/304 response) - -## Sources - -- [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) -- [RFC 9110 §8.8.3.2](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3.2) +--- +title: "ETag Weak — Caching" +description: "This is a sequence test — it captures the server's ETag and resends it with a W/ weak prefix in If-None-Match to test whether the server uses the weak comparison function for GET requests." +weight: 18 +--- + +| | | +|---|---| +| **Test ID** | `CAP-ETAG-WEAK` | +| **Category** | Capabilities | +| **Type** | Sequence (2 steps) | +| **Scored** | No | +| **RFC** | [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) | +| **RFC Level** | SHOULD | +| **Expected** | `304` | + +## What it does + +This is a **sequence test** — it captures the server's ETag and resends it with a `W/` weak prefix in `If-None-Match` to test whether the server uses the weak comparison function for GET requests. + +### Step 1: Initial GET (capture ETag) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Connection: keep-alive\r\n +\r\n +``` + +Captures the `ETag` header from the response. If the ETag is strong (e.g., `"abc123"`), step 2 will prepend `W/` to make it weak (`W/"abc123"`). If already weak, it is sent as-is. + +### Step 2: Conditional GET (If-None-Match: W/etag) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +If-None-Match: {ETag from step 1, weak}\r\n +\r\n +``` + +The weak ETag should still match via the weak comparison function, which only compares the opaque-tag portion. + +## What the RFC says + +> "A recipient MUST use the weak comparison function when comparing entity-tags for If-None-Match." — RFC 9110 §13.1.2 + +The weak comparison function is defined as: "two entity-tags are equivalent if their opaque-tags match character-by-character, regardless of either or both being tagged as 'weak'." + +## Why it matters + +GET conditional requests must use weak comparison. A server that only does byte-for-byte matching of the full ETag string (including `W/` prefix) will fail to match weak ETags, causing unnecessary full responses for cacheable content. + +## Verdicts + +- **Pass** — Step 2 returns `304` (weak comparison matched) +- **Warn** — No ETag in step 1, or step 2 returns `200` (server didn't use weak comparison) +- **Fail** — Unexpected error (non-2xx/304 response) + +## Sources + +- [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) +- [RFC 9110 §8.8.3.2](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3.2) diff --git a/docs/content/docs/caching/ims-future.md b/docs/content/docs/caching/ims-future.md index 69b4a43..bee7ee6 100644 --- a/docs/content/docs/caching/ims-future.md +++ b/docs/content/docs/caching/ims-future.md @@ -1,59 +1,59 @@ ---- -title: "IMS-FUTURE" -description: "CAP-IMS-FUTURE capability test documentation" -weight: 15 ---- - -| | | -|---|---| -| **Test ID** | `CAP-IMS-FUTURE` | -| **Category** | Capabilities | -| **Type** | Sequence (2 steps) | -| **Scored** | No | -| **RFC** | [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) | -| **RFC Level** | SHOULD | -| **Expected** | `200` | - -## What it does - -This is a **sequence test** — it sends an `If-Modified-Since` header with a date far in the future to check whether the server correctly ignores it. - -### Step 1: Initial GET (confirm 2xx) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Connection: keep-alive\r\n -\r\n -``` - -Verifies the resource exists and returns a success response. - -### Step 2: Conditional GET (If-Modified-Since: future date) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -If-Modified-Since: Thu, 01 Jan 2099 00:00:00 GMT\r\n -\r\n -``` - -Sends a future date. A compliant server must ignore an `If-Modified-Since` value that is later than the server's current time and return the resource normally. - -## What the RFC says - -> "A recipient MUST ignore If-Modified-Since if the field value is not a valid HTTP-date, or if the field value is a date in the future (compared to the server's current time)." — RFC 9110 §13.1.3 - -## Why it matters - -A server that blindly compares dates without checking whether the date is in the future could incorrectly return `304 Not Modified` for every request with a future timestamp, allowing cache-poisoning or stale-content attacks. - -## Verdicts - -- **Pass** — Step 2 returns `200` (correctly ignores future date) -- **Warn** — Server returns `304` (didn't validate the date against current time) -- **Fail** — Unexpected error (non-2xx/304 response) - -## Sources - -- [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) +--- +title: "If-Modified-Since Future — Caching" +description: "This is a sequence test — it sends an If-Modified-Since header with a date far in the future to check whether the server correctly ignores it. Tested against RFC 9110 §13.1.3." +weight: 15 +--- + +| | | +|---|---| +| **Test ID** | `CAP-IMS-FUTURE` | +| **Category** | Capabilities | +| **Type** | Sequence (2 steps) | +| **Scored** | No | +| **RFC** | [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) | +| **RFC Level** | SHOULD | +| **Expected** | `200` | + +## What it does + +This is a **sequence test** — it sends an `If-Modified-Since` header with a date far in the future to check whether the server correctly ignores it. + +### Step 1: Initial GET (confirm 2xx) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Connection: keep-alive\r\n +\r\n +``` + +Verifies the resource exists and returns a success response. + +### Step 2: Conditional GET (If-Modified-Since: future date) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +If-Modified-Since: Thu, 01 Jan 2099 00:00:00 GMT\r\n +\r\n +``` + +Sends a future date. A compliant server must ignore an `If-Modified-Since` value that is later than the server's current time and return the resource normally. + +## What the RFC says + +> "A recipient MUST ignore If-Modified-Since if the field value is not a valid HTTP-date, or if the field value is a date in the future (compared to the server's current time)." — RFC 9110 §13.1.3 + +## Why it matters + +A server that blindly compares dates without checking whether the date is in the future could incorrectly return `304 Not Modified` for every request with a future timestamp, allowing cache-poisoning or stale-content attacks. + +## Verdicts + +- **Pass** — Step 2 returns `200` (correctly ignores future date) +- **Warn** — Server returns `304` (didn't validate the date against current time) +- **Fail** — Unexpected error (non-2xx/304 response) + +## Sources + +- [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) diff --git a/docs/content/docs/caching/ims-invalid.md b/docs/content/docs/caching/ims-invalid.md index 9eacc25..a81e122 100644 --- a/docs/content/docs/caching/ims-invalid.md +++ b/docs/content/docs/caching/ims-invalid.md @@ -1,59 +1,59 @@ ---- -title: "IMS-INVALID" -description: "CAP-IMS-INVALID capability test documentation" -weight: 16 ---- - -| | | -|---|---| -| **Test ID** | `CAP-IMS-INVALID` | -| **Category** | Capabilities | -| **Type** | Sequence (2 steps) | -| **Scored** | No | -| **RFC** | [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) | -| **RFC Level** | SHOULD | -| **Expected** | `200` | - -## What it does - -This is a **sequence test** — it sends an `If-Modified-Since` header with an unparseable garbage value to check whether the server correctly ignores it. - -### Step 1: Initial GET (confirm 2xx) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Connection: keep-alive\r\n -\r\n -``` - -Verifies the resource exists and returns a success response. - -### Step 2: Conditional GET (If-Modified-Since: garbage) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -If-Modified-Since: not-a-date\r\n -\r\n -``` - -Sends a value that is not a valid HTTP-date. A compliant server must ignore the header and return the resource normally. - -## What the RFC says - -> "A recipient MUST ignore If-Modified-Since if the field value is not a valid HTTP-date." — RFC 9110 §13.1.3 - -## Why it matters - -If a server treats an unparseable date as "very old" and returns `304`, it could cause clients to serve stale cached content. Correct behavior is to ignore the invalid header entirely. - -## Verdicts - -- **Pass** — Step 2 returns `200` (correctly ignores invalid date) -- **Warn** — Server returns `304` (treated garbage as a valid date) -- **Fail** — Unexpected error (non-2xx/304 response) - -## Sources - -- [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) +--- +title: "If-Modified-Since Invalid — Caching" +description: "This is a sequence test — it sends an If-Modified-Since header with an unparseable garbage value to check whether the server correctly ignores it. Tested against RFC 9110 §13.1.3." +weight: 16 +--- + +| | | +|---|---| +| **Test ID** | `CAP-IMS-INVALID` | +| **Category** | Capabilities | +| **Type** | Sequence (2 steps) | +| **Scored** | No | +| **RFC** | [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) | +| **RFC Level** | SHOULD | +| **Expected** | `200` | + +## What it does + +This is a **sequence test** — it sends an `If-Modified-Since` header with an unparseable garbage value to check whether the server correctly ignores it. + +### Step 1: Initial GET (confirm 2xx) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Connection: keep-alive\r\n +\r\n +``` + +Verifies the resource exists and returns a success response. + +### Step 2: Conditional GET (If-Modified-Since: garbage) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +If-Modified-Since: not-a-date\r\n +\r\n +``` + +Sends a value that is not a valid HTTP-date. A compliant server must ignore the header and return the resource normally. + +## What the RFC says + +> "A recipient MUST ignore If-Modified-Since if the field value is not a valid HTTP-date." — RFC 9110 §13.1.3 + +## Why it matters + +If a server treats an unparseable date as "very old" and returns `304`, it could cause clients to serve stale cached content. Correct behavior is to ignore the invalid header entirely. + +## Verdicts + +- **Pass** — Step 2 returns `200` (correctly ignores invalid date) +- **Warn** — Server returns `304` (treated garbage as a valid date) +- **Fail** — Unexpected error (non-2xx/304 response) + +## Sources + +- [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) diff --git a/docs/content/docs/caching/inm-precedence.md b/docs/content/docs/caching/inm-precedence.md index 1cd7ad3..c0255d7 100644 --- a/docs/content/docs/caching/inm-precedence.md +++ b/docs/content/docs/caching/inm-precedence.md @@ -1,61 +1,61 @@ ---- -title: "INM-PRECEDENCE" -description: "CAP-INM-PRECEDENCE capability test documentation" -weight: 13 ---- - -| | | -|---|---| -| **Test ID** | `CAP-INM-PRECEDENCE` | -| **Category** | Capabilities | -| **Type** | Sequence (2 steps) | -| **Scored** | No | -| **RFC** | [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) | -| **RFC Level** | SHOULD | -| **Expected** | `304` | - -## What it does - -This is a **sequence test** — it sends a conditional GET with both `If-None-Match` (matching ETag) and `If-Modified-Since` (epoch timestamp, guaranteed stale) to verify that the server correctly gives precedence to ETag matching. - -### Step 1: Initial GET (capture ETag) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Connection: keep-alive\r\n -\r\n -``` - -Captures the `ETag` header from the response. - -### Step 2: Conditional GET (INM + stale IMS) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -If-None-Match: {ETag from step 1}\r\n -If-Modified-Since: Thu, 01 Jan 1970 00:00:00 GMT\r\n -\r\n -``` - -Replays the `ETag` value captured from step 1 in `If-None-Match` (should produce `304`), combined with `If-Modified-Since` set to epoch (should produce `200` since the resource was certainly modified after 1970). If the server returns `304`, it correctly evaluated `If-None-Match` first. - -## What the RFC says - -> "A recipient MUST ignore If-Modified-Since if the request contains an If-None-Match header field; the condition in If-None-Match is considered to be a more accurate replacement for the condition in If-Modified-Since." — RFC 9110 §13.1.3 - -## Why it matters - -This is a **MUST**-level requirement in RFC 9110 §13.1.3 for servers that support both mechanisms. If a server evaluates `If-Modified-Since` instead of (or in addition to) `If-None-Match`, clients may get unexpected `200` responses and re-download unchanged resources. - -## Verdicts - -- **Pass** — Step 2 returns `304` (If-None-Match took precedence) -- **Warn** — Server does not support ETags, or returns `200` (If-Modified-Since took precedence) -- **Fail** — Unexpected error (non-2xx/304 response) - -## Sources - -- [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) -- [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) +--- +title: "If-None-Match Precedence — Caching" +description: "This is a sequence test — it sends a conditional GET with both If-None-Match (matching ETag) and If-Modified-Since (epoch timestamp, guaranteed stale) to verify that the server correctly gives…" +weight: 13 +--- + +| | | +|---|---| +| **Test ID** | `CAP-INM-PRECEDENCE` | +| **Category** | Capabilities | +| **Type** | Sequence (2 steps) | +| **Scored** | No | +| **RFC** | [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) | +| **RFC Level** | SHOULD | +| **Expected** | `304` | + +## What it does + +This is a **sequence test** — it sends a conditional GET with both `If-None-Match` (matching ETag) and `If-Modified-Since` (epoch timestamp, guaranteed stale) to verify that the server correctly gives precedence to ETag matching. + +### Step 1: Initial GET (capture ETag) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Connection: keep-alive\r\n +\r\n +``` + +Captures the `ETag` header from the response. + +### Step 2: Conditional GET (INM + stale IMS) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +If-None-Match: {ETag from step 1}\r\n +If-Modified-Since: Thu, 01 Jan 1970 00:00:00 GMT\r\n +\r\n +``` + +Replays the `ETag` value captured from step 1 in `If-None-Match` (should produce `304`), combined with `If-Modified-Since` set to epoch (should produce `200` since the resource was certainly modified after 1970). If the server returns `304`, it correctly evaluated `If-None-Match` first. + +## What the RFC says + +> "A recipient MUST ignore If-Modified-Since if the request contains an If-None-Match header field; the condition in If-None-Match is considered to be a more accurate replacement for the condition in If-Modified-Since." — RFC 9110 §13.1.3 + +## Why it matters + +This is a **MUST**-level requirement in RFC 9110 §13.1.3 for servers that support both mechanisms. If a server evaluates `If-Modified-Since` instead of (or in addition to) `If-None-Match`, clients may get unexpected `200` responses and re-download unchanged resources. + +## Verdicts + +- **Pass** — Step 2 returns `304` (If-None-Match took precedence) +- **Warn** — Server does not support ETags, or returns `200` (If-Modified-Since took precedence) +- **Fail** — Unexpected error (non-2xx/304 response) + +## Sources + +- [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) +- [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) diff --git a/docs/content/docs/caching/inm-unquoted.md b/docs/content/docs/caching/inm-unquoted.md index 06cc667..fff2162 100644 --- a/docs/content/docs/caching/inm-unquoted.md +++ b/docs/content/docs/caching/inm-unquoted.md @@ -1,63 +1,63 @@ ---- -title: "INM-UNQUOTED" -description: "CAP-INM-UNQUOTED capability test documentation" -weight: 17 ---- - -| | | -|---|---| -| **Test ID** | `CAP-INM-UNQUOTED` | -| **Category** | Capabilities | -| **Type** | Sequence (2 steps) | -| **Scored** | No | -| **RFC** | [RFC 9110 §8.8.3](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3) | -| **RFC Level** | SHOULD | -| **Expected** | `200` | - -## What it does - -This is a **sequence test** — it captures the server's ETag, strips the surrounding quotes, and sends it back unquoted in `If-None-Match` to test whether the server enforces ETag syntax. - -### Step 1: Initial GET (capture ETag) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Connection: keep-alive\r\n -\r\n -``` - -Captures the `ETag` header from the response for use in step 2. - -### Step 2: Conditional GET (If-None-Match: unquoted) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -If-None-Match: {ETag from step 1, unquoted}\r\n -\r\n -``` - -Sends the ETag value captured from step 1, stripped of the required surrounding double quotes. According to the RFC grammar, `entity-tag = [ weak ] opaque-tag` and `opaque-tag = DQUOTE *etagc DQUOTE` — the quotes are mandatory. - -## What the RFC says - -> `entity-tag = [ weak ] opaque-tag` -> `opaque-tag = DQUOTE *etagc DQUOTE` — RFC 9110 §8.8.3 - -An unquoted value violates the entity-tag syntax. A strict server should not match it. - -## Why it matters - -Accepting unquoted ETags means the server is doing lenient parsing of conditional headers. While not a security vulnerability, it indicates relaxed syntax validation that could mask other parsing issues. - -## Verdicts - -- **Pass** — Step 2 returns `200` (correctly rejects malformed ETag syntax) -- **Warn** — No ETag in step 1 (no ETag support), or step 2 returns `304` (accepted unquoted ETag) -- **Fail** — Unexpected error (non-2xx/304 response) - -## Sources - -- [RFC 9110 §8.8.3](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3) -- [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) +--- +title: "If-None-Match Unquoted — Caching" +description: "This is a sequence test — it captures the server's ETag, strips the surrounding quotes, and sends it back unquoted in If-None-Match to test whether the server enforces ETag syntax." +weight: 17 +--- + +| | | +|---|---| +| **Test ID** | `CAP-INM-UNQUOTED` | +| **Category** | Capabilities | +| **Type** | Sequence (2 steps) | +| **Scored** | No | +| **RFC** | [RFC 9110 §8.8.3](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3) | +| **RFC Level** | SHOULD | +| **Expected** | `200` | + +## What it does + +This is a **sequence test** — it captures the server's ETag, strips the surrounding quotes, and sends it back unquoted in `If-None-Match` to test whether the server enforces ETag syntax. + +### Step 1: Initial GET (capture ETag) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Connection: keep-alive\r\n +\r\n +``` + +Captures the `ETag` header from the response for use in step 2. + +### Step 2: Conditional GET (If-None-Match: unquoted) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +If-None-Match: {ETag from step 1, unquoted}\r\n +\r\n +``` + +Sends the ETag value captured from step 1, stripped of the required surrounding double quotes. According to the RFC grammar, `entity-tag = [ weak ] opaque-tag` and `opaque-tag = DQUOTE *etagc DQUOTE` — the quotes are mandatory. + +## What the RFC says + +> `entity-tag = [ weak ] opaque-tag` +> `opaque-tag = DQUOTE *etagc DQUOTE` — RFC 9110 §8.8.3 + +An unquoted value violates the entity-tag syntax. A strict server should not match it. + +## Why it matters + +Accepting unquoted ETags means the server is doing lenient parsing of conditional headers. While not a security vulnerability, it indicates relaxed syntax validation that could mask other parsing issues. + +## Verdicts + +- **Pass** — Step 2 returns `200` (correctly rejects malformed ETag syntax) +- **Warn** — No ETag in step 1 (no ETag support), or step 2 returns `304` (accepted unquoted ETag) +- **Fail** — Unexpected error (non-2xx/304 response) + +## Sources + +- [RFC 9110 §8.8.3](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3) +- [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) diff --git a/docs/content/docs/caching/inm-wildcard.md b/docs/content/docs/caching/inm-wildcard.md index a83dcc1..1f811e3 100644 --- a/docs/content/docs/caching/inm-wildcard.md +++ b/docs/content/docs/caching/inm-wildcard.md @@ -1,61 +1,61 @@ ---- -title: "INM-WILDCARD" -description: "CAP-INM-WILDCARD capability test documentation" -weight: 14 ---- - -| | | -|---|---| -| **Test ID** | `CAP-INM-WILDCARD` | -| **Category** | Capabilities | -| **Type** | Sequence (2 steps) | -| **Scored** | No | -| **RFC** | [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) | -| **RFC Level** | SHOULD | -| **Expected** | `304` | - -## What it does - -This is a **sequence test** — it uses the wildcard `*` value in `If-None-Match` to test whether the server recognizes that any current representation matches. - -### Step 1: Initial GET (confirm 2xx) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Connection: keep-alive\r\n -\r\n -``` - -Verifies the resource exists and returns a success response. - -### Step 2: Conditional GET (If-None-Match: *) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -If-None-Match: *\r\n -\r\n -``` - -The wildcard `*` means "match any entity-tag". Since step 1 confirmed the resource exists, the server should return `304 Not Modified`. - -## What the RFC says - -> "If the field value is '*', the condition is false if the origin server has a current representation for the target resource." — RFC 9110 §13.1.2 - -In other words, `If-None-Match: *` means "give me the resource only if it doesn't exist". Since it does exist, the condition is false, and the server should return `304`. - -## Why it matters - -The wildcard `If-None-Match` is primarily used for preventing the "lost update" problem in PUT requests (only create if absent). For GET, it's a useful test of whether the server has a standards-compliant conditional request implementation beyond simple ETag string matching. - -## Verdicts - -- **Pass** — Step 2 returns `304 Not Modified` -- **Warn** — Server returns `200` (ignores `If-None-Match: *`) -- **Fail** — Unexpected error (non-2xx/304 response) - -## Sources - -- [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) +--- +title: "If-None-Match Wildcard — Caching" +description: "This is a sequence test — it uses the wildcard * value in If-None-Match to test whether the server recognizes that any current representation matches. Tested against RFC 9110 §13.1.2." +weight: 14 +--- + +| | | +|---|---| +| **Test ID** | `CAP-INM-WILDCARD` | +| **Category** | Capabilities | +| **Type** | Sequence (2 steps) | +| **Scored** | No | +| **RFC** | [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) | +| **RFC Level** | SHOULD | +| **Expected** | `304` | + +## What it does + +This is a **sequence test** — it uses the wildcard `*` value in `If-None-Match` to test whether the server recognizes that any current representation matches. + +### Step 1: Initial GET (confirm 2xx) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Connection: keep-alive\r\n +\r\n +``` + +Verifies the resource exists and returns a success response. + +### Step 2: Conditional GET (If-None-Match: *) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +If-None-Match: *\r\n +\r\n +``` + +The wildcard `*` means "match any entity-tag". Since step 1 confirmed the resource exists, the server should return `304 Not Modified`. + +## What the RFC says + +> "If the field value is '*', the condition is false if the origin server has a current representation for the target resource." — RFC 9110 §13.1.2 + +In other words, `If-None-Match: *` means "give me the resource only if it doesn't exist". Since it does exist, the condition is false, and the server should return `304`. + +## Why it matters + +The wildcard `If-None-Match` is primarily used for preventing the "lost update" problem in PUT requests (only create if absent). For GET, it's a useful test of whether the server has a standards-compliant conditional request implementation beyond simple ETag string matching. + +## Verdicts + +- **Pass** — Step 2 returns `304 Not Modified` +- **Warn** — Server returns `200` (ignores `If-None-Match: *`) +- **Fail** — Unexpected error (non-2xx/304 response) + +## Sources + +- [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) diff --git a/docs/content/docs/caching/last-modified-304.md b/docs/content/docs/caching/last-modified-304.md index aec84da..161d8ea 100644 --- a/docs/content/docs/caching/last-modified-304.md +++ b/docs/content/docs/caching/last-modified-304.md @@ -1,60 +1,60 @@ ---- -title: "LAST-MODIFIED-304" -description: "CAP-LAST-MODIFIED-304 capability test documentation" -weight: 11 ---- - -| | | -|---|---| -| **Test ID** | `CAP-LAST-MODIFIED-304` | -| **Category** | Capabilities | -| **Type** | Sequence (2 steps) | -| **Scored** | No | -| **RFC** | [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) | -| **RFC Level** | SHOULD | -| **Expected** | `304` | - -## What it does - -This is a **sequence test** — it sends two requests on the same TCP connection to test Last-Modified-based conditional request handling. - -### Step 1: Initial GET (capture Last-Modified) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Connection: keep-alive\r\n -\r\n -``` - -Captures the `Last-Modified` header from the response for use in step 2. - -### Step 2: Conditional GET (If-Modified-Since) - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -If-Modified-Since: {Last-Modified from step 1}\r\n -\r\n -``` - -Replays the `Last-Modified` value captured from step 1 in an `If-Modified-Since` header. If the resource hasn't changed since that date, the server should return `304 Not Modified`. If the server did not include a `Last-Modified` header in step 1, the test reports Warn immediately. - -## What the RFC says - -> "A recipient MUST ignore If-Modified-Since if the request contains an If-None-Match header field... The condition in If-Modified-Since is only evaluated if the request is for a safe method." — RFC 9110 §13.1.3 - -## Why it matters - -Last-Modified is the oldest conditional request mechanism in HTTP and remains widely deployed. Unlike ETags, it relies on timestamps, which makes it less precise but simpler to implement for static file servers. - -## Verdicts - -- **Pass** — Step 2 returns `304 Not Modified` -- **Warn** — Server does not include Last-Modified in step 1, or returns `200` in step 2 (no conditional support) -- **Fail** — Unexpected error (non-2xx/304 response) - -## Sources - -- [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) -- [RFC 9110 §8.8.2](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.2) +--- +title: "Last Modified 304 — Caching" +description: "This is a sequence test — it sends two requests on the same TCP connection to test Last-Modified-based conditional request handling. Tested against RFC 9110 §13.1.3." +weight: 11 +--- + +| | | +|---|---| +| **Test ID** | `CAP-LAST-MODIFIED-304` | +| **Category** | Capabilities | +| **Type** | Sequence (2 steps) | +| **Scored** | No | +| **RFC** | [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) | +| **RFC Level** | SHOULD | +| **Expected** | `304` | + +## What it does + +This is a **sequence test** — it sends two requests on the same TCP connection to test Last-Modified-based conditional request handling. + +### Step 1: Initial GET (capture Last-Modified) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Connection: keep-alive\r\n +\r\n +``` + +Captures the `Last-Modified` header from the response for use in step 2. + +### Step 2: Conditional GET (If-Modified-Since) + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +If-Modified-Since: {Last-Modified from step 1}\r\n +\r\n +``` + +Replays the `Last-Modified` value captured from step 1 in an `If-Modified-Since` header. If the resource hasn't changed since that date, the server should return `304 Not Modified`. If the server did not include a `Last-Modified` header in step 1, the test reports Warn immediately. + +## What the RFC says + +> "A recipient MUST ignore If-Modified-Since if the request contains an If-None-Match header field... The condition in If-Modified-Since is only evaluated if the request is for a safe method." — RFC 9110 §13.1.3 + +## Why it matters + +Last-Modified is the oldest conditional request mechanism in HTTP and remains widely deployed. Unlike ETags, it relies on timestamps, which makes it less precise but simpler to implement for static file servers. + +## Verdicts + +- **Pass** — Step 2 returns `304 Not Modified` +- **Warn** — Server does not include Last-Modified in step 1, or returns `200` in step 2 (no conditional support) +- **Fail** — Unexpected error (non-2xx/304 response) + +## Sources + +- [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) +- [RFC 9110 §8.8.2](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.2) diff --git a/docs/content/docs/content-length/_index.md b/docs/content/docs/content-length/_index.md index 547656b..f1a2e25 100644 --- a/docs/content/docs/content-length/_index.md +++ b/docs/content/docs/content-length/_index.md @@ -1,6 +1,6 @@ --- title: Content-Length -description: "Content-Length — Http11Probe documentation" +description: "Content-Length header validation tests covering non-numeric values, plus signs, integer overflow, and other malformed framing per RFC 9112." weight: 8 sidebar: open: false diff --git a/docs/content/docs/content-length/cl-non-numeric.md b/docs/content/docs/content-length/cl-non-numeric.md index 8d934ba..7b59175 100644 --- a/docs/content/docs/content-length/cl-non-numeric.md +++ b/docs/content/docs/content-length/cl-non-numeric.md @@ -1,83 +1,83 @@ ---- -title: "CL-NON-NUMERIC" -description: "CL-NON-NUMERIC test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-6.1-CL-NON-NUMERIC` | -| **Category** | Compliance | -| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6), [RFC 9112 Section 6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -A request with a non-numeric `Content-Length` value, e.g., `Content-Length: abc`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: abc\r\n -\r\n -``` - - -## What the RFC says - -> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 - -A value containing non-digit characters (`abc`) does not match the `1*DIGIT` grammar. - -> "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, unless the field value can be successfully parsed as a comma-separated list (Section 5.6.1 of [HTTP]), all values in the list are valid, and all values in the list are the same." -- RFC 9112 Section 6.3 - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 Section 6.3 - -"Unrecoverable error" means the server must reject -- either with a 400 response or by closing the connection. It cannot attempt to parse the body. - -## Why it matters - -Content-Length is the primary framing mechanism for HTTP messages without Transfer-Encoding. If a server accepts a non-numeric Content-Length value, it has no reliable way to determine where the message body ends. This framing ambiguity is the foundation of HTTP request smuggling: if the server and a downstream proxy disagree on the body length, an attacker can inject a second request into the body of the first. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -Content-Length = 1*DIGIT -DIGIT = %x30-39 ; 0-9 -``` - -The Content-Length grammar is one of the simplest in the HTTP specification: one or more ASCII digit characters. No sign characters (`+`, `-`), no whitespace, no alphabetic characters, no hexadecimal notation -- strictly decimal digits. - -### RFC Evidence - -**RFC 9110 Section 8.6** defines the grammar: - -> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 - -**RFC 9112 Section 6.3** mandates how invalid Content-Length must be handled: - -> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." -- RFC 9112 Section 6.3 - -**RFC 9112 Section 6.3** specifies the exact server response for unrecoverable errors: - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 Section 6.3 - -### Chain of Reasoning - -1. The test sends `Content-Length: abc`. The characters `a`, `b`, `c` are not in the DIGIT range (`%x30-39`). -2. The value `abc` cannot match `1*DIGIT`, making the Content-Length header field invalid. -3. Without Transfer-Encoding, Content-Length is the sole mechanism for determining message body length. An invalid Content-Length means the message framing is indeterminate. -4. RFC 9112 Section 6.3 explicitly designates this as an "unrecoverable error" -- the strongest error classification in the HTTP framing specification. -5. For requests, the server MUST respond with 400 and then close the connection. The RFC mandates both actions: the 400 response and the subsequent connection closure. - -### Scoring Justification - -**Scored (MUST).** RFC 9112 Section 6.3 mandates 400 followed by connection close for invalid Content-Length in requests. This is one of the few requirements where the RFC mandates both a specific status code and a connection behavior. Both 400 and connection close are acceptable test outcomes because the "and then close the connection" phrasing means some servers may close the connection before fully transmitting the 400 response. - -## Sources - -- [RFC 9110 Section 8.6 -- Content-Length](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9112 Section 6.3 -- Message Body Length](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL Non Numeric — HTTP/1.1 Compliance" +description: "A request with a non-numeric Content-Length value, e.g., Content-Length: abc. Tested against RFC 9110 Section 8.6." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-6.1-CL-NON-NUMERIC` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6), [RFC 9112 Section 6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +A request with a non-numeric `Content-Length` value, e.g., `Content-Length: abc`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: abc\r\n +\r\n +``` + + +## What the RFC says + +> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 + +A value containing non-digit characters (`abc`) does not match the `1*DIGIT` grammar. + +> "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, unless the field value can be successfully parsed as a comma-separated list (Section 5.6.1 of [HTTP]), all values in the list are valid, and all values in the list are the same." -- RFC 9112 Section 6.3 + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 Section 6.3 + +"Unrecoverable error" means the server must reject -- either with a 400 response or by closing the connection. It cannot attempt to parse the body. + +## Why it matters + +Content-Length is the primary framing mechanism for HTTP messages without Transfer-Encoding. If a server accepts a non-numeric Content-Length value, it has no reliable way to determine where the message body ends. This framing ambiguity is the foundation of HTTP request smuggling: if the server and a downstream proxy disagree on the body length, an attacker can inject a second request into the body of the first. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +Content-Length = 1*DIGIT +DIGIT = %x30-39 ; 0-9 +``` + +The Content-Length grammar is one of the simplest in the HTTP specification: one or more ASCII digit characters. No sign characters (`+`, `-`), no whitespace, no alphabetic characters, no hexadecimal notation -- strictly decimal digits. + +### RFC Evidence + +**RFC 9110 Section 8.6** defines the grammar: + +> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 + +**RFC 9112 Section 6.3** mandates how invalid Content-Length must be handled: + +> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." -- RFC 9112 Section 6.3 + +**RFC 9112 Section 6.3** specifies the exact server response for unrecoverable errors: + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 Section 6.3 + +### Chain of Reasoning + +1. The test sends `Content-Length: abc`. The characters `a`, `b`, `c` are not in the DIGIT range (`%x30-39`). +2. The value `abc` cannot match `1*DIGIT`, making the Content-Length header field invalid. +3. Without Transfer-Encoding, Content-Length is the sole mechanism for determining message body length. An invalid Content-Length means the message framing is indeterminate. +4. RFC 9112 Section 6.3 explicitly designates this as an "unrecoverable error" -- the strongest error classification in the HTTP framing specification. +5. For requests, the server MUST respond with 400 and then close the connection. The RFC mandates both actions: the 400 response and the subsequent connection closure. + +### Scoring Justification + +**Scored (MUST).** RFC 9112 Section 6.3 mandates 400 followed by connection close for invalid Content-Length in requests. This is one of the few requirements where the RFC mandates both a specific status code and a connection behavior. Both 400 and connection close are acceptable test outcomes because the "and then close the connection" phrasing means some servers may close the connection before fully transmitting the 400 response. + +## Sources + +- [RFC 9110 Section 8.6 -- Content-Length](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9112 Section 6.3 -- Message Body Length](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/content-length/cl-plus-sign.md b/docs/content/docs/content-length/cl-plus-sign.md index 5f41734..ff93472 100644 --- a/docs/content/docs/content-length/cl-plus-sign.md +++ b/docs/content/docs/content-length/cl-plus-sign.md @@ -1,82 +1,82 @@ ---- -title: "CL-PLUS-SIGN" -description: "CL-PLUS-SIGN test documentation" -weight: 2 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-6.1-CL-PLUS-SIGN` | -| **Category** | Compliance | -| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6), [RFC 9112 Section 6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -A request with a plus sign in the Content-Length value: `Content-Length: +5`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: +5\r\n -\r\n -hello -``` - - -## What the RFC says - -> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 - -The `+` character is not in the DIGIT set (`%x30-39`), so `+5` does not match `1*DIGIT`. - -> "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." -- RFC 9112 Section 6.3 - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 Section 6.3 - -## Why it matters - -Many programming languages’ integer parsers accept leading `+` signs (e.g., `parseInt("+42")` returns `42` in JavaScript). A server that blindly passes Content-Length through such a parser may accept this value while another server in the chain rejects it — creating a framing disagreement. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -Content-Length = 1*DIGIT -DIGIT = %x30-39 ; 0-9 -``` - -The `+` character (0x2B) is not in the DIGIT range (`%x30-39`). The ABNF is unambiguous: Content-Length must begin with a digit, not a sign character. - -### RFC Evidence - -**RFC 9110 Section 8.6** defines the grammar: - -> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 - -**RFC 9112 Section 6.3** classifies invalid Content-Length as an unrecoverable error: - -> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." -- RFC 9112 Section 6.3 - -**RFC 9112 Section 6.3** mandates the server response: - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 Section 6.3 - -### Chain of Reasoning - -1. The test sends `Content-Length: +5`. The `+` character (ASCII 0x2B) is not a DIGIT (`%x30-39`). -2. The value `+5` does not match `1*DIGIT` because the first character is not a digit. This makes the Content-Length invalid. -3. The subtle danger is that many programming language standard libraries accept leading `+` in integer parsing: JavaScript's `parseInt("+5")` returns `5`, Python's `int("+5")` returns `5`, and C#'s `int.Parse("+5")` returns `5`. -4. If the server's parser accepts `+5` as `5` but a downstream proxy's parser rejects it (or vice versa), the two disagree on the message body length. This framing disagreement is the fundamental precondition for request smuggling. -5. The RFC deliberately chose `1*DIGIT` rather than a more permissive integer syntax precisely to prevent this class of parsing divergence. - -### Scoring Justification - -**Scored (MUST).** The `+` character violates the `1*DIGIT` grammar, making this an invalid Content-Length. RFC 9112 Section 6.3 mandates 400 followed by connection close. Both 400 and connection close are acceptable test outcomes. A server that parses `+5` as `5` and processes the request normally is non-compliant and vulnerable to smuggling via framing disagreement. - -## Sources - -- [RFC 9110 Section 8.6 -- Content-Length](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9112 Section 6.3 -- Message Body Length](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL Plus Sign — HTTP/1.1 Compliance" +description: "A request with a plus sign in the Content-Length value: Content-Length: +5. Tested against RFC 9110 Section 8.6." +weight: 2 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-6.1-CL-PLUS-SIGN` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6), [RFC 9112 Section 6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +A request with a plus sign in the Content-Length value: `Content-Length: +5`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: +5\r\n +\r\n +hello +``` + + +## What the RFC says + +> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 + +The `+` character is not in the DIGIT set (`%x30-39`), so `+5` does not match `1*DIGIT`. + +> "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." -- RFC 9112 Section 6.3 + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 Section 6.3 + +## Why it matters + +Many programming languages’ integer parsers accept leading `+` signs (e.g., `parseInt("+42")` returns `42` in JavaScript). A server that blindly passes Content-Length through such a parser may accept this value while another server in the chain rejects it — creating a framing disagreement. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +Content-Length = 1*DIGIT +DIGIT = %x30-39 ; 0-9 +``` + +The `+` character (0x2B) is not in the DIGIT range (`%x30-39`). The ABNF is unambiguous: Content-Length must begin with a digit, not a sign character. + +### RFC Evidence + +**RFC 9110 Section 8.6** defines the grammar: + +> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 + +**RFC 9112 Section 6.3** classifies invalid Content-Length as an unrecoverable error: + +> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." -- RFC 9112 Section 6.3 + +**RFC 9112 Section 6.3** mandates the server response: + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 Section 6.3 + +### Chain of Reasoning + +1. The test sends `Content-Length: +5`. The `+` character (ASCII 0x2B) is not a DIGIT (`%x30-39`). +2. The value `+5` does not match `1*DIGIT` because the first character is not a digit. This makes the Content-Length invalid. +3. The subtle danger is that many programming language standard libraries accept leading `+` in integer parsing: JavaScript's `parseInt("+5")` returns `5`, Python's `int("+5")` returns `5`, and C#'s `int.Parse("+5")` returns `5`. +4. If the server's parser accepts `+5` as `5` but a downstream proxy's parser rejects it (or vice versa), the two disagree on the message body length. This framing disagreement is the fundamental precondition for request smuggling. +5. The RFC deliberately chose `1*DIGIT` rather than a more permissive integer syntax precisely to prevent this class of parsing divergence. + +### Scoring Justification + +**Scored (MUST).** The `+` character violates the `1*DIGIT` grammar, making this an invalid Content-Length. RFC 9112 Section 6.3 mandates 400 followed by connection close. Both 400 and connection close are acceptable test outcomes. A server that parses `+5` as `5` and processes the request normally is non-compliant and vulnerable to smuggling via framing disagreement. + +## Sources + +- [RFC 9110 Section 8.6 -- Content-Length](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9112 Section 6.3 -- Message Body Length](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/content-length/no-cl-in-204.md b/docs/content/docs/content-length/no-cl-in-204.md index 86cadc9..1cd30ed 100644 --- a/docs/content/docs/content-length/no-cl-in-204.md +++ b/docs/content/docs/content-length/no-cl-in-204.md @@ -1,35 +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) +--- +title: "No CL In 204 — HTTP/1.1 Compliance" +description: "An OPTIONS request to the root path. Some servers respond with 204 No Content, which triggers the validation. Tested against RFC 9110 §8.6." +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/cookies/control-chars.md b/docs/content/docs/cookies/control-chars.md index eacceed..dcc9fec 100644 --- a/docs/content/docs/cookies/control-chars.md +++ b/docs/content/docs/cookies/control-chars.md @@ -1,37 +1,37 @@ ---- -title: "CONTROL-CHARS" -description: "COOK-CONTROL-CHARS cookie test documentation" -weight: 5 ---- - -| | | -|---|---| -| **Test ID** | `COOK-CONTROL-CHARS` | -| **Category** | Cookies | -| **Scored** | No | -| **RFC Level** | N/A | -| **Expected** | `400 (rejected) or 2xx without control chars` | - -## What it sends - -Control characters (0x01-0x03) in cookie value — dangerous if preserved. - -```http -GET /echo HTTP/1.1\r\n -Host: localhost:8080\r\n -Cookie: foo=\x01\x02\x03\r\n -\r\n -``` - -## Why it matters - -Control characters in cookie values violate RFC 6265's cookie-octet grammar and can enable response splitting or log injection if passed through to output. - -## Verdicts - -- **Pass** — 400 rejected, or 2xx with control chars stripped -- **Fail** — 2xx with control chars preserved (dangerous), or 500 - -## Sources - -- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header +--- +title: "Control Chars — Cookie Handling" +description: "Control characters (0x01-0x03) in cookie value — dangerous if preserved. Part of Http11Probe's cookie parsing resilience suite (RFC 6265)." +weight: 5 +--- + +| | | +|---|---| +| **Test ID** | `COOK-CONTROL-CHARS` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `400 (rejected) or 2xx without control chars` | + +## What it sends + +Control characters (0x01-0x03) in cookie value — dangerous if preserved. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: foo=\x01\x02\x03\r\n +\r\n +``` + +## Why it matters + +Control characters in cookie values violate RFC 6265's cookie-octet grammar and can enable response splitting or log injection if passed through to output. + +## Verdicts + +- **Pass** — 400 rejected, or 2xx with control chars stripped +- **Fail** — 2xx with control chars preserved (dangerous), or 500 + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/echo.md b/docs/content/docs/cookies/echo.md index 43eaa7a..028044c 100644 --- a/docs/content/docs/cookies/echo.md +++ b/docs/content/docs/cookies/echo.md @@ -1,37 +1,37 @@ ---- -title: "ECHO" -description: "COOK-ECHO cookie test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `COOK-ECHO` | -| **Category** | Cookies | -| **Scored** | No | -| **RFC Level** | N/A | -| **Expected** | `2xx with Cookie in body` | - -## What it sends - -Basic Cookie header echoed back by /echo endpoint. - -```http -GET /echo HTTP/1.1\r\n -Host: localhost:8080\r\n -Cookie: foo=bar\r\n -\r\n -``` - -## Why it matters - -Baseline test — verifies the server's echo endpoint reflects Cookie headers, which is required for all other cookie tests to work. - -## Verdicts - -- **Pass** — 2xx and body contains `Cookie:` header -- **Fail** — No response or missing header - -## Sources - -- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header +--- +title: "Echo — Cookie Handling" +description: "Basic Cookie header echoed back by /echo endpoint. Part of Http11Probe's cookie parsing resilience suite (RFC 6265)." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `COOK-ECHO` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx with Cookie in body` | + +## What it sends + +Basic Cookie header echoed back by /echo endpoint. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: foo=bar\r\n +\r\n +``` + +## Why it matters + +Baseline test — verifies the server's echo endpoint reflects Cookie headers, which is required for all other cookie tests to work. + +## Verdicts + +- **Pass** — 2xx and body contains `Cookie:` header +- **Fail** — No response or missing header + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/empty.md b/docs/content/docs/cookies/empty.md index 6565104..e533636 100644 --- a/docs/content/docs/cookies/empty.md +++ b/docs/content/docs/cookies/empty.md @@ -1,37 +1,37 @@ ---- -title: "EMPTY" -description: "COOK-EMPTY cookie test documentation" -weight: 3 ---- - -| | | -|---|---| -| **Test ID** | `COOK-EMPTY` | -| **Category** | Cookies | -| **Scored** | No | -| **RFC Level** | N/A | -| **Expected** | `2xx or 400` | - -## What it sends - -Empty Cookie header value — tests parser resilience. - -```http -GET /echo HTTP/1.1\r\n -Host: localhost:8080\r\n -Cookie: \r\n -\r\n -``` - -## Why it matters - -Empty Cookie headers can cause null-reference exceptions or crashes in parsers that assume at least one key=value pair. - -## Verdicts - -- **Pass** — 2xx or 400 -- **Fail** — 500 (crash) - -## Sources - -- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header +--- +title: "Empty — Cookie Handling" +description: "Empty Cookie header value — tests parser resilience. Part of Http11Probe's cookie parsing resilience suite (RFC 6265)." +weight: 3 +--- + +| | | +|---|---| +| **Test ID** | `COOK-EMPTY` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx or 400` | + +## What it sends + +Empty Cookie header value — tests parser resilience. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: \r\n +\r\n +``` + +## Why it matters + +Empty Cookie headers can cause null-reference exceptions or crashes in parsers that assume at least one key=value pair. + +## Verdicts + +- **Pass** — 2xx or 400 +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/malformed.md b/docs/content/docs/cookies/malformed.md index c1d5106..fc30e9e 100644 --- a/docs/content/docs/cookies/malformed.md +++ b/docs/content/docs/cookies/malformed.md @@ -1,37 +1,37 @@ ---- -title: "MALFORMED" -description: "COOK-MALFORMED cookie test documentation" -weight: 7 ---- - -| | | -|---|---| -| **Test ID** | `COOK-MALFORMED` | -| **Category** | Cookies | -| **Scored** | No | -| **RFC Level** | N/A | -| **Expected** | `2xx or 400` | - -## What it sends - -Completely malformed cookie value (===;;;) — tests parser crash resilience. - -```http -GET /echo HTTP/1.1\r\n -Host: localhost:8080\r\n -Cookie: ===;;;\r\n -\r\n -``` - -## Why it matters - -Garbage cookie values with no valid key=value structure can crash naive parsers that split on `=` without bounds checking. - -## Verdicts - -- **Pass** — 2xx or 400 -- **Fail** — 500 (crash) - -## Sources - -- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header +--- +title: "Malformed — Cookie Handling" +description: "Completely malformed cookie value (===;;;) — tests parser crash resilience. Part of Http11Probe's cookie parsing resilience suite (RFC 6265)." +weight: 7 +--- + +| | | +|---|---| +| **Test ID** | `COOK-MALFORMED` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx or 400` | + +## What it sends + +Completely malformed cookie value (===;;;) — tests parser crash resilience. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: ===;;;\r\n +\r\n +``` + +## Why it matters + +Garbage cookie values with no valid key=value structure can crash naive parsers that split on `=` without bounds checking. + +## Verdicts + +- **Pass** — 2xx or 400 +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/many-pairs.md b/docs/content/docs/cookies/many-pairs.md index 7e73026..6b6b156 100644 --- a/docs/content/docs/cookies/many-pairs.md +++ b/docs/content/docs/cookies/many-pairs.md @@ -1,37 +1,37 @@ ---- -title: "MANY-PAIRS" -description: "COOK-MANY-PAIRS cookie test documentation" -weight: 6 ---- - -| | | -|---|---| -| **Test ID** | `COOK-MANY-PAIRS` | -| **Category** | Cookies | -| **Scored** | No | -| **RFC Level** | N/A | -| **Expected** | `2xx or 400/431` | - -## What it sends - -1000 cookie key=value pairs — tests parser performance limits. - -```http -GET /echo HTTP/1.1\r\n -Host: localhost:8080\r\n -Cookie: k0=v0; k1=v1; ... k999=v999\r\n -\r\n -``` - -## Why it matters - -A large number of cookie pairs can cause O(n^2) parsing behavior, hashtable flooding, or memory exhaustion in frameworks that eagerly parse all cookies. - -## Verdicts - -- **Pass** — 2xx or 400/431 -- **Fail** — 500 (crash) - -## Sources - -- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header +--- +title: "Many Pairs — Cookie Handling" +description: "1000 cookie key=value pairs — tests parser performance limits. Part of Http11Probe's cookie parsing resilience suite (RFC 6265)." +weight: 6 +--- + +| | | +|---|---| +| **Test ID** | `COOK-MANY-PAIRS` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx or 400/431` | + +## What it sends + +1000 cookie key=value pairs — tests parser performance limits. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: k0=v0; k1=v1; ... k999=v999\r\n +\r\n +``` + +## Why it matters + +A large number of cookie pairs can cause O(n^2) parsing behavior, hashtable flooding, or memory exhaustion in frameworks that eagerly parse all cookies. + +## Verdicts + +- **Pass** — 2xx or 400/431 +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/multi-header.md b/docs/content/docs/cookies/multi-header.md index db9f1e1..e5520da 100644 --- a/docs/content/docs/cookies/multi-header.md +++ b/docs/content/docs/cookies/multi-header.md @@ -1,39 +1,39 @@ ---- -title: "MULTI-HEADER" -description: "COOK-MULTI-HEADER cookie test documentation" -weight: 8 ---- - -| | | -|---|---| -| **Test ID** | `COOK-MULTI-HEADER` | -| **Category** | Cookies | -| **Scored** | No | -| **RFC Level** | N/A | -| **Expected** | `2xx with both cookies` | - -## What it sends - -Two separate Cookie headers — should be folded per RFC 6265 §5.4. - -```http -GET /echo HTTP/1.1\r\n -Host: localhost:8080\r\n -Cookie: a=1\r\n -Cookie: b=2\r\n -\r\n -``` - -## Why it matters - -RFC 6265 §5.4 says the user agent SHOULD combine multiple cookie values with `; `, but servers must handle receiving them separately since some clients and proxies split them. - -## Verdicts - -- **Pass** — 2xx with both a=1 and b=2 in body -- **Warn** — Only one cookie echoed, or 400 -- **Fail** — 500 (crash) - -## Sources - -- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header +--- +title: "Multi Header — Cookie Handling" +description: "Two separate Cookie headers — should be folded per RFC 6265 §5.4. Part of Http11Probe's cookie parsing resilience suite (RFC 6265)." +weight: 8 +--- + +| | | +|---|---| +| **Test ID** | `COOK-MULTI-HEADER` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx with both cookies` | + +## What it sends + +Two separate Cookie headers — should be folded per RFC 6265 §5.4. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: a=1\r\n +Cookie: b=2\r\n +\r\n +``` + +## Why it matters + +RFC 6265 §5.4 says the user agent SHOULD combine multiple cookie values with `; `, but servers must handle receiving them separately since some clients and proxies split them. + +## Verdicts + +- **Pass** — 2xx with both a=1 and b=2 in body +- **Warn** — Only one cookie echoed, or 400 +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/null.md b/docs/content/docs/cookies/null.md index f592a95..aad4c7c 100644 --- a/docs/content/docs/cookies/null.md +++ b/docs/content/docs/cookies/null.md @@ -1,39 +1,39 @@ ---- -title: "NUL" -description: "COOK-NUL cookie test documentation" -weight: 4 ---- - -| | | -|---|---| -| **Test ID** | `COOK-NUL` | -| **Category** | Cookies | -| **Scored** | No | -| **RFC Level** | N/A | -| **Expected** | `400 (rejected) or 2xx without NUL` | - -## What it sends - -NUL byte in cookie value — dangerous if preserved by parser. - -```http -GET /echo HTTP/1.1\r\n -Host: localhost:8080\r\n -Cookie: foo=\0bar\r\n -\r\n -``` - -The cookie value contains a NUL byte (`0x00`). - -## Why it matters - -NUL bytes in cookie values can truncate strings in C-based parsers, cause log injection, or enable header injection if the NUL terminates a string boundary check. - -## Verdicts - -- **Pass** — 400 rejected, or 2xx with NUL stripped -- **Fail** — 2xx with NUL byte preserved in output (dangerous), or 500 - -## Sources - -- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header +--- +title: "Null — Cookie Handling" +description: "NUL byte in cookie value — dangerous if preserved by parser. Part of Http11Probe's cookie parsing resilience suite (RFC 6265)." +weight: 4 +--- + +| | | +|---|---| +| **Test ID** | `COOK-NUL` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `400 (rejected) or 2xx without NUL` | + +## What it sends + +NUL byte in cookie value — dangerous if preserved by parser. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: foo=\0bar\r\n +\r\n +``` + +The cookie value contains a NUL byte (`0x00`). + +## Why it matters + +NUL bytes in cookie values can truncate strings in C-based parsers, cause log injection, or enable header injection if the NUL terminates a string boundary check. + +## Verdicts + +- **Pass** — 400 rejected, or 2xx with NUL stripped +- **Fail** — 2xx with NUL byte preserved in output (dangerous), or 500 + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/oversized.md b/docs/content/docs/cookies/oversized.md index b3f3bdb..c12476f 100644 --- a/docs/content/docs/cookies/oversized.md +++ b/docs/content/docs/cookies/oversized.md @@ -1,39 +1,39 @@ ---- -title: "OVERSIZED" -description: "COOK-OVERSIZED cookie test documentation" -weight: 2 ---- - -| | | -|---|---| -| **Test ID** | `COOK-OVERSIZED` | -| **Category** | Cookies | -| **Scored** | No | -| **RFC Level** | N/A | -| **Expected** | `400/431 (rejected) or 2xx (survived)` | - -## What it sends - -64KB Cookie header — tests header size limits on cookie data. - -```http -GET /echo HTTP/1.1\r\n -Host: localhost:8080\r\n -Cookie: big=AAAA...AAAA\r\n -\r\n -``` - -The cookie value contains 65,536 bytes of `A`. - -## Why it matters - -Oversized cookies can trigger buffer overflows, OOM crashes, or excessive memory allocation in parsers that don't enforce size limits. - -## Verdicts - -- **Pass** — 400/431 rejected, or 2xx survived, or connection close -- **Fail** — 500 (crash) - -## Sources - -- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header +--- +title: "Oversized — Cookie Handling" +description: "64KB Cookie header — tests header size limits on cookie data. Part of Http11Probe's cookie parsing resilience suite (RFC 6265)." +weight: 2 +--- + +| | | +|---|---| +| **Test ID** | `COOK-OVERSIZED` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `400/431 (rejected) or 2xx (survived)` | + +## What it sends + +64KB Cookie header — tests header size limits on cookie data. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: big=AAAA...AAAA\r\n +\r\n +``` + +The cookie value contains 65,536 bytes of `A`. + +## Why it matters + +Oversized cookies can trigger buffer overflows, OOM crashes, or excessive memory allocation in parsers that don't enforce size limits. + +## Verdicts + +- **Pass** — 400/431 rejected, or 2xx survived, or connection close +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/parsed-basic.md b/docs/content/docs/cookies/parsed-basic.md index 2b91206..2cee3f1 100644 --- a/docs/content/docs/cookies/parsed-basic.md +++ b/docs/content/docs/cookies/parsed-basic.md @@ -1,38 +1,38 @@ ---- -title: "PARSED-BASIC" -description: "COOK-PARSED-BASIC cookie test documentation" -weight: 9 ---- - -| | | -|---|---| -| **Test ID** | `COOK-PARSED-BASIC` | -| **Category** | Cookies | -| **Scored** | No | -| **RFC Level** | N/A | -| **Expected** | `2xx with foo=bar in body` | - -## What it sends - -Basic cookie parsed correctly by framework. - -```http -GET /cookie HTTP/1.1\r\n -Host: localhost:8080\r\n -Cookie: foo=bar\r\n -\r\n -``` - -## Why it matters - -Tests that the framework's cookie parser correctly extracts a simple name=value pair — the most basic cookie parsing operation. - -## Verdicts - -- **Pass** — 2xx and body contains `foo=bar` -- **Warn** — 404 (endpoint not available) -- **Fail** — 500 or mangled output - -## Sources - -- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header +--- +title: "Parsed Basic — Cookie Handling" +description: "Basic cookie parsed correctly by framework. Part of Http11Probe's cookie parsing resilience suite (RFC 6265)." +weight: 9 +--- + +| | | +|---|---| +| **Test ID** | `COOK-PARSED-BASIC` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx with foo=bar in body` | + +## What it sends + +Basic cookie parsed correctly by framework. + +```http +GET /cookie HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: foo=bar\r\n +\r\n +``` + +## Why it matters + +Tests that the framework's cookie parser correctly extracts a simple name=value pair — the most basic cookie parsing operation. + +## Verdicts + +- **Pass** — 2xx and body contains `foo=bar` +- **Warn** — 404 (endpoint not available) +- **Fail** — 500 or mangled output + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/parsed-empty-val.md b/docs/content/docs/cookies/parsed-empty-val.md index b85bf61..4de267b 100644 --- a/docs/content/docs/cookies/parsed-empty-val.md +++ b/docs/content/docs/cookies/parsed-empty-val.md @@ -1,38 +1,38 @@ ---- -title: "PARSED-EMPTY-VAL" -description: "COOK-PARSED-EMPTY-VAL cookie test documentation" -weight: 11 ---- - -| | | -|---|---| -| **Test ID** | `COOK-PARSED-EMPTY-VAL` | -| **Category** | Cookies | -| **Scored** | No | -| **RFC Level** | N/A | -| **Expected** | `2xx (no crash)` | - -## What it sends - -Cookie with empty value parsed without crash. - -```http -GET /cookie HTTP/1.1\r\n -Host: localhost:8080\r\n -Cookie: foo=\r\n -\r\n -``` - -## Why it matters - -Cookies with empty values (`foo=`) are valid per RFC 6265 but can crash parsers that assume a non-empty value after the `=` sign. - -## Verdicts - -- **Pass** — 2xx or 400 -- **Warn** — 404 (endpoint not available) -- **Fail** — 500 (crash) - -## Sources - -- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header +--- +title: "Parsed Empty Val — Cookie Handling" +description: "Cookie with empty value parsed without crash. Part of Http11Probe's cookie parsing resilience suite (RFC 6265)." +weight: 11 +--- + +| | | +|---|---| +| **Test ID** | `COOK-PARSED-EMPTY-VAL` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx (no crash)` | + +## What it sends + +Cookie with empty value parsed without crash. + +```http +GET /cookie HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: foo=\r\n +\r\n +``` + +## Why it matters + +Cookies with empty values (`foo=`) are valid per RFC 6265 but can crash parsers that assume a non-empty value after the `=` sign. + +## Verdicts + +- **Pass** — 2xx or 400 +- **Warn** — 404 (endpoint not available) +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/parsed-multi.md b/docs/content/docs/cookies/parsed-multi.md index ec953bd..f9e522e 100644 --- a/docs/content/docs/cookies/parsed-multi.md +++ b/docs/content/docs/cookies/parsed-multi.md @@ -1,38 +1,38 @@ ---- -title: "PARSED-MULTI" -description: "COOK-PARSED-MULTI cookie test documentation" -weight: 10 ---- - -| | | -|---|---| -| **Test ID** | `COOK-PARSED-MULTI` | -| **Category** | Cookies | -| **Scored** | No | -| **RFC Level** | N/A | -| **Expected** | `2xx with a=1, b=2, c=3 in body` | - -## What it sends - -Multiple cookies parsed correctly by framework. - -```http -GET /cookie HTTP/1.1\r\n -Host: localhost:8080\r\n -Cookie: a=1; b=2; c=3\r\n -\r\n -``` - -## Why it matters - -Tests the framework's ability to correctly split and parse multiple semicolon-delimited cookie pairs. - -## Verdicts - -- **Pass** — 2xx and body contains all three pairs -- **Warn** — 404 (endpoint not available) -- **Fail** — Missing pairs or 500 - -## Sources - -- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header +--- +title: "Parsed Multi — Cookie Handling" +description: "Multiple cookies parsed correctly by framework. Part of Http11Probe's cookie parsing resilience suite (RFC 6265)." +weight: 10 +--- + +| | | +|---|---| +| **Test ID** | `COOK-PARSED-MULTI` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx with a=1, b=2, c=3 in body` | + +## What it sends + +Multiple cookies parsed correctly by framework. + +```http +GET /cookie HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: a=1; b=2; c=3\r\n +\r\n +``` + +## Why it matters + +Tests the framework's ability to correctly split and parse multiple semicolon-delimited cookie pairs. + +## Verdicts + +- **Pass** — 2xx and body contains all three pairs +- **Warn** — 404 (endpoint not available) +- **Fail** — Missing pairs or 500 + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/parsed-special.md b/docs/content/docs/cookies/parsed-special.md index ab73992..a9e9611 100644 --- a/docs/content/docs/cookies/parsed-special.md +++ b/docs/content/docs/cookies/parsed-special.md @@ -1,38 +1,38 @@ ---- -title: "PARSED-SPECIAL" -description: "COOK-PARSED-SPECIAL cookie test documentation" -weight: 12 ---- - -| | | -|---|---| -| **Test ID** | `COOK-PARSED-SPECIAL` | -| **Category** | Cookies | -| **Scored** | No | -| **RFC Level** | N/A | -| **Expected** | `2xx (no crash)` | - -## What it sends - -Cookies with spaces and = in values — tests framework parser edge cases. - -```http -GET /cookie HTTP/1.1\r\n -Host: localhost:8080\r\n -Cookie: a=hello world; b=x=y\r\n -\r\n -``` - -## Why it matters - -Spaces in values and `=` signs within values are common in real-world cookies (e.g., Base64-encoded tokens) and can confuse parsers that split on `=` or whitespace too aggressively. - -## Verdicts - -- **Pass** — 2xx or 400 -- **Warn** — 404 (endpoint not available) -- **Fail** — 500 (crash) - -## Sources - -- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header +--- +title: "Parsed Special — Cookie Handling" +description: "Cookies with spaces and = in values — tests framework parser edge cases. Part of Http11Probe's cookie parsing resilience suite (RFC 6265)." +weight: 12 +--- + +| | | +|---|---| +| **Test ID** | `COOK-PARSED-SPECIAL` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx (no crash)` | + +## What it sends + +Cookies with spaces and = in values — tests framework parser edge cases. + +```http +GET /cookie HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: a=hello world; b=x=y\r\n +\r\n +``` + +## Why it matters + +Spaces in values and `=` signs within values are common in real-world cookies (e.g., Base64-encoded tokens) and can confuse parsers that split on `=` or whitespace too aggressively. + +## Verdicts + +- **Pass** — 2xx or 400 +- **Warn** — 404 (endpoint not available) +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/headers/_index.md b/docs/content/docs/headers/_index.md index 0301d04..75e9d9b 100644 --- a/docs/content/docs/headers/_index.md +++ b/docs/content/docs/headers/_index.md @@ -1,6 +1,6 @@ --- title: Header Syntax -description: "Header Syntax — Http11Probe documentation" +description: "HTTP header field syntax tests covering obs-fold, space before colon, empty names, invalid characters, and missing colons per RFC 9112 Section 5." weight: 6 sidebar: open: false diff --git a/docs/content/docs/headers/accept-nonsense.md b/docs/content/docs/headers/accept-nonsense.md index fda8a4a..fa8d71d 100644 --- a/docs/content/docs/headers/accept-nonsense.md +++ b/docs/content/docs/headers/accept-nonsense.md @@ -1,45 +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) +--- +title: "Accept Nonsense — HTTP/1.1 Compliance" +description: "A GET request with an Accept header requesting a non-existent media type. Tested against RFC 9110 §12.5.1." +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/connection-close.md b/docs/content/docs/headers/connection-close.md index 10b9492..3c82147 100644 --- a/docs/content/docs/headers/connection-close.md +++ b/docs/content/docs/headers/connection-close.md @@ -1,76 +1,76 @@ ---- -title: "CONNECTION-CLOSE" -description: "CONNECTION-CLOSE test documentation" -weight: 6 ---- - -| | | -|---|---| -| **Test ID** | `COMP-CONNECTION-CLOSE` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 §9.6](https://www.rfc-editor.org/rfc/rfc9112#section-9.6) | -| **Requirement** | MUST | -| **Expected** | `2xx` + connection closed | - -## What it sends - -A standard GET request with `Connection: close` indicating the client wants the server to close the connection after sending the response. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Connection: close\r\n -\r\n -``` - -## What the RFC says - -> "A server that receives a 'close' connection option MUST initiate closure of the connection (see below) after it sends the final response to the request that contained the 'close' connection option. The server SHOULD send a 'close' connection option in its final response on that connection. The server MUST NOT process any further requests received on that connection." -- RFC 9112 Section 9.6 - -The server must both respond successfully and close the TCP connection afterward. Responding with `2xx` but leaving the connection open violates this requirement. - -## Why it matters - -If a server ignores `Connection: close` and keeps the connection alive, a client may send a second request on what it believes is a new connection. In proxy environments, this can lead to response mismatch: the proxy believes the connection is closed and assigns it to a different client, who then receives the first client's response. Honoring `Connection: close` is essential for correct connection lifecycle management. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -Connection = 1#connection-option -connection-option = token -``` - -The `close` token is a connection option sent within the `Connection` header field. When present, it signals that the sender wishes to close the connection after the current request/response exchange. - -### RFC Evidence - -**RFC 9112 Section 9.6** mandates the server behavior unambiguously: - -> "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." -- RFC 9112 Section 9.6 - -**RFC 9112 Section 9.6** further prohibits processing additional requests: - -> "The server MUST NOT process any further requests received on that connection." -- RFC 9112 Section 9.6 - -**RFC 9112 Section 9.6** also recommends the server echo the close option: - -> "The server SHOULD send a 'close' connection option in its final response on that connection." -- RFC 9112 Section 9.6 - -### Chain of Reasoning - -1. The test sends a standard `GET / HTTP/1.1` with `Connection: close`, requesting the server close the connection after responding. -2. The server MUST respond to the request (returning a normal status code such as 2xx) and then close the TCP connection. -3. A server that responds with 2xx but leaves the connection open violates the MUST requirement in Section 9.6. -4. The test validates two things: (a) the server returns a successful response, and (b) the server actually closes the TCP connection afterward. -5. If the server keeps the connection alive, downstream components (especially proxies) may misroute subsequent data, as they expect the connection to be closed. - -### Scoring Justification - -**Scored (MUST).** The RFC uses MUST for connection closure after receiving the `close` option. The expected behavior is a 2xx response followed by TCP connection close. The test validates both the response status and the connection state. A server that responds successfully but does not close the connection fails this test. - -## Sources - -- [RFC 9112 Section 9.6 -- Tear-down](https://www.rfc-editor.org/rfc/rfc9112#section-9.6) -- [RFC 9110 Section 7.6.1 -- Connection](https://www.rfc-editor.org/rfc/rfc9110#section-7.6.1) +--- +title: "Connection Close — HTTP/1.1 Compliance" +description: "A standard GET request with Connection: close indicating the client wants the server to close the connection after sending the response. Tested against RFC 9112 §9.6." +weight: 6 +--- + +| | | +|---|---| +| **Test ID** | `COMP-CONNECTION-CLOSE` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 §9.6](https://www.rfc-editor.org/rfc/rfc9112#section-9.6) | +| **Requirement** | MUST | +| **Expected** | `2xx` + connection closed | + +## What it sends + +A standard GET request with `Connection: close` indicating the client wants the server to close the connection after sending the response. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Connection: close\r\n +\r\n +``` + +## What the RFC says + +> "A server that receives a 'close' connection option MUST initiate closure of the connection (see below) after it sends the final response to the request that contained the 'close' connection option. The server SHOULD send a 'close' connection option in its final response on that connection. The server MUST NOT process any further requests received on that connection." -- RFC 9112 Section 9.6 + +The server must both respond successfully and close the TCP connection afterward. Responding with `2xx` but leaving the connection open violates this requirement. + +## Why it matters + +If a server ignores `Connection: close` and keeps the connection alive, a client may send a second request on what it believes is a new connection. In proxy environments, this can lead to response mismatch: the proxy believes the connection is closed and assigns it to a different client, who then receives the first client's response. Honoring `Connection: close` is essential for correct connection lifecycle management. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +Connection = 1#connection-option +connection-option = token +``` + +The `close` token is a connection option sent within the `Connection` header field. When present, it signals that the sender wishes to close the connection after the current request/response exchange. + +### RFC Evidence + +**RFC 9112 Section 9.6** mandates the server behavior unambiguously: + +> "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." -- RFC 9112 Section 9.6 + +**RFC 9112 Section 9.6** further prohibits processing additional requests: + +> "The server MUST NOT process any further requests received on that connection." -- RFC 9112 Section 9.6 + +**RFC 9112 Section 9.6** also recommends the server echo the close option: + +> "The server SHOULD send a 'close' connection option in its final response on that connection." -- RFC 9112 Section 9.6 + +### Chain of Reasoning + +1. The test sends a standard `GET / HTTP/1.1` with `Connection: close`, requesting the server close the connection after responding. +2. The server MUST respond to the request (returning a normal status code such as 2xx) and then close the TCP connection. +3. A server that responds with 2xx but leaves the connection open violates the MUST requirement in Section 9.6. +4. The test validates two things: (a) the server returns a successful response, and (b) the server actually closes the TCP connection afterward. +5. If the server keeps the connection alive, downstream components (especially proxies) may misroute subsequent data, as they expect the connection to be closed. + +### Scoring Justification + +**Scored (MUST).** The RFC uses MUST for connection closure after receiving the `close` option. The expected behavior is a 2xx response followed by TCP connection close. The test validates both the response status and the connection state. A server that responds successfully but does not close the connection fails this test. + +## Sources + +- [RFC 9112 Section 9.6 -- Tear-down](https://www.rfc-editor.org/rfc/rfc9112#section-9.6) +- [RFC 9110 Section 7.6.1 -- Connection](https://www.rfc-editor.org/rfc/rfc9110#section-7.6.1) diff --git a/docs/content/docs/headers/content-type-presence.md b/docs/content/docs/headers/content-type-presence.md index 2777f2f..c4e4363 100644 --- a/docs/content/docs/headers/content-type-presence.md +++ b/docs/content/docs/headers/content-type-presence.md @@ -1,39 +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) +--- +title: "Content Type Presence — HTTP/1.1 Compliance" +description: "A standard GET request. The test validates that the server includes a Content-Type header when the response contains a body. Tested against RFC 9110 §8.3." +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-format.md b/docs/content/docs/headers/date-format.md index 211c51d..21371a9 100644 --- a/docs/content/docs/headers/date-format.md +++ b/docs/content/docs/headers/date-format.md @@ -1,54 +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) +--- +title: "Date Format — HTTP/1.1 Compliance" +description: "Sends a standard GET request and checks whether the Date response header uses the preferred IMF-fixdate format. Tested against RFC 9110 §5.6.7." +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/date-header.md b/docs/content/docs/headers/date-header.md index 45e1f22..12cbc2f 100644 --- a/docs/content/docs/headers/date-header.md +++ b/docs/content/docs/headers/date-header.md @@ -1,35 +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) +--- +title: "Date Header — HTTP/1.1 Compliance" +description: "A standard GET request. The test validates that the server includes a Date header in its response. Tested against RFC 9110 §6.6.1." +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/duplicate-ct.md b/docs/content/docs/headers/duplicate-ct.md index 7a23774..762e79a 100644 --- a/docs/content/docs/headers/duplicate-ct.md +++ b/docs/content/docs/headers/duplicate-ct.md @@ -1,50 +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) +--- +title: "Duplicate Content-Type — HTTP/1.1 Compliance" +description: "A POST request with two Content-Type headers that have conflicting values. Tested against RFC 9110 §5.3." +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/headers/empty-header-name.md b/docs/content/docs/headers/empty-header-name.md index 9ae0bb9..ac4d259 100644 --- a/docs/content/docs/headers/empty-header-name.md +++ b/docs/content/docs/headers/empty-header-name.md @@ -1,89 +1,89 @@ ---- -title: "EMPTY-HEADER-NAME" -description: "EMPTY-HEADER-NAME test documentation" -weight: 3 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-5-EMPTY-HEADER-NAME` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | -| **Requirement** | Implicit MUST (grammar violation) | -| **Expected** | `400` or close | - -## What it sends - -A header line starting with a colon — effectively an empty field name: `: value`. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -: empty-name\r\n -\r\n -``` - -A header line starting with `:` — the header name is empty. - - -## What the RFC says - -The field-line grammar requires a non-empty field name: - -> "field-line = field-name ':' OWS field-value OWS" -- RFC 9112 Section 5 - -> "field-name = token" -- RFC 9110 Section 5.1 - -> "token = 1*tchar" -- RFC 9110 Section 5.6.2 - -Field names are defined as `token = 1*tchar`, requiring **at least one** valid token character. An empty string does not match `1*tchar`. While there is no explicit "MUST reject empty field names with 400" statement, a line starting with `:` fails to match the `field-line` grammar entirely. - -## Why it matters - -A header line with an empty name is structurally ambiguous. Different parsers may treat `: value` as a valid header with an empty name, as a continuation of the previous header, or as garbage. This disagreement between parsers is a classic smuggling precondition. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -field-line = field-name ":" OWS field-value OWS -field-name = token -token = 1*tchar -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" - / "~" / DIGIT / ALPHA -``` - -The critical production is `token = 1*tchar`. The `1*` operator requires at least one `tchar` character. An empty string (zero characters) cannot match this production. - -### RFC Evidence - -**RFC 9112 Section 5** defines the structure of a header field line: - -> "Each field line consists of a case-insensitive field name followed by a colon (':'), optional leading whitespace, the field line value, and optional trailing whitespace." -- RFC 9112 Section 5 - -**RFC 9110 Section 5.6.2** defines the token rule used by field-name: - -> "token = 1*tchar" -- RFC 9110 Section 5.6.2 - -**RFC 9112 Section 5** states the grammar: - -> "field-line = field-name ':' OWS field-value OWS" -- RFC 9112 Section 5 - -### Chain of Reasoning - -1. A line beginning with `:` (e.g., `: empty-name`) has an empty string where `field-name` should appear. -2. The `field-name` production requires `token`, and `token` requires `1*tchar` -- at least one valid token character. -3. An empty string does not match `1*tchar`, so the line fails to parse as a valid `field-line`. -4. A line that does not match the `field-line` grammar is unparseable. The general principle from RFC 9112 Section 2.2 applies: when servers receive malformed requests, they "SHOULD respond with a 400 (Bad Request) response and close the connection." -5. Although there is no explicit "MUST reject empty field names" statement, the grammar violation is unambiguous -- no conformant parser can produce a valid field-name from an empty string. - -### Scoring Justification - -**Scored (implicit MUST, grammar violation).** The requirement is implicit rather than explicit: the ABNF grammar makes it impossible for a conformant parser to accept an empty field name. Both 400 and connection close are acceptable outcomes because the RFC does not prescribe a specific status code for generic grammar violations. The `AllowConnectionClose` flag is set because closing the connection is a reasonable response to an unparseable message. - -## Sources - -- [RFC 9112 Section 5 -- Field Syntax](https://www.rfc-editor.org/rfc/rfc9112#section-5) -- [RFC 9110 Section 5.1 -- Field Names](https://www.rfc-editor.org/rfc/rfc9110#section-5.1) +--- +title: "Empty Header Name — HTTP/1.1 Compliance" +description: "A header line starting with a colon — effectively an empty field name: : value. Tested against RFC 9112 Section 5." +weight: 3 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-5-EMPTY-HEADER-NAME` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | +| **Requirement** | Implicit MUST (grammar violation) | +| **Expected** | `400` or close | + +## What it sends + +A header line starting with a colon — effectively an empty field name: `: value`. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +: empty-name\r\n +\r\n +``` + +A header line starting with `:` — the header name is empty. + + +## What the RFC says + +The field-line grammar requires a non-empty field name: + +> "field-line = field-name ':' OWS field-value OWS" -- RFC 9112 Section 5 + +> "field-name = token" -- RFC 9110 Section 5.1 + +> "token = 1*tchar" -- RFC 9110 Section 5.6.2 + +Field names are defined as `token = 1*tchar`, requiring **at least one** valid token character. An empty string does not match `1*tchar`. While there is no explicit "MUST reject empty field names with 400" statement, a line starting with `:` fails to match the `field-line` grammar entirely. + +## Why it matters + +A header line with an empty name is structurally ambiguous. Different parsers may treat `: value` as a valid header with an empty name, as a continuation of the previous header, or as garbage. This disagreement between parsers is a classic smuggling precondition. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +field-line = field-name ":" OWS field-value OWS +field-name = token +token = 1*tchar +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" + / "~" / DIGIT / ALPHA +``` + +The critical production is `token = 1*tchar`. The `1*` operator requires at least one `tchar` character. An empty string (zero characters) cannot match this production. + +### RFC Evidence + +**RFC 9112 Section 5** defines the structure of a header field line: + +> "Each field line consists of a case-insensitive field name followed by a colon (':'), optional leading whitespace, the field line value, and optional trailing whitespace." -- RFC 9112 Section 5 + +**RFC 9110 Section 5.6.2** defines the token rule used by field-name: + +> "token = 1*tchar" -- RFC 9110 Section 5.6.2 + +**RFC 9112 Section 5** states the grammar: + +> "field-line = field-name ':' OWS field-value OWS" -- RFC 9112 Section 5 + +### Chain of Reasoning + +1. A line beginning with `:` (e.g., `: empty-name`) has an empty string where `field-name` should appear. +2. The `field-name` production requires `token`, and `token` requires `1*tchar` -- at least one valid token character. +3. An empty string does not match `1*tchar`, so the line fails to parse as a valid `field-line`. +4. A line that does not match the `field-line` grammar is unparseable. The general principle from RFC 9112 Section 2.2 applies: when servers receive malformed requests, they "SHOULD respond with a 400 (Bad Request) response and close the connection." +5. Although there is no explicit "MUST reject empty field names" statement, the grammar violation is unambiguous -- no conformant parser can produce a valid field-name from an empty string. + +### Scoring Justification + +**Scored (implicit MUST, grammar violation).** The requirement is implicit rather than explicit: the ABNF grammar makes it impossible for a conformant parser to accept an empty field name. Both 400 and connection close are acceptable outcomes because the RFC does not prescribe a specific status code for generic grammar violations. The `AllowConnectionClose` flag is set because closing the connection is a reasonable response to an unparseable message. + +## Sources + +- [RFC 9112 Section 5 -- Field Syntax](https://www.rfc-editor.org/rfc/rfc9112#section-5) +- [RFC 9110 Section 5.1 -- Field Names](https://www.rfc-editor.org/rfc/rfc9110#section-5.1) diff --git a/docs/content/docs/headers/expect-unknown.md b/docs/content/docs/headers/expect-unknown.md index 4b9a243..1981545 100644 --- a/docs/content/docs/headers/expect-unknown.md +++ b/docs/content/docs/headers/expect-unknown.md @@ -1,82 +1,82 @@ ---- -title: "EXPECT-UNKNOWN" -description: "EXPECT-UNKNOWN test documentation" -weight: 7 ---- - -| | | -|---|---| -| **Test ID** | `COMP-EXPECT-UNKNOWN` | -| **Category** | Compliance | -| **RFC** | [RFC 9110 Section 10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) | -| **Requirement** | MAY respond with 417 | -| **Expected** | `417`; `2xx` is a warning | - -## What it sends - -`Expect: 200-ok` — an Expect header with a value the server cannot fulfill. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Expect: 200-ok\r\n -\r\n -``` - -The `Expect` header contains an unknown expectation value (not `100-continue`). - - -## What the RFC says - -> "The only expectation defined by this specification is '100-continue' (with no defined parameters)." -- RFC 9110 Section 10.1.1 - -> "A server that receives an Expect field value containing a member other than 100-continue MAY respond with a 417 (Expectation Failed) status code to indicate that the unexpected expectation cannot be met." -- RFC 9110 Section 10.1.1 - -The RFC uses "MAY", not "MUST". A `417 Expectation Failed` is the semantically correct response for an unrecognized Expect value, but silently ignoring unknown expectations and processing the request normally is also permitted. - -**Pass:** Server responds with `417 Expectation Failed`. -**Warn:** Server responds with `2xx` (valid per MAY, but less strict). - -## Why it matters - -The Expect mechanism is a contract between client and server. If a server ignores unknown Expect values, clients cannot rely on the mechanism for future extensions. Returning `417` signals clear rejection of unsupported expectations. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -Expect = "100-continue" -``` - -The ABNF for the Expect header field defines only a single valid expectation value: `100-continue`. There is no extensibility mechanism for additional expectation values in the current specification. - -### RFC Evidence - -**RFC 9110 Section 10.1.1** limits the defined expectations: - -> "The only expectation defined by this specification is '100-continue' (with no defined parameters)." -- RFC 9110 Section 10.1.1 - -**RFC 9110 Section 10.1.1** provides the MAY-level guidance for unknown expectations: - -> "A server that receives an Expect field value containing a member other than 100-continue MAY respond with a 417 (Expectation Failed) status code to indicate that the unexpected expectation cannot be met." -- RFC 9110 Section 10.1.1 - -**RFC 9110 Section 10.1.1** mandates server behavior for the known expectation: - -> "A server that receives an Expect header field with a value of 100-continue MUST either respond with a 100 (Continue) status or respond with a final status code." -- RFC 9110 Section 10.1.1 - -### Chain of Reasoning - -1. The test sends `Expect: 200-ok`, which is not `100-continue` and is therefore an unknown expectation. -2. The RFC uses MAY (not MUST) for the 417 response, meaning the server is permitted but not required to reject the request. -3. A server that ignores the unknown expectation and processes the request normally (returning 2xx) is technically compliant with the MAY. -4. However, 417 is the semantically precise response: it tells the client that the expectation cannot be met, which is the truth for any unrecognized value. -5. A server that silently ignores unknown expectations may cause problems for future protocol extensions that rely on the Expect mechanism. - -### Scoring Justification - -**Unscored (MAY).** Since the RFC uses MAY rather than MUST or SHOULD, there is no normative obligation to return 417. The test is therefore unscored: 417 is recorded as Pass (the server actively recognized and rejected the unknown expectation) and 2xx is recorded as Warn (the server ignored the expectation, which is permitted but less informative). No result is recorded as Fail because neither behavior violates the specification. - -## Sources - -- [RFC 9110 Section 10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) +--- +title: "Expect Unknown — HTTP/1.1 Compliance" +description: "Expect: 200-ok — an Expect header with a value the server cannot fulfill. Tested against RFC 9110 Section 10.1.1." +weight: 7 +--- + +| | | +|---|---| +| **Test ID** | `COMP-EXPECT-UNKNOWN` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 Section 10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) | +| **Requirement** | MAY respond with 417 | +| **Expected** | `417`; `2xx` is a warning | + +## What it sends + +`Expect: 200-ok` — an Expect header with a value the server cannot fulfill. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Expect: 200-ok\r\n +\r\n +``` + +The `Expect` header contains an unknown expectation value (not `100-continue`). + + +## What the RFC says + +> "The only expectation defined by this specification is '100-continue' (with no defined parameters)." -- RFC 9110 Section 10.1.1 + +> "A server that receives an Expect field value containing a member other than 100-continue MAY respond with a 417 (Expectation Failed) status code to indicate that the unexpected expectation cannot be met." -- RFC 9110 Section 10.1.1 + +The RFC uses "MAY", not "MUST". A `417 Expectation Failed` is the semantically correct response for an unrecognized Expect value, but silently ignoring unknown expectations and processing the request normally is also permitted. + +**Pass:** Server responds with `417 Expectation Failed`. +**Warn:** Server responds with `2xx` (valid per MAY, but less strict). + +## Why it matters + +The Expect mechanism is a contract between client and server. If a server ignores unknown Expect values, clients cannot rely on the mechanism for future extensions. Returning `417` signals clear rejection of unsupported expectations. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +Expect = "100-continue" +``` + +The ABNF for the Expect header field defines only a single valid expectation value: `100-continue`. There is no extensibility mechanism for additional expectation values in the current specification. + +### RFC Evidence + +**RFC 9110 Section 10.1.1** limits the defined expectations: + +> "The only expectation defined by this specification is '100-continue' (with no defined parameters)." -- RFC 9110 Section 10.1.1 + +**RFC 9110 Section 10.1.1** provides the MAY-level guidance for unknown expectations: + +> "A server that receives an Expect field value containing a member other than 100-continue MAY respond with a 417 (Expectation Failed) status code to indicate that the unexpected expectation cannot be met." -- RFC 9110 Section 10.1.1 + +**RFC 9110 Section 10.1.1** mandates server behavior for the known expectation: + +> "A server that receives an Expect header field with a value of 100-continue MUST either respond with a 100 (Continue) status or respond with a final status code." -- RFC 9110 Section 10.1.1 + +### Chain of Reasoning + +1. The test sends `Expect: 200-ok`, which is not `100-continue` and is therefore an unknown expectation. +2. The RFC uses MAY (not MUST) for the 417 response, meaning the server is permitted but not required to reject the request. +3. A server that ignores the unknown expectation and processes the request normally (returning 2xx) is technically compliant with the MAY. +4. However, 417 is the semantically precise response: it tells the client that the expectation cannot be met, which is the truth for any unrecognized value. +5. A server that silently ignores unknown expectations may cause problems for future protocol extensions that rely on the Expect mechanism. + +### Scoring Justification + +**Unscored (MAY).** Since the RFC uses MAY rather than MUST or SHOULD, there is no normative obligation to return 417. The test is therefore unscored: 417 is recorded as Pass (the server actively recognized and rejected the unknown expectation) and 2xx is recorded as Warn (the server ignored the expectation, which is permitted but less informative). No result is recorded as Fail because neither behavior violates the specification. + +## Sources + +- [RFC 9110 Section 10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) diff --git a/docs/content/docs/headers/header-no-colon.md b/docs/content/docs/headers/header-no-colon.md index 3031c88..2b14343 100644 --- a/docs/content/docs/headers/header-no-colon.md +++ b/docs/content/docs/headers/header-no-colon.md @@ -1,83 +1,83 @@ ---- -title: "HEADER-NO-COLON" -description: "HEADER-NO-COLON test documentation" -weight: 5 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-5-HEADER-NO-COLON` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | -| **Requirement** | Implicit MUST (grammar violation) | -| **Expected** | `400` or close | - -## What it sends - -A header line with no colon: `InvalidHeaderNoColon`. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -NoColonHere\r\n -\r\n -``` - -A header line without any colon separator. - - -## What the RFC says - -> "field-line = field-name ':' OWS field-value OWS" -- RFC 9112 Section 5 - -> "Each field line consists of a case-insensitive field name followed by a colon (':'), optional leading whitespace, the field line value, and optional trailing whitespace." -- RFC 9112 Section 5 - -A line without a colon does not match the `field-line` grammar. It could be misinterpreted as a continuation line, a new request, or garbage -- any of which is dangerous. - -## Why it matters - -A header line without a colon is structurally ambiguous. Some parsers may treat it as a malformed header and discard it, while others may interpret it as a continuation of the previous header value (obs-fold behavior) or even as the start of a new request. This parsing disagreement between components in a request chain is exactly the condition attackers exploit for request smuggling. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -HTTP-message = start-line CRLF - *( field-line CRLF ) - CRLF - [ message-body ] -field-line = field-name ":" OWS field-value OWS -``` - -The `field-line` production requires a colon as a mandatory separator between `field-name` and `field-value`. Without a colon, the line cannot be parsed as a field-line. - -### RFC Evidence - -**RFC 9112 Section 5** describes the structure clearly: - -> "Each field line consists of a case-insensitive field name followed by a colon (':'), optional leading whitespace, the field line value, and optional trailing whitespace." -- RFC 9112 Section 5 - -**RFC 9112 Section 5** provides the formal grammar: - -> "field-line = field-name ':' OWS field-value OWS" -- RFC 9112 Section 5 - -**RFC 9112 Section 2.2** establishes the general error handling principle: - -> "A recipient that receives whitespace between the start-line and the first header field MUST either reject the message as invalid or consume each whitespace-preceded line without further processing of it." -- RFC 9112 Section 2.2 - -### Chain of Reasoning - -1. A line like `NoColonHere` in the header section does not contain a colon, so it cannot match the `field-line` production. -2. Without a colon, the parser cannot determine where the field name ends and the field value begins. -3. The line could be misinterpreted in multiple dangerous ways: as a continuation of the previous header (similar to obs-fold), as the start of a new request (if it looks like a request-line), or as part of the message body. -4. This ambiguity is precisely the kind of parsing disagreement that enables request smuggling. If one component treats it as a header and another treats it as body content, the message boundaries diverge. -5. RFC 9112 Section 2.2 states that when a server receives a malformed request, it "SHOULD respond with a 400 (Bad Request) response and close the connection." - -### Scoring Justification - -**Scored (implicit MUST, grammar violation).** The colon is a mandatory syntactic element in `field-line`. Its absence is an unambiguous grammar violation. Both 400 and connection close are acceptable responses because no specific status code is mandated for generic parse failures. The `AllowConnectionClose` flag is set because the message structure is so fundamentally broken that the server may not be able to generate a well-formed response. - -## Sources - -- [RFC 9112 Section 5 -- Field Syntax](https://www.rfc-editor.org/rfc/rfc9112#section-5) +--- +title: "Header No Colon — HTTP/1.1 Compliance" +description: "A header line with no colon: InvalidHeaderNoColon. Tested against RFC 9112 Section 5." +weight: 5 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-5-HEADER-NO-COLON` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | +| **Requirement** | Implicit MUST (grammar violation) | +| **Expected** | `400` or close | + +## What it sends + +A header line with no colon: `InvalidHeaderNoColon`. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +NoColonHere\r\n +\r\n +``` + +A header line without any colon separator. + + +## What the RFC says + +> "field-line = field-name ':' OWS field-value OWS" -- RFC 9112 Section 5 + +> "Each field line consists of a case-insensitive field name followed by a colon (':'), optional leading whitespace, the field line value, and optional trailing whitespace." -- RFC 9112 Section 5 + +A line without a colon does not match the `field-line` grammar. It could be misinterpreted as a continuation line, a new request, or garbage -- any of which is dangerous. + +## Why it matters + +A header line without a colon is structurally ambiguous. Some parsers may treat it as a malformed header and discard it, while others may interpret it as a continuation of the previous header value (obs-fold behavior) or even as the start of a new request. This parsing disagreement between components in a request chain is exactly the condition attackers exploit for request smuggling. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +HTTP-message = start-line CRLF + *( field-line CRLF ) + CRLF + [ message-body ] +field-line = field-name ":" OWS field-value OWS +``` + +The `field-line` production requires a colon as a mandatory separator between `field-name` and `field-value`. Without a colon, the line cannot be parsed as a field-line. + +### RFC Evidence + +**RFC 9112 Section 5** describes the structure clearly: + +> "Each field line consists of a case-insensitive field name followed by a colon (':'), optional leading whitespace, the field line value, and optional trailing whitespace." -- RFC 9112 Section 5 + +**RFC 9112 Section 5** provides the formal grammar: + +> "field-line = field-name ':' OWS field-value OWS" -- RFC 9112 Section 5 + +**RFC 9112 Section 2.2** establishes the general error handling principle: + +> "A recipient that receives whitespace between the start-line and the first header field MUST either reject the message as invalid or consume each whitespace-preceded line without further processing of it." -- RFC 9112 Section 2.2 + +### Chain of Reasoning + +1. A line like `NoColonHere` in the header section does not contain a colon, so it cannot match the `field-line` production. +2. Without a colon, the parser cannot determine where the field name ends and the field value begins. +3. The line could be misinterpreted in multiple dangerous ways: as a continuation of the previous header (similar to obs-fold), as the start of a new request (if it looks like a request-line), or as part of the message body. +4. This ambiguity is precisely the kind of parsing disagreement that enables request smuggling. If one component treats it as a header and another treats it as body content, the message boundaries diverge. +5. RFC 9112 Section 2.2 states that when a server receives a malformed request, it "SHOULD respond with a 400 (Bad Request) response and close the connection." + +### Scoring Justification + +**Scored (implicit MUST, grammar violation).** The colon is a mandatory syntactic element in `field-line`. Its absence is an unambiguous grammar violation. Both 400 and connection close are acceptable responses because no specific status code is mandated for generic parse failures. The `AllowConnectionClose` flag is set because the message structure is so fundamentally broken that the server may not be able to generate a well-formed response. + +## Sources + +- [RFC 9112 Section 5 -- Field Syntax](https://www.rfc-editor.org/rfc/rfc9112#section-5) diff --git a/docs/content/docs/headers/http10-default-close.md b/docs/content/docs/headers/http10-default-close.md index 3cb9165..b889938 100644 --- a/docs/content/docs/headers/http10-default-close.md +++ b/docs/content/docs/headers/http10-default-close.md @@ -1,81 +1,81 @@ ---- -title: "HTTP10-DEFAULT-CLOSE" -description: "HTTP10-DEFAULT-CLOSE test documentation" -weight: 7 ---- - -| | | -|---|---| -| **Test ID** | `COMP-HTTP10-DEFAULT-CLOSE` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) | -| **Requirement** | SHOULD | -| **Expected** | `2xx` + connection closed | - -## What it sends - -An HTTP/1.0 GET request without a `Connection: keep-alive` header. - -```http -GET / HTTP/1.0\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -HTTP/1.0 connections are not persistent by default. Unlike HTTP/1.1, where persistent connections are the default, an HTTP/1.0 client must explicitly request persistence via `Connection: keep-alive`. - -The RFC defines persistence rules by precedence. For HTTP/1.0 without `keep-alive`, the final rule applies: - -> "If the 'close' connection option is present (Section 9.6), the connection will not persist after the current response; else, if the received protocol is HTTP/1.1 (or later), the connection will persist after the current response; else, if the received protocol is HTTP/1.0, the 'keep-alive' connection option is present, either the recipient is not a proxy or the message is a response, and the recipient wishes to honor the HTTP/1.0 'keep-alive' mechanism, the connection will persist after the current response; otherwise, the connection will close after the current response." -- RFC 9112 Section 9.3 - -Without `Connection: keep-alive`, the server should treat the connection as non-persistent and close it after delivering the response. - -**Pass:** Server responds `2xx` and closes the connection. -**Warn:** Server responds `2xx` but keeps the connection open (minor violation of SHOULD). - -## Why it matters - -If a server treats HTTP/1.0 connections as persistent by default, it may hold the connection open indefinitely waiting for another request that will never come, wasting resources. More critically, in proxy chains, a downstream server keeping an HTTP/1.0 connection alive when the proxy expects it to close can cause response desynchronization. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -HTTP-version = HTTP-name "/" DIGIT "." DIGIT -HTTP-name = %x48.54.54.50 ; "HTTP" -``` - -An HTTP/1.0 request uses `HTTP/1.0` as the version token. The persistence behavior is not governed by ABNF but by the protocol rules in RFC 9112 Section 9.3. - -### RFC Evidence - -**RFC 9112 Section 9.3** establishes the default persistence model: - -> "HTTP/1.1 defaults to the use of 'persistent connections', allowing multiple requests and responses to be carried over a single connection." -- RFC 9112 Section 9.3 - -**RFC 9112 Section 9.3** defines the persistence precedence rules, with the final fallback: - -> "If the 'close' connection option is present, the connection will not persist after the current response; else, if the received protocol is HTTP/1.1 (or later), the connection will persist after the current response; else, if the received protocol is HTTP/1.0, the 'keep-alive' connection option is present...the connection will persist after the current response; otherwise, the connection will close after the current response." -- RFC 9112 Section 9.3 - -**RFC 9112 Section 9.3** mandates proxy behavior: - -> "A proxy server MUST NOT maintain a persistent connection with an HTTP/1.0 client." -- RFC 9112 Section 9.3 - -### Chain of Reasoning - -1. The test sends an HTTP/1.0 request without `Connection: keep-alive`. -2. Following the persistence precedence in Section 9.3: the `close` option is not present, the protocol is not HTTP/1.1, and `keep-alive` is not present, so the final rule applies -- "the connection will close after the current response." -3. The server should respond normally (2xx) and then close the TCP connection. -4. This is a SHOULD-level behavior because the RFC does not use explicit MUST language for the final fallback rule. The persistence determination is described as a set of conditions rather than a MUST directive. -5. A server that keeps the connection open after an HTTP/1.0 request without keep-alive is wasting resources and may cause proxy desynchronization. - -### Scoring Justification - -**Scored (SHOULD).** The persistence rules in Section 9.3 describe expected behavior without explicit MUST language for the HTTP/1.0 fallback case. Pass is recorded when the server responds with 2xx and closes the connection. Warn is recorded when the server responds with 2xx but keeps the connection open, as this is a minor violation of the expected default behavior rather than a hard protocol error. - -## Sources - -- [RFC 9112 Section 9.3 -- Persistence](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) +--- +title: "HTTP/1.0 Default Close — HTTP/1.1 Compliance" +description: "An HTTP/1.0 GET request without a Connection: keep-alive header. Tested against RFC 9112 §9.3." +weight: 7 +--- + +| | | +|---|---| +| **Test ID** | `COMP-HTTP10-DEFAULT-CLOSE` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) | +| **Requirement** | SHOULD | +| **Expected** | `2xx` + connection closed | + +## What it sends + +An HTTP/1.0 GET request without a `Connection: keep-alive` header. + +```http +GET / HTTP/1.0\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +HTTP/1.0 connections are not persistent by default. Unlike HTTP/1.1, where persistent connections are the default, an HTTP/1.0 client must explicitly request persistence via `Connection: keep-alive`. + +The RFC defines persistence rules by precedence. For HTTP/1.0 without `keep-alive`, the final rule applies: + +> "If the 'close' connection option is present (Section 9.6), the connection will not persist after the current response; else, if the received protocol is HTTP/1.1 (or later), the connection will persist after the current response; else, if the received protocol is HTTP/1.0, the 'keep-alive' connection option is present, either the recipient is not a proxy or the message is a response, and the recipient wishes to honor the HTTP/1.0 'keep-alive' mechanism, the connection will persist after the current response; otherwise, the connection will close after the current response." -- RFC 9112 Section 9.3 + +Without `Connection: keep-alive`, the server should treat the connection as non-persistent and close it after delivering the response. + +**Pass:** Server responds `2xx` and closes the connection. +**Warn:** Server responds `2xx` but keeps the connection open (minor violation of SHOULD). + +## Why it matters + +If a server treats HTTP/1.0 connections as persistent by default, it may hold the connection open indefinitely waiting for another request that will never come, wasting resources. More critically, in proxy chains, a downstream server keeping an HTTP/1.0 connection alive when the proxy expects it to close can cause response desynchronization. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +HTTP-version = HTTP-name "/" DIGIT "." DIGIT +HTTP-name = %x48.54.54.50 ; "HTTP" +``` + +An HTTP/1.0 request uses `HTTP/1.0` as the version token. The persistence behavior is not governed by ABNF but by the protocol rules in RFC 9112 Section 9.3. + +### RFC Evidence + +**RFC 9112 Section 9.3** establishes the default persistence model: + +> "HTTP/1.1 defaults to the use of 'persistent connections', allowing multiple requests and responses to be carried over a single connection." -- RFC 9112 Section 9.3 + +**RFC 9112 Section 9.3** defines the persistence precedence rules, with the final fallback: + +> "If the 'close' connection option is present, the connection will not persist after the current response; else, if the received protocol is HTTP/1.1 (or later), the connection will persist after the current response; else, if the received protocol is HTTP/1.0, the 'keep-alive' connection option is present...the connection will persist after the current response; otherwise, the connection will close after the current response." -- RFC 9112 Section 9.3 + +**RFC 9112 Section 9.3** mandates proxy behavior: + +> "A proxy server MUST NOT maintain a persistent connection with an HTTP/1.0 client." -- RFC 9112 Section 9.3 + +### Chain of Reasoning + +1. The test sends an HTTP/1.0 request without `Connection: keep-alive`. +2. Following the persistence precedence in Section 9.3: the `close` option is not present, the protocol is not HTTP/1.1, and `keep-alive` is not present, so the final rule applies -- "the connection will close after the current response." +3. The server should respond normally (2xx) and then close the TCP connection. +4. This is a SHOULD-level behavior because the RFC does not use explicit MUST language for the final fallback rule. The persistence determination is described as a set of conditions rather than a MUST directive. +5. A server that keeps the connection open after an HTTP/1.0 request without keep-alive is wasting resources and may cause proxy desynchronization. + +### Scoring Justification + +**Scored (SHOULD).** The persistence rules in Section 9.3 describe expected behavior without explicit MUST language for the HTTP/1.0 fallback case. Pass is recorded when the server responds with 2xx and closes the connection. Warn is recorded when the server responds with 2xx but keeps the connection open, as this is a minor violation of the expected default behavior rather than a hard protocol error. + +## Sources + +- [RFC 9112 Section 9.3 -- Persistence](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) diff --git a/docs/content/docs/headers/invalid-header-name.md b/docs/content/docs/headers/invalid-header-name.md index 7b4e377..a95a9c2 100644 --- a/docs/content/docs/headers/invalid-header-name.md +++ b/docs/content/docs/headers/invalid-header-name.md @@ -1,86 +1,86 @@ ---- -title: "INVALID-HEADER-NAME" -description: "INVALID-HEADER-NAME test documentation" -weight: 4 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-5-INVALID-HEADER-NAME` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | -| **Requirement** | Implicit MUST (grammar violation) | -| **Expected** | `400` or close | - -## What it sends - -A header with non-token characters in the field name (e.g., characters outside the `tchar` set defined in RFC 9110 Section 5.6.2). - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Bad[Name: value\r\n -\r\n -``` - -The header name contains `[` which is not a valid token character. - - -## What the RFC says - -> "field-name = token" -- RFC 9110 Section 5.1 - -> "token = 1*tchar" -- RFC 9110 Section 5.6.2 - -> "tchar = '!' / '#' / '$' / '%' / '&' / ''' / '*' / '+' / '-' / '.' / '^' / '_' / '`' / '|' / '~' / DIGIT / ALPHA ; any VCHAR, except delimiters" -- RFC 9110 Section 5.6.2 - -The `[` character in `Bad[Name` is not in the `tchar` set, so the field name violates the grammar. Characters outside this set in a field name make the header line unparseable as a valid `field-line`. - -## Why it matters - -If a server accepts non-token characters in field names, it may interpret a header differently from other components in the request chain. Delimiter characters like `[`, `]`, `{`, `}`, or `@` in field names could cause parsing divergence between the server and upstream proxies. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -field-line = field-name ":" OWS field-value OWS -field-name = token -token = 1*tchar -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" - / "~" / DIGIT / ALPHA -``` - -The `tchar` set is a closed enumeration. Characters outside this set -- including `[`, `]`, `{`, `}`, `(`, `)`, `<`, `>`, `@`, `,`, `;`, `\`, `"`, `/`, `?`, `=`, and SP -- are delimiters or otherwise excluded. - -### RFC Evidence - -**RFC 9110 Section 5.1** ties field names to the token production: - -> "field-name = token" -- RFC 9110 Section 5.1 - -**RFC 9110 Section 5.6.2** defines token as exclusively composed of tchar: - -> "token = 1*tchar" -- RFC 9110 Section 5.6.2 - -**RFC 9110 Section 5.6.2** enumerates the allowed characters: - -> "tchar = '!' / '#' / '$' / '%' / '&' / ''' / '*' / '+' / '-' / '.' / '^' / '_' / '`' / '|' / '~' / DIGIT / ALPHA ; any VCHAR, except delimiters" -- RFC 9110 Section 5.6.2 - -### Chain of Reasoning - -1. The header name `Bad[Name` contains `[`, which is not in the `tchar` set. -2. Since `field-name = token = 1*tchar`, a field name containing non-tchar characters fails to match the grammar. -3. A line that does not match `field-line` is a malformed message element. RFC 9112 Section 2.2 states that when servers receive malformed requests, they "SHOULD respond with a 400 (Bad Request) response and close the connection." -4. The violation is structural -- the parser encounters a character that cannot appear in a field name, making the rest of the line ambiguous (is the `[` part of the name? a delimiter? the start of something else?). - -### Scoring Justification - -**Scored (implicit MUST, grammar violation).** Like EMPTY-HEADER-NAME, this is a grammar violation rather than an explicitly stated MUST. The ABNF is unambiguous -- `[` is not a `tchar` -- but no specific status code is mandated for this class of error. Both 400 and connection close are acceptable, so `AllowConnectionClose` is set. A server that processes the request normally (2xx) is accepting input that violates the grammar, which is a compliance failure. - -## Sources - -- [RFC 9112 Section 5 -- Field Syntax](https://www.rfc-editor.org/rfc/rfc9112#section-5) -- [RFC 9110 Section 5.6.2 -- Tokens](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.2) +--- +title: "Invalid Header Name — HTTP/1.1 Compliance" +description: "A header with non-token characters in the field name (e.g., characters outside the tchar set defined in RFC 9110 Section 5.6.2). Tested against RFC 9112 Section 5." +weight: 4 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-5-INVALID-HEADER-NAME` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | +| **Requirement** | Implicit MUST (grammar violation) | +| **Expected** | `400` or close | + +## What it sends + +A header with non-token characters in the field name (e.g., characters outside the `tchar` set defined in RFC 9110 Section 5.6.2). + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Bad[Name: value\r\n +\r\n +``` + +The header name contains `[` which is not a valid token character. + + +## What the RFC says + +> "field-name = token" -- RFC 9110 Section 5.1 + +> "token = 1*tchar" -- RFC 9110 Section 5.6.2 + +> "tchar = '!' / '#' / '$' / '%' / '&' / ''' / '*' / '+' / '-' / '.' / '^' / '_' / '`' / '|' / '~' / DIGIT / ALPHA ; any VCHAR, except delimiters" -- RFC 9110 Section 5.6.2 + +The `[` character in `Bad[Name` is not in the `tchar` set, so the field name violates the grammar. Characters outside this set in a field name make the header line unparseable as a valid `field-line`. + +## Why it matters + +If a server accepts non-token characters in field names, it may interpret a header differently from other components in the request chain. Delimiter characters like `[`, `]`, `{`, `}`, or `@` in field names could cause parsing divergence between the server and upstream proxies. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +field-line = field-name ":" OWS field-value OWS +field-name = token +token = 1*tchar +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" + / "~" / DIGIT / ALPHA +``` + +The `tchar` set is a closed enumeration. Characters outside this set -- including `[`, `]`, `{`, `}`, `(`, `)`, `<`, `>`, `@`, `,`, `;`, `\`, `"`, `/`, `?`, `=`, and SP -- are delimiters or otherwise excluded. + +### RFC Evidence + +**RFC 9110 Section 5.1** ties field names to the token production: + +> "field-name = token" -- RFC 9110 Section 5.1 + +**RFC 9110 Section 5.6.2** defines token as exclusively composed of tchar: + +> "token = 1*tchar" -- RFC 9110 Section 5.6.2 + +**RFC 9110 Section 5.6.2** enumerates the allowed characters: + +> "tchar = '!' / '#' / '$' / '%' / '&' / ''' / '*' / '+' / '-' / '.' / '^' / '_' / '`' / '|' / '~' / DIGIT / ALPHA ; any VCHAR, except delimiters" -- RFC 9110 Section 5.6.2 + +### Chain of Reasoning + +1. The header name `Bad[Name` contains `[`, which is not in the `tchar` set. +2. Since `field-name = token = 1*tchar`, a field name containing non-tchar characters fails to match the grammar. +3. A line that does not match `field-line` is a malformed message element. RFC 9112 Section 2.2 states that when servers receive malformed requests, they "SHOULD respond with a 400 (Bad Request) response and close the connection." +4. The violation is structural -- the parser encounters a character that cannot appear in a field name, making the rest of the line ambiguous (is the `[` part of the name? a delimiter? the start of something else?). + +### Scoring Justification + +**Scored (implicit MUST, grammar violation).** Like EMPTY-HEADER-NAME, this is a grammar violation rather than an explicitly stated MUST. The ABNF is unambiguous -- `[` is not a `tchar` -- but no specific status code is mandated for this class of error. Both 400 and connection close are acceptable, so `AllowConnectionClose` is set. A server that processes the request normally (2xx) is accepting input that violates the grammar, which is a compliance failure. + +## Sources + +- [RFC 9112 Section 5 -- Field Syntax](https://www.rfc-editor.org/rfc/rfc9112#section-5) +- [RFC 9110 Section 5.6.2 -- Tokens](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.2) diff --git a/docs/content/docs/headers/no-1xx-http10.md b/docs/content/docs/headers/no-1xx-http10.md index 6206f86..a506061 100644 --- a/docs/content/docs/headers/no-1xx-http10.md +++ b/docs/content/docs/headers/no-1xx-http10.md @@ -1,38 +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) +--- +title: "No 1xx HTTP/1.0 — HTTP/1.1 Compliance" +description: "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. Tested against RFC 9110 §15.2." +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/headers/obs-fold.md b/docs/content/docs/headers/obs-fold.md index 247ab2b..ccdf9be 100644 --- a/docs/content/docs/headers/obs-fold.md +++ b/docs/content/docs/headers/obs-fold.md @@ -1,79 +1,79 @@ ---- -title: "OBS-FOLD" -description: "OBS-FOLD test documentation" -weight: 2 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-5.1-OBS-FOLD` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 5.2](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) | -| **Requirement** | MUST | -| **Expected** | `400` | - -## What it sends - -A request with an obsolete line-folded header value — a continuation line that starts with whitespace: - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -X-Test: value\r\n - continued\r\n -\r\n -``` - -The `X-Test` header value is split across two lines. The second line starts with a space (obs-fold / line folding). - - -## What the RFC says - -> "A server that receives an obs-fold in a request message that is not within a 'message/http' container MUST either reject the message by sending a 400 (Bad Request), preferably with a representation explaining that obsolete line folding is unacceptable, or replace each received obs-fold with one or more SP octets prior to interpreting the field value or forwarding the message downstream." -- RFC 9112 Section 5.2 - -This is a MUST with two alternatives: send 400 or silently fix it. Http11Probe scores rejection (400) as a pass because it's the stricter option. - -## Why it matters - -Obs-fold creates ambiguity: is the continuation line part of the previous header's value, or a new header/request? Different parsers may disagree, creating a smuggling vector. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -field-line = field-name ":" OWS field-value OWS -obs-fold = OWS CRLF RWS - ; obsolete line folding -``` - -The `obs-fold` production allows a field value to span multiple lines by following a CRLF with required whitespace (RWS -- at least one SP or HTAB). This was permitted in older HTTP versions but is now deprecated. - -### RFC Evidence - -**RFC 9112 Section 5.2** establishes the sender prohibition: - -> "A sender MUST NOT generate a message that includes line folding (i.e., that has any field line value that contains a match to the obs-fold rule) unless the message is intended for packaging within the 'message/http' media type." -- RFC 9112 Section 5.2 - -**RFC 9112 Section 5.2** mandates two server alternatives: - -> "A server that receives an obs-fold in a request message that is not within a 'message/http' container MUST either reject the message by sending a 400 (Bad Request), preferably with a representation explaining that obsolete line folding is unacceptable, or replace each received obs-fold with one or more SP octets prior to interpreting the field value or forwarding the message downstream." -- RFC 9112 Section 5.2 - -**RFC 9112 Section 5.2** also governs intermediaries: - -> "A proxy or gateway that receives an obs-fold in a response message that is not within a 'message/http' container MUST either discard the message and replace it with a 502 (Bad Gateway) response...or replace each received obs-fold with one or more SP octets prior to interpreting the field value or forwarding the message downstream." -- RFC 9112 Section 5.2 - -### Chain of Reasoning - -1. The `obs-fold` rule allows a CRLF followed by whitespace to appear within a field value, making one logical header span multiple lines. -2. Different parsers may disagree on whether the continuation line is part of the previous header value or the start of a new header (or even a new request). This ambiguity is a smuggling vector. -3. The RFC offers servers two MUST-level alternatives: reject with 400, or normalize by replacing obs-fold with SP. Both are compliant. -4. Http11Probe scores 400 as Pass because rejection is the stricter and safer option. A server that normalizes obs-fold would need to be tested differently (by verifying the resulting field value), which is outside the scope of this probe. - -### Scoring Justification - -**Scored (MUST).** Although the RFC provides two compliant alternatives (reject with 400 or replace with SP), Http11Probe can only observe the rejection path from the outside. A 400 response demonstrates the server recognized and rejected the deprecated syntax. A 2xx response is ambiguous -- it could mean the server correctly normalized the obs-fold, or it could mean the server blindly accepted malformed input. Since the probe cannot distinguish these cases, 400 is scored as Pass. - -## Sources - -- [RFC 9112 Section 5.2 -- Obsolete Line Folding](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) +--- +title: "Obs Fold — HTTP/1.1 Compliance" +description: "A request with an obsolete line-folded header value — a continuation line that starts with whitespace:. Tested against RFC 9112 Section 5.2." +weight: 2 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-5.1-OBS-FOLD` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 5.2](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) | +| **Requirement** | MUST | +| **Expected** | `400` | + +## What it sends + +A request with an obsolete line-folded header value — a continuation line that starts with whitespace: + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +X-Test: value\r\n + continued\r\n +\r\n +``` + +The `X-Test` header value is split across two lines. The second line starts with a space (obs-fold / line folding). + + +## What the RFC says + +> "A server that receives an obs-fold in a request message that is not within a 'message/http' container MUST either reject the message by sending a 400 (Bad Request), preferably with a representation explaining that obsolete line folding is unacceptable, or replace each received obs-fold with one or more SP octets prior to interpreting the field value or forwarding the message downstream." -- RFC 9112 Section 5.2 + +This is a MUST with two alternatives: send 400 or silently fix it. Http11Probe scores rejection (400) as a pass because it's the stricter option. + +## Why it matters + +Obs-fold creates ambiguity: is the continuation line part of the previous header's value, or a new header/request? Different parsers may disagree, creating a smuggling vector. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +field-line = field-name ":" OWS field-value OWS +obs-fold = OWS CRLF RWS + ; obsolete line folding +``` + +The `obs-fold` production allows a field value to span multiple lines by following a CRLF with required whitespace (RWS -- at least one SP or HTAB). This was permitted in older HTTP versions but is now deprecated. + +### RFC Evidence + +**RFC 9112 Section 5.2** establishes the sender prohibition: + +> "A sender MUST NOT generate a message that includes line folding (i.e., that has any field line value that contains a match to the obs-fold rule) unless the message is intended for packaging within the 'message/http' media type." -- RFC 9112 Section 5.2 + +**RFC 9112 Section 5.2** mandates two server alternatives: + +> "A server that receives an obs-fold in a request message that is not within a 'message/http' container MUST either reject the message by sending a 400 (Bad Request), preferably with a representation explaining that obsolete line folding is unacceptable, or replace each received obs-fold with one or more SP octets prior to interpreting the field value or forwarding the message downstream." -- RFC 9112 Section 5.2 + +**RFC 9112 Section 5.2** also governs intermediaries: + +> "A proxy or gateway that receives an obs-fold in a response message that is not within a 'message/http' container MUST either discard the message and replace it with a 502 (Bad Gateway) response...or replace each received obs-fold with one or more SP octets prior to interpreting the field value or forwarding the message downstream." -- RFC 9112 Section 5.2 + +### Chain of Reasoning + +1. The `obs-fold` rule allows a CRLF followed by whitespace to appear within a field value, making one logical header span multiple lines. +2. Different parsers may disagree on whether the continuation line is part of the previous header value or the start of a new header (or even a new request). This ambiguity is a smuggling vector. +3. The RFC offers servers two MUST-level alternatives: reject with 400, or normalize by replacing obs-fold with SP. Both are compliant. +4. Http11Probe scores 400 as Pass because rejection is the stricter and safer option. A server that normalizes obs-fold would need to be tested differently (by verifying the resulting field value), which is outside the scope of this probe. + +### Scoring Justification + +**Scored (MUST).** Although the RFC provides two compliant alternatives (reject with 400 or replace with SP), Http11Probe can only observe the rejection path from the outside. A 400 response demonstrates the server recognized and rejected the deprecated syntax. A 2xx response is ambiguous -- it could mean the server correctly normalized the obs-fold, or it could mean the server blindly accepted malformed input. Since the probe cannot distinguish these cases, 400 is scored as Pass. + +## Sources + +- [RFC 9112 Section 5.2 -- Obsolete Line Folding](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) diff --git a/docs/content/docs/headers/sp-before-colon.md b/docs/content/docs/headers/sp-before-colon.md index d3421e9..4245380 100644 --- a/docs/content/docs/headers/sp-before-colon.md +++ b/docs/content/docs/headers/sp-before-colon.md @@ -1,81 +1,81 @@ ---- -title: "SP-BEFORE-COLON" -description: "SP-BEFORE-COLON test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `RFC9110-5.6.2-SP-BEFORE-COLON` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | -| **Requirement** | MUST | -| **Expected** | `400` | - -## What it sends - -A request with a space between the header field name and the colon: `Host : localhost`. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -X-Test : value\r\n -\r\n -``` - -Note the space between `X-Test` and the colon. - - -## What the RFC says - -> "No whitespace is allowed between the field name and colon. In the past, differences in the handling of such whitespace have led to security vulnerabilities in request routing and response handling. A server MUST reject, with a response status code of 400 (Bad Request), any received request message that contains whitespace between a header field name and colon." -- RFC 9112 Section 5.1 - -This is one of the strongest requirements in the HTTP spec -- **MUST reject with 400 specifically**. Not close, not 500 -- exactly 400. - -## Why it matters - -This requirement was added specifically because of real-world security vulnerabilities. When different parsers handle `Header : value` vs `Header: value` differently, attackers can craft requests that are interpreted as having different headers by different components. - -The `Transfer-Encoding` smuggling variant (`Transfer-Encoding : chunked`) exploits exactly this. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -field-line = field-name ":" OWS field-value OWS -field-name = token -token = 1*tchar -``` - -The colon immediately follows `field-name` with no intervening whitespace permitted by the grammar. The optional whitespace (OWS) is only allowed *after* the colon, between `":"` and `field-value`. - -### RFC Evidence - -**RFC 9112 Section 5.1** provides the definitive prohibition: - -> "No whitespace is allowed between the field name and colon. In the past, differences in the handling of such whitespace have led to security vulnerabilities in request routing and response handling. A server MUST reject, with a response status code of 400 (Bad Request), any received request message that contains whitespace between a header field name and colon." -- RFC 9112 Section 5.1 - -**RFC 9112 Section 5.1** also mandates proxy behavior: - -> "A proxy MUST remove any such whitespace from a response message before forwarding the message downstream." -- RFC 9112 Section 5.1 - -**RFC 9112 Section 2.2** reinforces the broader parsing principle: - -> "A sender MUST NOT send whitespace between the start-line and the first header field." -- RFC 9112 Section 2.2 - -### Chain of Reasoning - -1. The `field-line` ABNF grammar places the colon immediately after `field-name` with zero intervening characters. -2. The RFC explicitly calls out that past differences in handling this whitespace caused real security vulnerabilities. -3. The requirement is MUST reject with specifically 400 -- not close, not 500, not any other status code. This is one of the most prescriptive requirements in the entire HTTP specification. -4. The RFC chose to require 400 specifically (rather than allowing connection close as an alternative) because the security implications demand an unambiguous signal to the client. - -### Scoring Justification - -**Scored (MUST).** The RFC mandates exactly one acceptable server behavior: respond with 400 (Bad Request). There is no alternative disposition such as "or close the connection." Connection close alone does not satisfy this requirement because the RFC explicitly specifies the status code. A server that closes the connection without sending 400 is non-compliant. - -## Sources - -- [RFC 9112 Section 5 -- Field Syntax](https://www.rfc-editor.org/rfc/rfc9112#section-5) -- [RFC 9110 Section 16.3.1 -- Request Smuggling](https://www.rfc-editor.org/rfc/rfc9110#section-16.3.1) +--- +title: "Space Before Colon — HTTP/1.1 Compliance" +description: "A request with a space between the header field name and the colon: Host : localhost. Tested against RFC 9112 Section 5." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `RFC9110-5.6.2-SP-BEFORE-COLON` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | +| **Requirement** | MUST | +| **Expected** | `400` | + +## What it sends + +A request with a space between the header field name and the colon: `Host : localhost`. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +X-Test : value\r\n +\r\n +``` + +Note the space between `X-Test` and the colon. + + +## What the RFC says + +> "No whitespace is allowed between the field name and colon. In the past, differences in the handling of such whitespace have led to security vulnerabilities in request routing and response handling. A server MUST reject, with a response status code of 400 (Bad Request), any received request message that contains whitespace between a header field name and colon." -- RFC 9112 Section 5.1 + +This is one of the strongest requirements in the HTTP spec -- **MUST reject with 400 specifically**. Not close, not 500 -- exactly 400. + +## Why it matters + +This requirement was added specifically because of real-world security vulnerabilities. When different parsers handle `Header : value` vs `Header: value` differently, attackers can craft requests that are interpreted as having different headers by different components. + +The `Transfer-Encoding` smuggling variant (`Transfer-Encoding : chunked`) exploits exactly this. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +field-line = field-name ":" OWS field-value OWS +field-name = token +token = 1*tchar +``` + +The colon immediately follows `field-name` with no intervening whitespace permitted by the grammar. The optional whitespace (OWS) is only allowed *after* the colon, between `":"` and `field-value`. + +### RFC Evidence + +**RFC 9112 Section 5.1** provides the definitive prohibition: + +> "No whitespace is allowed between the field name and colon. In the past, differences in the handling of such whitespace have led to security vulnerabilities in request routing and response handling. A server MUST reject, with a response status code of 400 (Bad Request), any received request message that contains whitespace between a header field name and colon." -- RFC 9112 Section 5.1 + +**RFC 9112 Section 5.1** also mandates proxy behavior: + +> "A proxy MUST remove any such whitespace from a response message before forwarding the message downstream." -- RFC 9112 Section 5.1 + +**RFC 9112 Section 2.2** reinforces the broader parsing principle: + +> "A sender MUST NOT send whitespace between the start-line and the first header field." -- RFC 9112 Section 2.2 + +### Chain of Reasoning + +1. The `field-line` ABNF grammar places the colon immediately after `field-name` with zero intervening characters. +2. The RFC explicitly calls out that past differences in handling this whitespace caused real security vulnerabilities. +3. The requirement is MUST reject with specifically 400 -- not close, not 500, not any other status code. This is one of the most prescriptive requirements in the entire HTTP specification. +4. The RFC chose to require 400 specifically (rather than allowing connection close as an alternative) because the security implications demand an unambiguous signal to the client. + +### Scoring Justification + +**Scored (MUST).** The RFC mandates exactly one acceptable server behavior: respond with 400 (Bad Request). There is no alternative disposition such as "or close the connection." Connection close alone does not satisfy this requirement because the RFC explicitly specifies the status code. A server that closes the connection without sending 400 is non-compliant. + +## Sources + +- [RFC 9112 Section 5 -- Field Syntax](https://www.rfc-editor.org/rfc/rfc9112#section-5) +- [RFC 9110 Section 16.3.1 -- Request Smuggling](https://www.rfc-editor.org/rfc/rfc9110#section-16.3.1) diff --git a/docs/content/docs/headers/whitespace-before-headers.md b/docs/content/docs/headers/whitespace-before-headers.md index 3046881..e4aa95e 100644 --- a/docs/content/docs/headers/whitespace-before-headers.md +++ b/docs/content/docs/headers/whitespace-before-headers.md @@ -1,82 +1,82 @@ ---- -title: "WHITESPACE-BEFORE-HEADERS" -description: "WHITESPACE-BEFORE-HEADERS test documentation" -weight: 6 ---- - -| | | -|---|---| -| **Test ID** | `COMP-WHITESPACE-BEFORE-HEADERS` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | -| **Requirement** | MUST reject or ignore | -| **Expected** | `400` or close | - -## What it sends - -A request with whitespace (SP) before the first header line, between the request-line and the headers. - -```http -GET / HTTP/1.1\r\n - \r\n -Host: localhost:8080\r\n -\r\n -``` - -A line with a single space appears between the request-line and the first header. - - -## What the RFC says - -> "A sender MUST NOT send whitespace between the start-line and the first header field." -- RFC 9112 Section 2.2 - -> "A recipient that receives whitespace between the start-line and the first header field MUST either reject the message as invalid or consume each whitespace-preceded line without further processing of it (i.e., ignore the entire line, along with any subsequent lines preceded by whitespace, until a properly formed header field is received or the header section is terminated)." -- RFC 9112 Section 2.2 - -> "Rejection or removal of invalid whitespace-preceded lines is necessary to prevent their misinterpretation by downstream recipients that might be vulnerable to request smuggling (Section 11.2) or response splitting (Section 11.1) attacks." -- RFC 9112 Section 2.2 - -## Why it matters - -Whitespace before headers can confuse parsers about where headers begin, potentially enabling smuggling. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -HTTP-message = start-line CRLF - *( field-line CRLF ) - CRLF - [ message-body ] -``` - -The grammar transitions directly from `start-line CRLF` to `*( field-line CRLF )`. There is no provision for whitespace-only lines between the start-line and the first header field. - -### RFC Evidence - -**RFC 9112 Section 2.2** establishes the sender prohibition: - -> "A sender MUST NOT send whitespace between the start-line and the first header field." -- RFC 9112 Section 2.2 - -**RFC 9112 Section 2.2** mandates the recipient behavior with two alternatives: - -> "A recipient that receives whitespace between the start-line and the first header field MUST either reject the message as invalid or consume each whitespace-preceded line without further processing of it (i.e., ignore the entire line, along with any subsequent lines preceded by whitespace, until a properly formed header field is received or the header section is terminated)." -- RFC 9112 Section 2.2 - -**RFC 9112 Section 2.2** explains the security motivation: - -> "Rejection or removal of invalid whitespace-preceded lines is necessary to prevent their misinterpretation by downstream recipients that might be vulnerable to request smuggling (Section 11.2) or response splitting (Section 11.1) attacks." -- RFC 9112 Section 2.2 - -### Chain of Reasoning - -1. The test inserts a line with a single SP between the request-line and the first header (`Host:`). -2. This whitespace-preceded line does not match `field-line` (which requires `field-name ":" ...`), so the parser enters an error state. -3. The RFC provides two MUST-level alternatives: reject as invalid, or silently consume the offending lines. -4. If the server rejects, 400 is the expected response. If the server consumes, it should skip the whitespace line and process `Host: localhost:8080` as the first header, returning a normal response. -5. The explicit mention of request smuggling and response splitting as motivations underscores that ignoring this requirement is a security vulnerability, not merely a compliance gap. - -### Scoring Justification - -**Scored (MUST).** The RFC mandates one of two behaviors at the MUST level: reject or consume. Both 400 (rejection) and connection close are acceptable outcomes. A 2xx response is also acceptable if the server correctly consumed the whitespace-preceded line and processed the remaining headers normally. The `AllowConnectionClose` flag is set because rejection by closing the connection satisfies the "reject the message as invalid" alternative. - -## Sources - -- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) +--- +title: "Whitespace Before Headers — HTTP/1.1 Compliance" +description: "A request with whitespace (SP) before the first header line, between the request-line and the headers. Tested against RFC 9112 Section 2.2." +weight: 6 +--- + +| | | +|---|---| +| **Test ID** | `COMP-WHITESPACE-BEFORE-HEADERS` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | +| **Requirement** | MUST reject or ignore | +| **Expected** | `400` or close | + +## What it sends + +A request with whitespace (SP) before the first header line, between the request-line and the headers. + +```http +GET / HTTP/1.1\r\n + \r\n +Host: localhost:8080\r\n +\r\n +``` + +A line with a single space appears between the request-line and the first header. + + +## What the RFC says + +> "A sender MUST NOT send whitespace between the start-line and the first header field." -- RFC 9112 Section 2.2 + +> "A recipient that receives whitespace between the start-line and the first header field MUST either reject the message as invalid or consume each whitespace-preceded line without further processing of it (i.e., ignore the entire line, along with any subsequent lines preceded by whitespace, until a properly formed header field is received or the header section is terminated)." -- RFC 9112 Section 2.2 + +> "Rejection or removal of invalid whitespace-preceded lines is necessary to prevent their misinterpretation by downstream recipients that might be vulnerable to request smuggling (Section 11.2) or response splitting (Section 11.1) attacks." -- RFC 9112 Section 2.2 + +## Why it matters + +Whitespace before headers can confuse parsers about where headers begin, potentially enabling smuggling. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +HTTP-message = start-line CRLF + *( field-line CRLF ) + CRLF + [ message-body ] +``` + +The grammar transitions directly from `start-line CRLF` to `*( field-line CRLF )`. There is no provision for whitespace-only lines between the start-line and the first header field. + +### RFC Evidence + +**RFC 9112 Section 2.2** establishes the sender prohibition: + +> "A sender MUST NOT send whitespace between the start-line and the first header field." -- RFC 9112 Section 2.2 + +**RFC 9112 Section 2.2** mandates the recipient behavior with two alternatives: + +> "A recipient that receives whitespace between the start-line and the first header field MUST either reject the message as invalid or consume each whitespace-preceded line without further processing of it (i.e., ignore the entire line, along with any subsequent lines preceded by whitespace, until a properly formed header field is received or the header section is terminated)." -- RFC 9112 Section 2.2 + +**RFC 9112 Section 2.2** explains the security motivation: + +> "Rejection or removal of invalid whitespace-preceded lines is necessary to prevent their misinterpretation by downstream recipients that might be vulnerable to request smuggling (Section 11.2) or response splitting (Section 11.1) attacks." -- RFC 9112 Section 2.2 + +### Chain of Reasoning + +1. The test inserts a line with a single SP between the request-line and the first header (`Host:`). +2. This whitespace-preceded line does not match `field-line` (which requires `field-name ":" ...`), so the parser enters an error state. +3. The RFC provides two MUST-level alternatives: reject as invalid, or silently consume the offending lines. +4. If the server rejects, 400 is the expected response. If the server consumes, it should skip the whitespace line and process `Host: localhost:8080` as the first header, returning a normal response. +5. The explicit mention of request smuggling and response splitting as motivations underscores that ignoring this requirement is a security vulnerability, not merely a compliance gap. + +### Scoring Justification + +**Scored (MUST).** The RFC mandates one of two behaviors at the MUST level: reject or consume. Both 400 (rejection) and connection close are acceptable outcomes. A 2xx response is also acceptable if the server correctly consumed the whitespace-preceded line and processed the remaining headers normally. The `AllowConnectionClose` flag is set because rejection by closing the connection satisfies the "reject the message as invalid" alternative. + +## Sources + +- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) diff --git a/docs/content/docs/host-header/_index.md b/docs/content/docs/host-header/_index.md index bc08b02..ac9d449 100644 --- a/docs/content/docs/host-header/_index.md +++ b/docs/content/docs/host-header/_index.md @@ -1,6 +1,6 @@ --- title: Host Header -description: "Host Header — Http11Probe documentation" +description: "Host header validation tests for missing, duplicate, and malformed values — the only case where RFC 9112 explicitly mandates a 400 response." weight: 7 sidebar: open: false diff --git a/docs/content/docs/host-header/duplicate-host-same.md b/docs/content/docs/host-header/duplicate-host-same.md index dc57da4..a9d04fa 100644 --- a/docs/content/docs/host-header/duplicate-host-same.md +++ b/docs/content/docs/host-header/duplicate-host-same.md @@ -1,77 +1,77 @@ ---- -title: "DUPLICATE-HOST-SAME" -description: "DUPLICATE-HOST-SAME test documentation" -weight: 3 ---- - -| | | -|---|---| -| **Test ID** | `COMP-DUPLICATE-HOST-SAME` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | -| **Requirement** | MUST respond with 400 | -| **Expected** | `400` | - -## What it sends - -A request with two identical Host headers. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Host: localhost:8080\r\n -\r\n -``` - -Two `Host` headers with identical values. - - -## What the RFC says - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -The phrase "more than one Host header field line" makes no exception for identical values. - -## Why it matters - -The RFC mandates 400 for *any* duplicate Host headers, regardless of whether the values match. Some servers incorrectly allow identical duplicates. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -Host = uri-host [ ":" port ] -``` - -The Host header is a singleton field. Its ABNF does not use the `#` list syntax, meaning only one Host header field line is permitted per request. The grammar makes no provision for combining or deduplicating multiple instances. - -### RFC Evidence - -**RFC 9112 Section 3.2** covers this case with the same MUST as different-value duplicates: - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -**RFC 9112 Section 3.2** mandates the client obligation: - -> "A client MUST send a Host header field in all HTTP/1.1 request messages." -- RFC 9112 Section 3.2 - -**RFC 9110 Section 7.2** specifies the Host grammar: - -> "Host = uri-host [ ':' port ]" -- RFC 9110 Section 7.2 - -### Chain of Reasoning - -1. The test sends two Host headers both containing `localhost:8080` -- identical values. -2. The RFC says "more than one Host header field line" without any qualifier about whether the values differ. The count of Host lines is what matters, not the content. -3. Some servers optimize by deduplicating identical headers before validation, effectively collapsing two identical Host lines into one. This is non-compliant: the RFC counts field lines, not unique values. -4. A server that accepts identical duplicate Host headers may also accept different-value duplicates in certain edge cases (e.g., if case sensitivity or trailing whitespace causes the "identical" check to fail), creating a host injection vulnerability. -5. The 400 requirement is absolute -- no alternative disposition is offered. - -### Scoring Justification - -**Scored (MUST).** The RFC mandates exactly 400 for any request with more than one Host header field line, regardless of value equality. Connection close without sending 400 is non-compliant. The `AllowConnectionClose` flag is not set because the RFC explicitly requires the 400 status code. - -## Sources - -- [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) +--- +title: "Duplicate Host Same — HTTP/1.1 Compliance" +description: "A request with two identical Host headers. Tested against RFC 9112 Section 3.2." +weight: 3 +--- + +| | | +|---|---| +| **Test ID** | `COMP-DUPLICATE-HOST-SAME` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | +| **Requirement** | MUST respond with 400 | +| **Expected** | `400` | + +## What it sends + +A request with two identical Host headers. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Host: localhost:8080\r\n +\r\n +``` + +Two `Host` headers with identical values. + + +## What the RFC says + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +The phrase "more than one Host header field line" makes no exception for identical values. + +## Why it matters + +The RFC mandates 400 for *any* duplicate Host headers, regardless of whether the values match. Some servers incorrectly allow identical duplicates. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +Host = uri-host [ ":" port ] +``` + +The Host header is a singleton field. Its ABNF does not use the `#` list syntax, meaning only one Host header field line is permitted per request. The grammar makes no provision for combining or deduplicating multiple instances. + +### RFC Evidence + +**RFC 9112 Section 3.2** covers this case with the same MUST as different-value duplicates: + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +**RFC 9112 Section 3.2** mandates the client obligation: + +> "A client MUST send a Host header field in all HTTP/1.1 request messages." -- RFC 9112 Section 3.2 + +**RFC 9110 Section 7.2** specifies the Host grammar: + +> "Host = uri-host [ ':' port ]" -- RFC 9110 Section 7.2 + +### Chain of Reasoning + +1. The test sends two Host headers both containing `localhost:8080` -- identical values. +2. The RFC says "more than one Host header field line" without any qualifier about whether the values differ. The count of Host lines is what matters, not the content. +3. Some servers optimize by deduplicating identical headers before validation, effectively collapsing two identical Host lines into one. This is non-compliant: the RFC counts field lines, not unique values. +4. A server that accepts identical duplicate Host headers may also accept different-value duplicates in certain edge cases (e.g., if case sensitivity or trailing whitespace causes the "identical" check to fail), creating a host injection vulnerability. +5. The 400 requirement is absolute -- no alternative disposition is offered. + +### Scoring Justification + +**Scored (MUST).** The RFC mandates exactly 400 for any request with more than one Host header field line, regardless of value equality. Connection close without sending 400 is non-compliant. The `AllowConnectionClose` flag is not set because the RFC explicitly requires the 400 status code. + +## Sources + +- [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) diff --git a/docs/content/docs/host-header/duplicate-host.md b/docs/content/docs/host-header/duplicate-host.md index eb11696..28f218e 100644 --- a/docs/content/docs/host-header/duplicate-host.md +++ b/docs/content/docs/host-header/duplicate-host.md @@ -1,81 +1,81 @@ ---- -title: "DUPLICATE-HOST" -description: "DUPLICATE-HOST test documentation" -weight: 2 ---- - -| | | -|---|---| -| **Test ID** | `RFC9110-5.4-DUPLICATE-HOST` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | -| **Requirement** | MUST | -| **Expected** | `400` | - -## What it sends - -A request with two `Host` headers containing different values: - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Host: other.example.com\r\n -\r\n -``` - -Two `Host` headers with different values. - - -## What the RFC says - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -Same MUST-400 as the missing Host case. The server must send an actual 400 response. - -## Why it matters - -Duplicate Host headers with different values are a classic host header injection attack. If the application uses the first Host and the CDN uses the second (or vice versa), the attacker can: -- Poison caches with content for the wrong domain -- Bypass host-based access controls -- Trigger SSRF via internal hostnames - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -Host = uri-host [ ":" port ] -``` - -The Host header is defined as a singleton field -- it is not a list-based header and does not use the `#` (comma-separated list) syntax. Multiple Host header field lines are therefore structurally invalid, regardless of whether the values match. - -### RFC Evidence - -**RFC 9112 Section 3.2** explicitly covers the duplicate case: - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -**RFC 9112 Section 3.2** mandates the client side: - -> "A client MUST send a Host header field in all HTTP/1.1 request messages." -- RFC 9112 Section 3.2 - -**RFC 9110 Section 7.2** reinforces the requirement: - -> "A client MUST send the Host header field in an HTTP/1.1 request message, unless the request target is a URI whose origin is undefined." -- RFC 9110 Section 7.2 - -### Chain of Reasoning - -1. The test sends two Host headers with different values: `localhost:8080` and `other.example.com`. -2. The phrase "more than one Host header field line" in RFC 9112 Section 3.2 unambiguously covers this case. -3. The RFC mandates 400 with no alternative disposition -- not "reject," not "close," but specifically "respond with a 400." -4. Different values in duplicate Host headers are a textbook host header injection attack. If the server picks the first value and a proxy picks the second (or vice versa), the attacker controls routing for one of the two components. -5. Even if both values were identical, the RFC still requires 400 (see DUPLICATE-HOST-SAME). The prohibition is on the structural count of Host lines, not on the semantic content. - -### Scoring Justification - -**Scored (MUST).** The RFC mandates exactly 400 for duplicate Host headers. No alternative response is permitted. Connection close without sending 400 is non-compliant. The `AllowConnectionClose` flag is not set because the RFC explicitly requires the 400 status code. - -## Sources - -- [RFC 9112 Section 3.2 -- Request Target](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) -- [RFC 9110 Section 7.2 -- Host and :authority](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) +--- +title: "Duplicate Host — HTTP/1.1 Compliance" +description: "A request with two Host headers containing different values:. Tested against RFC 9112 Section 3.2." +weight: 2 +--- + +| | | +|---|---| +| **Test ID** | `RFC9110-5.4-DUPLICATE-HOST` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | +| **Requirement** | MUST | +| **Expected** | `400` | + +## What it sends + +A request with two `Host` headers containing different values: + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Host: other.example.com\r\n +\r\n +``` + +Two `Host` headers with different values. + + +## What the RFC says + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +Same MUST-400 as the missing Host case. The server must send an actual 400 response. + +## Why it matters + +Duplicate Host headers with different values are a classic host header injection attack. If the application uses the first Host and the CDN uses the second (or vice versa), the attacker can: +- Poison caches with content for the wrong domain +- Bypass host-based access controls +- Trigger SSRF via internal hostnames + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +Host = uri-host [ ":" port ] +``` + +The Host header is defined as a singleton field -- it is not a list-based header and does not use the `#` (comma-separated list) syntax. Multiple Host header field lines are therefore structurally invalid, regardless of whether the values match. + +### RFC Evidence + +**RFC 9112 Section 3.2** explicitly covers the duplicate case: + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +**RFC 9112 Section 3.2** mandates the client side: + +> "A client MUST send a Host header field in all HTTP/1.1 request messages." -- RFC 9112 Section 3.2 + +**RFC 9110 Section 7.2** reinforces the requirement: + +> "A client MUST send the Host header field in an HTTP/1.1 request message, unless the request target is a URI whose origin is undefined." -- RFC 9110 Section 7.2 + +### Chain of Reasoning + +1. The test sends two Host headers with different values: `localhost:8080` and `other.example.com`. +2. The phrase "more than one Host header field line" in RFC 9112 Section 3.2 unambiguously covers this case. +3. The RFC mandates 400 with no alternative disposition -- not "reject," not "close," but specifically "respond with a 400." +4. Different values in duplicate Host headers are a textbook host header injection attack. If the server picks the first value and a proxy picks the second (or vice versa), the attacker controls routing for one of the two components. +5. Even if both values were identical, the RFC still requires 400 (see DUPLICATE-HOST-SAME). The prohibition is on the structural count of Host lines, not on the semantic content. + +### Scoring Justification + +**Scored (MUST).** The RFC mandates exactly 400 for duplicate Host headers. No alternative response is permitted. Connection close without sending 400 is non-compliant. The `AllowConnectionClose` flag is not set because the RFC explicitly requires the 400 status code. + +## Sources + +- [RFC 9112 Section 3.2 -- Request Target](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) +- [RFC 9110 Section 7.2 -- Host and :authority](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) diff --git a/docs/content/docs/host-header/host-empty-value.md b/docs/content/docs/host-header/host-empty-value.md index a49c081..e71f1f9 100644 --- a/docs/content/docs/host-header/host-empty-value.md +++ b/docs/content/docs/host-header/host-empty-value.md @@ -1,79 +1,79 @@ ---- -title: "HOST-EMPTY-VALUE" -description: "HOST-EMPTY-VALUE test documentation" -weight: 6 ---- - -| | | -|---|---| -| **Test ID** | `COMP-HOST-EMPTY-VALUE` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 §3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -A request with a `Host` header present but with an empty value. - -```http -GET / HTTP/1.1\r\n -Host: \r\n -\r\n -``` - -The `Host` header line exists, but its value is empty (nothing between the colon and CRLF). - -## What the RFC says - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -> "If the authority component is missing or undefined for the target URI, then a client MUST send a Host header field with an empty field value." -- RFC 9112 Section 3.2 - -An empty Host value is only valid when the authority component of the target URI is missing or undefined. For an origin-form request like `GET / HTTP/1.1`, the authority must come from the Host header, so an empty value leaves the server unable to determine which virtual host is being addressed. - -## Why it matters - -A Host header with an empty value is functionally equivalent to having no Host header at all. If a server accepts this, it may fall back to a default virtual host, potentially serving content from an unintended application. In multi-tenant environments, this can lead to information disclosure or incorrect routing. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -Host = uri-host [ ":" port ] -uri-host = -``` - -The Host grammar allows `uri-host` which can be an IP-literal, IPv4address, or reg-name. An empty string is a degenerate case: it does not match any of these productions (reg-name can match empty, but the RFC prohibits empty host identifiers). - -### RFC Evidence - -**RFC 9112 Section 3.2** provides the validity rule: - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -**RFC 9112 Section 3.2** describes when an empty Host is legitimate: - -> "If the authority component is missing or undefined for the target URI, then a client MUST send a Host header field with an empty field value." -- RFC 9112 Section 3.2 - -**RFC 9110 Section 4.2.1** prohibits empty host identifiers: - -> "A sender MUST NOT generate an 'http' URI with an empty host identifier." -- RFC 9110 Section 4.2.1 - -### Chain of Reasoning - -1. The test sends `Host: ` (empty value) with an origin-form request-target (`GET / HTTP/1.1`). -2. An empty Host value is only valid when the authority component of the target URI is "missing or undefined." For an origin-form request, the target URI is reconstructed using the Host header as the authority. An empty Host means no authority can be determined. -3. Since the request uses origin-form, the authority is neither "missing" nor "undefined" -- it is expected to be provided by the Host header. An empty value fails to provide the required information. -4. RFC 9110 Section 4.2.1 reinforces that http URIs must not have empty host identifiers. A request with an empty Host to an http-scheme server would produce an invalid effective request URI. -5. The server cannot determine which virtual host is being addressed, which is functionally equivalent to a missing Host header. - -### Scoring Justification - -**Scored (MUST).** An empty Host value on an origin-form request results in an invalid effective request URI, making it an "invalid field value" under RFC 9112 Section 3.2. The MUST-400 requirement applies. Both 400 and connection close are accepted because the empty Host creates ambiguity about whether the request has a valid target at all. - -## Sources - -- [RFC 9112 Section 3.2 -- Request Target](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) -- [RFC 9110 Section 7.2 -- Host and :authority](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) +--- +title: "Host Empty Value — HTTP/1.1 Compliance" +description: "A request with a Host header present but with an empty value. Tested against RFC 9112 §3.2." +weight: 6 +--- + +| | | +|---|---| +| **Test ID** | `COMP-HOST-EMPTY-VALUE` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 §3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +A request with a `Host` header present but with an empty value. + +```http +GET / HTTP/1.1\r\n +Host: \r\n +\r\n +``` + +The `Host` header line exists, but its value is empty (nothing between the colon and CRLF). + +## What the RFC says + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +> "If the authority component is missing or undefined for the target URI, then a client MUST send a Host header field with an empty field value." -- RFC 9112 Section 3.2 + +An empty Host value is only valid when the authority component of the target URI is missing or undefined. For an origin-form request like `GET / HTTP/1.1`, the authority must come from the Host header, so an empty value leaves the server unable to determine which virtual host is being addressed. + +## Why it matters + +A Host header with an empty value is functionally equivalent to having no Host header at all. If a server accepts this, it may fall back to a default virtual host, potentially serving content from an unintended application. In multi-tenant environments, this can lead to information disclosure or incorrect routing. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +Host = uri-host [ ":" port ] +uri-host = +``` + +The Host grammar allows `uri-host` which can be an IP-literal, IPv4address, or reg-name. An empty string is a degenerate case: it does not match any of these productions (reg-name can match empty, but the RFC prohibits empty host identifiers). + +### RFC Evidence + +**RFC 9112 Section 3.2** provides the validity rule: + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +**RFC 9112 Section 3.2** describes when an empty Host is legitimate: + +> "If the authority component is missing or undefined for the target URI, then a client MUST send a Host header field with an empty field value." -- RFC 9112 Section 3.2 + +**RFC 9110 Section 4.2.1** prohibits empty host identifiers: + +> "A sender MUST NOT generate an 'http' URI with an empty host identifier." -- RFC 9110 Section 4.2.1 + +### Chain of Reasoning + +1. The test sends `Host: ` (empty value) with an origin-form request-target (`GET / HTTP/1.1`). +2. An empty Host value is only valid when the authority component of the target URI is "missing or undefined." For an origin-form request, the target URI is reconstructed using the Host header as the authority. An empty Host means no authority can be determined. +3. Since the request uses origin-form, the authority is neither "missing" nor "undefined" -- it is expected to be provided by the Host header. An empty value fails to provide the required information. +4. RFC 9110 Section 4.2.1 reinforces that http URIs must not have empty host identifiers. A request with an empty Host to an http-scheme server would produce an invalid effective request URI. +5. The server cannot determine which virtual host is being addressed, which is functionally equivalent to a missing Host header. + +### Scoring Justification + +**Scored (MUST).** An empty Host value on an origin-form request results in an invalid effective request URI, making it an "invalid field value" under RFC 9112 Section 3.2. The MUST-400 requirement applies. Both 400 and connection close are accepted because the empty Host creates ambiguity about whether the request has a valid target at all. + +## Sources + +- [RFC 9112 Section 3.2 -- Request Target](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) +- [RFC 9110 Section 7.2 -- Host and :authority](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) diff --git a/docs/content/docs/host-header/host-with-path.md b/docs/content/docs/host-header/host-with-path.md index 97802fb..95c89e8 100644 --- a/docs/content/docs/host-header/host-with-path.md +++ b/docs/content/docs/host-header/host-with-path.md @@ -1,82 +1,82 @@ ---- -title: "HOST-WITH-PATH" -description: "HOST-WITH-PATH test documentation" -weight: 5 ---- - -| | | -|---|---| -| **Test ID** | `COMP-HOST-WITH-PATH` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | -| **Requirement** | MUST respond with 400 | -| **Expected** | `400` or close | - -## What it sends - -A request with `Host: hostname:port/path`. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080/path\r\n -\r\n -``` - -The `Host` header includes a `/path` component. - - -## What the RFC says - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -The Host header grammar is defined as: - -> "Host = uri-host [ ':' port ]" -- RFC 9110 Section 7.2 - -No path component is permitted. A value like `localhost:8080/path` does not match `uri-host [ ":" port ]` and is therefore an invalid field value. - -## Why it matters - -A path in the Host header is a clear sign of manipulation. If a reverse proxy uses the Host to route, a path component could alter routing. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -Host = uri-host [ ":" port ] -uri-host = -port = *DIGIT -``` - -The Host grammar terminates after the optional `port` component. There is no provision for a path (`/` followed by path segments) or any other URI component after the port. The `/` character is not valid in `uri-host` or `port`. - -### RFC Evidence - -**RFC 9110 Section 7.2** defines the Host grammar: - -> "Host = uri-host [ ':' port ]" -- RFC 9110 Section 7.2 - -**RFC 9112 Section 3.2** mandates rejection of invalid Host values: - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -**RFC 9112 Section 3.2** describes how the Host relates to the request-target: - -> "When a proxy receives a request with an absolute-form of request-target, the proxy MUST ignore the received Host header field (if any) and instead replace it with the host information of the request-target." -- RFC 9112 Section 3.2 - -### Chain of Reasoning - -1. The test sends `Host: localhost:8080/path`. The `/path` component follows the port. -2. The Host grammar is `uri-host [ ":" port ]`. The `port` production is `*DIGIT` (zero or more digits). After parsing `8080` as the port, the `/path` remainder does not match any part of the Host grammar. -3. Since the value does not conform to the Host grammar, it is an "invalid field value" per RFC 9112 Section 3.2, triggering the MUST-400 requirement. -4. A path component in the Host header is a strong indicator of manipulation. In a reverse proxy configuration, the Host header is used for routing decisions. If a proxy interprets the path component as part of the backend route, the attacker can redirect the request to an unintended backend path. -5. Some servers may attempt to parse the value as a full URI, extracting just the host and port. This normalization is not authorized by the RFC and bypasses the security protection that the grammar validation provides. - -### Scoring Justification - -**Scored (MUST).** The Host value `localhost:8080/path` is an invalid field value because it does not match the `Host = uri-host [ ":" port ]` grammar. RFC 9112 Section 3.2 mandates 400. Both 400 and connection close are accepted because the invalid value may cause parsing failures before the server can generate a response. - -## Sources - -- [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) +--- +title: "Host With Path — HTTP/1.1 Compliance" +description: "A request with Host: hostname:port/path. Tested against RFC 9112 Section 3.2." +weight: 5 +--- + +| | | +|---|---| +| **Test ID** | `COMP-HOST-WITH-PATH` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | +| **Requirement** | MUST respond with 400 | +| **Expected** | `400` or close | + +## What it sends + +A request with `Host: hostname:port/path`. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080/path\r\n +\r\n +``` + +The `Host` header includes a `/path` component. + + +## What the RFC says + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +The Host header grammar is defined as: + +> "Host = uri-host [ ':' port ]" -- RFC 9110 Section 7.2 + +No path component is permitted. A value like `localhost:8080/path` does not match `uri-host [ ":" port ]` and is therefore an invalid field value. + +## Why it matters + +A path in the Host header is a clear sign of manipulation. If a reverse proxy uses the Host to route, a path component could alter routing. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +Host = uri-host [ ":" port ] +uri-host = +port = *DIGIT +``` + +The Host grammar terminates after the optional `port` component. There is no provision for a path (`/` followed by path segments) or any other URI component after the port. The `/` character is not valid in `uri-host` or `port`. + +### RFC Evidence + +**RFC 9110 Section 7.2** defines the Host grammar: + +> "Host = uri-host [ ':' port ]" -- RFC 9110 Section 7.2 + +**RFC 9112 Section 3.2** mandates rejection of invalid Host values: + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +**RFC 9112 Section 3.2** describes how the Host relates to the request-target: + +> "When a proxy receives a request with an absolute-form of request-target, the proxy MUST ignore the received Host header field (if any) and instead replace it with the host information of the request-target." -- RFC 9112 Section 3.2 + +### Chain of Reasoning + +1. The test sends `Host: localhost:8080/path`. The `/path` component follows the port. +2. The Host grammar is `uri-host [ ":" port ]`. The `port` production is `*DIGIT` (zero or more digits). After parsing `8080` as the port, the `/path` remainder does not match any part of the Host grammar. +3. Since the value does not conform to the Host grammar, it is an "invalid field value" per RFC 9112 Section 3.2, triggering the MUST-400 requirement. +4. A path component in the Host header is a strong indicator of manipulation. In a reverse proxy configuration, the Host header is used for routing decisions. If a proxy interprets the path component as part of the backend route, the attacker can redirect the request to an unintended backend path. +5. Some servers may attempt to parse the value as a full URI, extracting just the host and port. This normalization is not authorized by the RFC and bypasses the security protection that the grammar validation provides. + +### Scoring Justification + +**Scored (MUST).** The Host value `localhost:8080/path` is an invalid field value because it does not match the `Host = uri-host [ ":" port ]` grammar. RFC 9112 Section 3.2 mandates 400. Both 400 and connection close are accepted because the invalid value may cause parsing failures before the server can generate a response. + +## Sources + +- [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) diff --git a/docs/content/docs/host-header/host-with-userinfo.md b/docs/content/docs/host-header/host-with-userinfo.md index 1fd3171..8ceee50 100644 --- a/docs/content/docs/host-header/host-with-userinfo.md +++ b/docs/content/docs/host-header/host-with-userinfo.md @@ -1,91 +1,91 @@ ---- -title: "HOST-WITH-USERINFO" -description: "HOST-WITH-USERINFO test documentation" -weight: 4 ---- - -| | | -|---|---| -| **Test ID** | `COMP-HOST-WITH-USERINFO` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | -| **Requirement** | MUST respond with 400 | -| **Expected** | `400` or close | - -## What it sends - -A request with `Host: user@hostname:port`. - -```http -GET / HTTP/1.1\r\n -Host: user@localhost:8080\r\n -\r\n -``` - -The `Host` header contains a `user@` userinfo prefix. - - -## What the RFC says - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -The Host header grammar is defined as: - -> "Host = uri-host [ ':' port ]" -- RFC 9110 Section 7.2 - -No userinfo component (`user@`) is permitted in the Host grammar. A value like `user@localhost:8080` does not match `uri-host [ ":" port ]` and is therefore an invalid field value. - -## Why it matters - -The userinfo component (`user@`) is not part of the Host grammar. A server that accepts it may be tricked into routing requests incorrectly. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -Host = uri-host [ ":" port ] -uri-host = -port = -``` - -The Host header grammar is `uri-host [ ":" port ]`. Notably absent from this grammar is the `userinfo "@"` component that appears in the broader URI `authority` production: - -``` -; From RFC 3986: -authority = [ userinfo "@" ] host [ ":" port ] -userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) -``` - -The Host header uses only `uri-host` and optionally `port`, deliberately excluding the `userinfo` subcomponent. - -### RFC Evidence - -**RFC 9110 Section 7.2** defines the Host grammar without userinfo: - -> "Host = uri-host [ ':' port ]" -- RFC 9110 Section 7.2 - -**RFC 9112 Section 3.2** mandates rejection of invalid Host values: - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -**RFC 9110 Section 4.2.1** prohibits userinfo in http URIs: - -> "A sender MUST NOT generate an 'http' URI with an empty host identifier." -- RFC 9110 Section 4.2.1 - -### Chain of Reasoning - -1. The test sends `Host: user@localhost:8080`. The `user@` prefix is a userinfo component. -2. The Host header grammar is `uri-host [ ":" port ]`. There is no `userinfo "@"` element in this production. -3. The `@` character is not part of `uri-host` syntax. When a parser encounters `user@localhost:8080`, the value does not match `uri-host [ ":" port ]`. -4. Since the value does not match the Host grammar, it is an "invalid field value" per RFC 9112 Section 3.2, triggering the MUST-400 requirement. -5. A server that strips the userinfo and uses only the host:port portion is performing normalization that the RFC does not authorize. The RFC says "invalid field value" triggers 400, not "normalize and proceed." - -### Scoring Justification - -**Scored (MUST).** The Host value `user@localhost:8080` is an invalid field value because it does not match the `Host = uri-host [ ":" port ]` grammar. RFC 9112 Section 3.2 mandates 400 for invalid Host values. Both 400 and connection close are accepted because the "invalid field value" clause is broader than the "missing Host" and "duplicate Host" clauses, and some servers may close the connection during URI parsing before generating a response. - -## Sources - -- [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) -- [RFC 3986 Section 3.2.1](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.1) +--- +title: "Host With Userinfo — HTTP/1.1 Compliance" +description: "A request with Host: user@hostname:port. Tested against RFC 9112 Section 3.2." +weight: 4 +--- + +| | | +|---|---| +| **Test ID** | `COMP-HOST-WITH-USERINFO` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | +| **Requirement** | MUST respond with 400 | +| **Expected** | `400` or close | + +## What it sends + +A request with `Host: user@hostname:port`. + +```http +GET / HTTP/1.1\r\n +Host: user@localhost:8080\r\n +\r\n +``` + +The `Host` header contains a `user@` userinfo prefix. + + +## What the RFC says + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +The Host header grammar is defined as: + +> "Host = uri-host [ ':' port ]" -- RFC 9110 Section 7.2 + +No userinfo component (`user@`) is permitted in the Host grammar. A value like `user@localhost:8080` does not match `uri-host [ ":" port ]` and is therefore an invalid field value. + +## Why it matters + +The userinfo component (`user@`) is not part of the Host grammar. A server that accepts it may be tricked into routing requests incorrectly. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +Host = uri-host [ ":" port ] +uri-host = +port = +``` + +The Host header grammar is `uri-host [ ":" port ]`. Notably absent from this grammar is the `userinfo "@"` component that appears in the broader URI `authority` production: + +``` +; From RFC 3986: +authority = [ userinfo "@" ] host [ ":" port ] +userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) +``` + +The Host header uses only `uri-host` and optionally `port`, deliberately excluding the `userinfo` subcomponent. + +### RFC Evidence + +**RFC 9110 Section 7.2** defines the Host grammar without userinfo: + +> "Host = uri-host [ ':' port ]" -- RFC 9110 Section 7.2 + +**RFC 9112 Section 3.2** mandates rejection of invalid Host values: + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +**RFC 9110 Section 4.2.1** prohibits userinfo in http URIs: + +> "A sender MUST NOT generate an 'http' URI with an empty host identifier." -- RFC 9110 Section 4.2.1 + +### Chain of Reasoning + +1. The test sends `Host: user@localhost:8080`. The `user@` prefix is a userinfo component. +2. The Host header grammar is `uri-host [ ":" port ]`. There is no `userinfo "@"` element in this production. +3. The `@` character is not part of `uri-host` syntax. When a parser encounters `user@localhost:8080`, the value does not match `uri-host [ ":" port ]`. +4. Since the value does not match the Host grammar, it is an "invalid field value" per RFC 9112 Section 3.2, triggering the MUST-400 requirement. +5. A server that strips the userinfo and uses only the host:port portion is performing normalization that the RFC does not authorize. The RFC says "invalid field value" triggers 400, not "normalize and proceed." + +### Scoring Justification + +**Scored (MUST).** The Host value `user@localhost:8080` is an invalid field value because it does not match the `Host = uri-host [ ":" port ]` grammar. RFC 9112 Section 3.2 mandates 400 for invalid Host values. Both 400 and connection close are accepted because the "invalid field value" clause is broader than the "missing Host" and "duplicate Host" clauses, and some servers may close the connection during URI parsing before generating a response. + +## Sources + +- [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) +- [RFC 3986 Section 3.2.1](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.1) diff --git a/docs/content/docs/host-header/http10-no-host.md b/docs/content/docs/host-header/http10-no-host.md index e963a77..53400a2 100644 --- a/docs/content/docs/host-header/http10-no-host.md +++ b/docs/content/docs/host-header/http10-no-host.md @@ -1,87 +1,87 @@ ---- -title: "HTTP10-NO-HOST" -description: "HTTP10-NO-HOST test documentation" -weight: 7 ---- - -| | | -|---|---| -| **Test ID** | `COMP-HTTP10-NO-HOST` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 §3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | -| **Requirement** | MAY (unscored) | -| **Expected** | `200` = Warn, `400` = Pass | - -## What it sends - -An HTTP/1.0 request with no `Host` header at all. - -```http -GET / HTTP/1.0\r\n -\r\n -``` - -No `Host` header is present, and the HTTP version is 1.0. - -## What the RFC says - -The `Host` header requirement was introduced in HTTP/1.1. HTTP/1.0 predates this requirement, so an HTTP/1.0 request without a `Host` header is not technically a protocol violation: - -> "A client MUST send a Host header field in all HTTP/1.1 request messages." -- RFC 9112 Section 3.2 - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -Note the MUST-400 requirement explicitly says "HTTP/1.1 request message." For HTTP/1.0, the server may choose to accept the request (routing to a default virtual host) or reject it. - -**Pass:** Server rejects with `400` (strict -- good security practice). -**Warn:** Server accepts with `200` (valid -- HTTP/1.0 did not require Host). - -## Why this test is unscored - -The RFC's MUST-400 requirement for missing Host applies only to HTTP/1.1 requests. Since this test sends an HTTP/1.0 request, there is no normative requirement to reject it. Both accepting and rejecting are valid behaviors, making a strict pass/fail determination inappropriate. - -## Why it matters - -In a virtual hosting environment, a request without a `Host` header gives the server no indication of which site is being targeted. Accepting such requests means the server must fall back to a default host, which could serve unintended content. Rejecting HTTP/1.0 requests without `Host` is the safer approach, especially since legitimate modern clients always send a `Host` header regardless of HTTP version. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -HTTP-version = HTTP-name "/" DIGIT "." DIGIT -Host = uri-host [ ":" port ] -``` - -The request uses `HTTP/1.0` as its version. The Host header grammar is unchanged between versions, but the obligation to send it is version-specific. - -### RFC Evidence - -**RFC 9112 Section 3.2** scopes the MUST to HTTP/1.1: - -> "A client MUST send a Host header field in all HTTP/1.1 request messages." -- RFC 9112 Section 3.2 - -**RFC 9112 Section 3.2** scopes the MUST-400 to HTTP/1.1: - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -**RFC 9112 Section 9.3** addresses HTTP/1.0 connection behavior: - -> "A proxy server MUST NOT maintain a persistent connection with an HTTP/1.0 client." -- RFC 9112 Section 9.3 - -### Chain of Reasoning - -1. The test sends `GET / HTTP/1.0` with no Host header. -2. The MUST-400 requirement in RFC 9112 Section 3.2 explicitly applies to "any HTTP/1.1 request message that lacks a Host header field." The HTTP/1.0 version falls outside this scope. -3. The second clause ("any request message that contains more than one Host header field line or a Host header field with an invalid field value") uses "any request message" without a version qualifier. However, the absence of a Host header is covered only by the first, version-scoped clause. -4. HTTP/1.0 predates the Host header requirement. The original HTTP/1.0 specification (RFC 1945) did not define Host as a request header at all. -5. A server that rejects HTTP/1.0 requests without Host is being stricter than required, which is a good security practice in virtual hosting environments. A server that accepts them is also compliant. - -### Scoring Justification - -**Unscored (MAY).** The RFC's MUST-400 for missing Host applies only to HTTP/1.1. Since this is an HTTP/1.0 request, no normative requirement exists to reject it. The test records 400 as Pass (the server applied the stricter, safer behavior) and 200 as Warn (the server accepted the request, which is valid but less secure). No result is recorded as Fail because neither behavior violates the specification. - -## Sources - -- [RFC 9112 Section 3.2 -- Request Target](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) -- [RFC 9110 Section 7.2 -- Host and :authority](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) +--- +title: "HTTP/1.0 No Host — HTTP/1.1 Compliance" +description: "An HTTP/1.0 request with no Host header at all. Tested against RFC 9112 §3.2." +weight: 7 +--- + +| | | +|---|---| +| **Test ID** | `COMP-HTTP10-NO-HOST` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 §3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | +| **Requirement** | MAY (unscored) | +| **Expected** | `200` = Warn, `400` = Pass | + +## What it sends + +An HTTP/1.0 request with no `Host` header at all. + +```http +GET / HTTP/1.0\r\n +\r\n +``` + +No `Host` header is present, and the HTTP version is 1.0. + +## What the RFC says + +The `Host` header requirement was introduced in HTTP/1.1. HTTP/1.0 predates this requirement, so an HTTP/1.0 request without a `Host` header is not technically a protocol violation: + +> "A client MUST send a Host header field in all HTTP/1.1 request messages." -- RFC 9112 Section 3.2 + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +Note the MUST-400 requirement explicitly says "HTTP/1.1 request message." For HTTP/1.0, the server may choose to accept the request (routing to a default virtual host) or reject it. + +**Pass:** Server rejects with `400` (strict -- good security practice). +**Warn:** Server accepts with `200` (valid -- HTTP/1.0 did not require Host). + +## Why this test is unscored + +The RFC's MUST-400 requirement for missing Host applies only to HTTP/1.1 requests. Since this test sends an HTTP/1.0 request, there is no normative requirement to reject it. Both accepting and rejecting are valid behaviors, making a strict pass/fail determination inappropriate. + +## Why it matters + +In a virtual hosting environment, a request without a `Host` header gives the server no indication of which site is being targeted. Accepting such requests means the server must fall back to a default host, which could serve unintended content. Rejecting HTTP/1.0 requests without `Host` is the safer approach, especially since legitimate modern clients always send a `Host` header regardless of HTTP version. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +HTTP-version = HTTP-name "/" DIGIT "." DIGIT +Host = uri-host [ ":" port ] +``` + +The request uses `HTTP/1.0` as its version. The Host header grammar is unchanged between versions, but the obligation to send it is version-specific. + +### RFC Evidence + +**RFC 9112 Section 3.2** scopes the MUST to HTTP/1.1: + +> "A client MUST send a Host header field in all HTTP/1.1 request messages." -- RFC 9112 Section 3.2 + +**RFC 9112 Section 3.2** scopes the MUST-400 to HTTP/1.1: + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +**RFC 9112 Section 9.3** addresses HTTP/1.0 connection behavior: + +> "A proxy server MUST NOT maintain a persistent connection with an HTTP/1.0 client." -- RFC 9112 Section 9.3 + +### Chain of Reasoning + +1. The test sends `GET / HTTP/1.0` with no Host header. +2. The MUST-400 requirement in RFC 9112 Section 3.2 explicitly applies to "any HTTP/1.1 request message that lacks a Host header field." The HTTP/1.0 version falls outside this scope. +3. The second clause ("any request message that contains more than one Host header field line or a Host header field with an invalid field value") uses "any request message" without a version qualifier. However, the absence of a Host header is covered only by the first, version-scoped clause. +4. HTTP/1.0 predates the Host header requirement. The original HTTP/1.0 specification (RFC 1945) did not define Host as a request header at all. +5. A server that rejects HTTP/1.0 requests without Host is being stricter than required, which is a good security practice in virtual hosting environments. A server that accepts them is also compliant. + +### Scoring Justification + +**Unscored (MAY).** The RFC's MUST-400 for missing Host applies only to HTTP/1.1. Since this is an HTTP/1.0 request, no normative requirement exists to reject it. The test records 400 as Pass (the server applied the stricter, safer behavior) and 200 as Warn (the server accepted the request, which is valid but less secure). No result is recorded as Fail because neither behavior violates the specification. + +## Sources + +- [RFC 9112 Section 3.2 -- Request Target](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) +- [RFC 9110 Section 7.2 -- Host and :authority](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) diff --git a/docs/content/docs/host-header/missing-host.md b/docs/content/docs/host-header/missing-host.md index ef2a5d4..e94e2d4 100644 --- a/docs/content/docs/host-header/missing-host.md +++ b/docs/content/docs/host-header/missing-host.md @@ -1,78 +1,78 @@ ---- -title: "MISSING-HOST" -description: "MISSING-HOST test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-7.1-MISSING-HOST` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | -| **Requirement** | MUST | -| **Expected** | `400` | - -## What it sends - -A valid `GET / HTTP/1.1` request with no `Host` header. - -```http -GET / HTTP/1.1\r\n -\r\n -``` - -No `Host` header at all. - - -## What the RFC says - -> "A client MUST send a Host header field in all HTTP/1.1 request messages." -- RFC 9112 Section 3.2 - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -This is one of the strongest requirements in the HTTP spec. The server MUST actually send a 400 response -- closing the connection silently does not satisfy this MUST. - -## Why it matters - -The Host header tells the server which virtual host is being addressed. Without it, the server cannot determine which site the request is for. In multi-tenant environments, processing a request without a Host header could route it to the wrong application. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -Host = uri-host [ ":" port ] -``` - -The Host header carries the authority information for the target URI. In an origin-form request (`GET / HTTP/1.1`), the Host header is the sole source of authority, making it indispensable for request routing. - -### RFC Evidence - -**RFC 9112 Section 3.2** mandates the client obligation: - -> "A client MUST send a Host header field in all HTTP/1.1 request messages." -- RFC 9112 Section 3.2 - -**RFC 9112 Section 3.2** mandates the server response with a specific status code: - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -**RFC 9110 Section 7.2** reinforces the requirement: - -> "A client MUST send the Host header field in an HTTP/1.1 request message, unless the request target is a URI whose origin is undefined." -- RFC 9110 Section 7.2 - -### Chain of Reasoning - -1. The test sends `GET / HTTP/1.1` with no Host header at all. -2. The request uses origin-form (`/`), which does not embed authority information in the request-target. The Host header is the only mechanism to convey which server or virtual host is being addressed. -3. RFC 9112 Section 3.2 uses "MUST respond with a 400" -- not "MUST reject" or "SHOULD respond." The status code 400 is explicitly mandated. -4. Connection close alone does not satisfy this requirement because the RFC specifies the exact response status code the server must use. A server that closes the TCP connection without sending a 400 response is non-compliant. -5. This is one of the few places in the HTTP specification where a specific status code is mandated at the MUST level, reflecting the critical role of the Host header in virtual hosting and request routing. - -### Scoring Justification - -**Scored (MUST).** The RFC mandates exactly 400 (Bad Request) for a missing Host header in HTTP/1.1. No alternative disposition is offered. Connection close without a 400 response is non-compliant. The `AllowConnectionClose` flag is not set because the RFC explicitly requires the 400 status code to be sent. - -## Sources - -- [RFC 9112 Section 3.2 -- Request Target](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) -- [RFC 9110 Section 7.2 -- Host and :authority](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) +--- +title: "Missing Host — HTTP/1.1 Compliance" +description: "A valid GET / HTTP/1.1 request with no Host header. Tested against RFC 9112 Section 3.2." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-7.1-MISSING-HOST` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | +| **Requirement** | MUST | +| **Expected** | `400` | + +## What it sends + +A valid `GET / HTTP/1.1` request with no `Host` header. + +```http +GET / HTTP/1.1\r\n +\r\n +``` + +No `Host` header at all. + + +## What the RFC says + +> "A client MUST send a Host header field in all HTTP/1.1 request messages." -- RFC 9112 Section 3.2 + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +This is one of the strongest requirements in the HTTP spec. The server MUST actually send a 400 response -- closing the connection silently does not satisfy this MUST. + +## Why it matters + +The Host header tells the server which virtual host is being addressed. Without it, the server cannot determine which site the request is for. In multi-tenant environments, processing a request without a Host header could route it to the wrong application. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +Host = uri-host [ ":" port ] +``` + +The Host header carries the authority information for the target URI. In an origin-form request (`GET / HTTP/1.1`), the Host header is the sole source of authority, making it indispensable for request routing. + +### RFC Evidence + +**RFC 9112 Section 3.2** mandates the client obligation: + +> "A client MUST send a Host header field in all HTTP/1.1 request messages." -- RFC 9112 Section 3.2 + +**RFC 9112 Section 3.2** mandates the server response with a specific status code: + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +**RFC 9110 Section 7.2** reinforces the requirement: + +> "A client MUST send the Host header field in an HTTP/1.1 request message, unless the request target is a URI whose origin is undefined." -- RFC 9110 Section 7.2 + +### Chain of Reasoning + +1. The test sends `GET / HTTP/1.1` with no Host header at all. +2. The request uses origin-form (`/`), which does not embed authority information in the request-target. The Host header is the only mechanism to convey which server or virtual host is being addressed. +3. RFC 9112 Section 3.2 uses "MUST respond with a 400" -- not "MUST reject" or "SHOULD respond." The status code 400 is explicitly mandated. +4. Connection close alone does not satisfy this requirement because the RFC specifies the exact response status code the server must use. A server that closes the TCP connection without sending a 400 response is non-compliant. +5. This is one of the few places in the HTTP specification where a specific status code is mandated at the MUST level, reflecting the critical role of the Host header in virtual hosting and request routing. + +### Scoring Justification + +**Scored (MUST).** The RFC mandates exactly 400 (Bad Request) for a missing Host header in HTTP/1.1. No alternative disposition is offered. Connection close without a 400 response is non-compliant. The `AllowConnectionClose` flag is not set because the RFC explicitly requires the 400 status code to be sent. + +## Sources + +- [RFC 9112 Section 3.2 -- Request Target](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) +- [RFC 9110 Section 7.2 -- Host and :authority](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) diff --git a/docs/content/docs/line-endings/_index.md b/docs/content/docs/line-endings/_index.md index 71ba5eb..d9859f3 100644 --- a/docs/content/docs/line-endings/_index.md +++ b/docs/content/docs/line-endings/_index.md @@ -1,6 +1,6 @@ --- title: Line Endings -description: "Line Endings — Http11Probe documentation" +description: "CRLF line-ending requirements for HTTP/1.1 messages, testing bare LF handling and bare CR rejection per RFC 9112 Section 2.2." weight: 4 sidebar: open: false diff --git a/docs/content/docs/line-endings/bare-lf-header.md b/docs/content/docs/line-endings/bare-lf-header.md index 04fafc7..b3be857 100644 --- a/docs/content/docs/line-endings/bare-lf-header.md +++ b/docs/content/docs/line-endings/bare-lf-header.md @@ -1,105 +1,105 @@ ---- -title: "BARE-LF-HEADER" -description: "BARE-LF-HEADER test documentation" -weight: 2 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-2.2-BARE-LF-HEADER` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | -| **Requirement** | MAY | -| **Expected** | `400` or close (pass), `2xx` (warn) | - -## What it sends - -A valid `GET` request where one of the header lines is terminated with `\n` (bare LF) instead of `\r\n`. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\n -X-Test: value\r\n -\r\n -``` - - -## What the RFC says - -> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." — RFC 9112 Section 2.2 - -The same rule applies to header field lines as to the request-line. Bare LF is a sender violation, but recipients are permitted to tolerate it. Strict rejection (`400` or connection close) is the safer posture because it eliminates parser disagreements between hops. - -## Why it matters - -If headers are delimited differently by different parsers in a request chain, an attacker can inject headers that only one parser sees. This is the foundation of header injection and smuggling attacks. - -## Deep Analysis - -### ABNF grammar for line endings - -The HTTP message grammar from RFC 9112 Section 2.1 requires CRLF after every field-line: - -``` -HTTP-message = start-line CRLF - *( field-line CRLF ) - CRLF - [ message-body ] -``` - -Each header is a `field-line` followed by `CRLF`. The core ABNF definitions from RFC 5234 Appendix B.1: - -``` -CRLF = CR LF ; Internet standard newline -CR = %x0D ; carriage return -LF = %x0A ; linefeed -``` - -The `field-line` production itself is: - -``` -field-line = field-name ":" OWS field-value OWS -``` - -The terminator is not part of `field-line` --- it comes from the outer `HTTP-message` rule, which demands `CRLF`. A bare `%x0A` after a header does not match the `CRLF` production. - -### RFC evidence - -**Quote 1 --- The canonical line terminator and bare LF allowance:** - -> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." --- RFC 9112 Section 2.2 - -The phrase "start-line **and fields**" is critical: the same MAY-accept rule applies equally to header field lines. A server has discretion to accept or reject bare LF in headers. - -**Quote 2 --- Security of octet-level parsing:** - -> "A recipient MUST parse an HTTP message as a sequence of octets in an encoding that is a superset of US-ASCII. Parsing an HTTP message as a stream of Unicode characters, without regard for the specific encoding, creates security vulnerabilities due to the varying ways that string processing libraries handle invalid multibyte character sequences that contain the octet LF (%x0A)." --- RFC 9112 Section 2.2 - -The RFC specifically warns about how different libraries handle the `%x0A` octet. When one parser in a chain treats bare LF as a header terminator and another does not, the two parsers see different header boundaries --- the foundation of header injection attacks. - -**Quote 3 --- Smuggling from inconsistent parsing:** - -> "Lenient parsing can result in request smuggling security vulnerabilities if there are multiple recipients of the message and each has its own unique interpretation of robustness." --- RFC 9112 Section 3 - -This directly supports why bare LF in headers is a security concern: if the front-end accepts `Host: localhost:8080\n` as a complete header but the back-end does not recognize the bare LF, the back-end may concatenate the next header into the Host value, or vice versa. This disagreement is exploitable. - -### Chain of reasoning - -1. **The payload:** The test sends a valid request-line terminated with CRLF, but the `Host` header is terminated with bare LF (`%x0A`) instead of CRLF (`%x0D %x0A`): `Host: localhost:8080\n`. -2. **The ABNF violation:** The `HTTP-message` grammar requires `field-line CRLF`. Bare LF does not match `CRLF = CR LF`. -3. **The MAY exception:** RFC 9112 Section 2.2 permits recipients to recognize bare LF as a line terminator for "the start-line and fields." This is a MAY --- full discretion. -4. **The header injection vector:** Header boundaries are security-critical. If a proxy and origin server disagree on where `Host: localhost:8080` ends, an attacker can inject headers visible to only one parser. For example, a bare-LF-tolerant proxy might see two headers where a strict origin sees one malformed header. -5. **Conclusion:** Rejecting with `400` or closing the connection is the safer posture. Accepting is a valid MAY but introduces risk in multi-hop deployments. - -### Scored / Unscored justification - -This test is **scored (Pass/Warn)**: - -- **Pass** for `400` or connection close --- strict rejection prevents header boundary disagreements between hops. -- **Warn** for `2xx` --- the server accepted bare LF in a header. This is permitted by the RFC (MAY) but introduces a smuggling vector in multi-hop architectures. - -The strict posture is rewarded because header-level bare LF is particularly sensitive --- header boundaries directly control how Content-Length, Transfer-Encoding, and Host are parsed, all of which are smuggling-critical fields. - -## Sources - -- [RFC 9112 Section 2.2 — Message Parsing](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) +--- +title: "Bare LF Header — HTTP/1.1 Compliance" +description: "A valid GET request where one of the header lines is terminated with \\n (bare LF) instead of \\r\\n. Tested against RFC 9112 Section 2.2." +weight: 2 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-2.2-BARE-LF-HEADER` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | +| **Requirement** | MAY | +| **Expected** | `400` or close (pass), `2xx` (warn) | + +## What it sends + +A valid `GET` request where one of the header lines is terminated with `\n` (bare LF) instead of `\r\n`. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\n +X-Test: value\r\n +\r\n +``` + + +## What the RFC says + +> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." — RFC 9112 Section 2.2 + +The same rule applies to header field lines as to the request-line. Bare LF is a sender violation, but recipients are permitted to tolerate it. Strict rejection (`400` or connection close) is the safer posture because it eliminates parser disagreements between hops. + +## Why it matters + +If headers are delimited differently by different parsers in a request chain, an attacker can inject headers that only one parser sees. This is the foundation of header injection and smuggling attacks. + +## Deep Analysis + +### ABNF grammar for line endings + +The HTTP message grammar from RFC 9112 Section 2.1 requires CRLF after every field-line: + +``` +HTTP-message = start-line CRLF + *( field-line CRLF ) + CRLF + [ message-body ] +``` + +Each header is a `field-line` followed by `CRLF`. The core ABNF definitions from RFC 5234 Appendix B.1: + +``` +CRLF = CR LF ; Internet standard newline +CR = %x0D ; carriage return +LF = %x0A ; linefeed +``` + +The `field-line` production itself is: + +``` +field-line = field-name ":" OWS field-value OWS +``` + +The terminator is not part of `field-line` --- it comes from the outer `HTTP-message` rule, which demands `CRLF`. A bare `%x0A` after a header does not match the `CRLF` production. + +### RFC evidence + +**Quote 1 --- The canonical line terminator and bare LF allowance:** + +> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." --- RFC 9112 Section 2.2 + +The phrase "start-line **and fields**" is critical: the same MAY-accept rule applies equally to header field lines. A server has discretion to accept or reject bare LF in headers. + +**Quote 2 --- Security of octet-level parsing:** + +> "A recipient MUST parse an HTTP message as a sequence of octets in an encoding that is a superset of US-ASCII. Parsing an HTTP message as a stream of Unicode characters, without regard for the specific encoding, creates security vulnerabilities due to the varying ways that string processing libraries handle invalid multibyte character sequences that contain the octet LF (%x0A)." --- RFC 9112 Section 2.2 + +The RFC specifically warns about how different libraries handle the `%x0A` octet. When one parser in a chain treats bare LF as a header terminator and another does not, the two parsers see different header boundaries --- the foundation of header injection attacks. + +**Quote 3 --- Smuggling from inconsistent parsing:** + +> "Lenient parsing can result in request smuggling security vulnerabilities if there are multiple recipients of the message and each has its own unique interpretation of robustness." --- RFC 9112 Section 3 + +This directly supports why bare LF in headers is a security concern: if the front-end accepts `Host: localhost:8080\n` as a complete header but the back-end does not recognize the bare LF, the back-end may concatenate the next header into the Host value, or vice versa. This disagreement is exploitable. + +### Chain of reasoning + +1. **The payload:** The test sends a valid request-line terminated with CRLF, but the `Host` header is terminated with bare LF (`%x0A`) instead of CRLF (`%x0D %x0A`): `Host: localhost:8080\n`. +2. **The ABNF violation:** The `HTTP-message` grammar requires `field-line CRLF`. Bare LF does not match `CRLF = CR LF`. +3. **The MAY exception:** RFC 9112 Section 2.2 permits recipients to recognize bare LF as a line terminator for "the start-line and fields." This is a MAY --- full discretion. +4. **The header injection vector:** Header boundaries are security-critical. If a proxy and origin server disagree on where `Host: localhost:8080` ends, an attacker can inject headers visible to only one parser. For example, a bare-LF-tolerant proxy might see two headers where a strict origin sees one malformed header. +5. **Conclusion:** Rejecting with `400` or closing the connection is the safer posture. Accepting is a valid MAY but introduces risk in multi-hop deployments. + +### Scored / Unscored justification + +This test is **scored (Pass/Warn)**: + +- **Pass** for `400` or connection close --- strict rejection prevents header boundary disagreements between hops. +- **Warn** for `2xx` --- the server accepted bare LF in a header. This is permitted by the RFC (MAY) but introduces a smuggling vector in multi-hop architectures. + +The strict posture is rewarded because header-level bare LF is particularly sensitive --- header boundaries directly control how Content-Length, Transfer-Encoding, and Host are parsed, all of which are smuggling-critical fields. + +## Sources + +- [RFC 9112 Section 2.2 — Message Parsing](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) diff --git a/docs/content/docs/line-endings/bare-lf-request-line.md b/docs/content/docs/line-endings/bare-lf-request-line.md index 62fd4ca..70deece 100644 --- a/docs/content/docs/line-endings/bare-lf-request-line.md +++ b/docs/content/docs/line-endings/bare-lf-request-line.md @@ -1,107 +1,107 @@ ---- -title: "BARE-LF-REQUEST-LINE" -description: "BARE-LF-REQUEST-LINE test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-2.2-BARE-LF-REQUEST-LINE` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | -| **Requirement** | MAY | -| **Expected** | `400` or close (pass), `2xx` (warn) | - -## What it sends - -A `GET / HTTP/1.1` request where the request-line is terminated with `\n` (bare LF) instead of `\r\n` (CRLF). - -```http -GET / HTTP/1.1\n -Host: localhost:8080\r\n -\r\n -``` - - -## What the RFC says - -> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." — RFC 9112 Section 2.2 - -The sender MUST NOT generate bare LF, but the recipient is explicitly given permission to accept it. This is a MAY — not a MUST. Strict rejection (`400` or connection close) is the safer posture because it eliminates parser disagreements between hops. - -## Why it matters - -Bare LF acceptance is a common source of parser disagreements. If a front-end proxy accepts bare LF as a line terminator but a back-end server does not (or vice versa), the two may disagree on request boundaries — a prerequisite for request smuggling. - -Strict rejection is the safer choice, which is why Http11Probe scores it as a pass when the server rejects. - -## Deep Analysis - -### ABNF grammar for line endings - -The formal grammar mandates CRLF as the line terminator throughout the HTTP message structure. From RFC 9112 Section 2.1: - -``` -HTTP-message = start-line CRLF - *( field-line CRLF ) - CRLF - [ message-body ] -``` - -And from RFC 5234 Appendix B.1, the core ABNF definition: - -``` -CRLF = CR LF ; Internet standard newline -CR = %x0D ; carriage return -LF = %x0A ; linefeed -``` - -The request-line itself is defined in RFC 9112 Section 3: - -``` -request-line = method SP request-target SP HTTP-version -``` - -The `request-line` does not include a line terminator in its own production rule --- the termination comes from the `HTTP-message` grammar, which places `CRLF` after `start-line`. This means the **only** formally valid terminator for the request-line is the two-octet sequence `%x0D %x0A`. - -### RFC evidence - -**Quote 1 --- The canonical line terminator:** - -> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." --- RFC 9112 Section 2.2 - -This sentence establishes two things: (a) CRLF is the normative line terminator, and (b) recognizing bare LF is **permitted** but not required. The keyword is MAY (RFC 2119): the recipient has full discretion to accept or reject. - -**Quote 2 --- Parsing as octets:** - -> "A recipient MUST parse an HTTP message as a sequence of octets in an encoding that is a superset of US-ASCII. Parsing an HTTP message as a stream of Unicode characters, without regard for the specific encoding, creates security vulnerabilities due to the varying ways that string processing libraries handle invalid multibyte character sequences that contain the octet LF (%x0A)." --- RFC 9112 Section 2.2 - -This is directly relevant because bare LF (`%x0A`) is called out by name as a security-sensitive octet. The RFC warns that how parsers handle the LF octet is a source of vulnerabilities. - -**Quote 3 --- Request smuggling from lenient parsing:** - -> "Lenient parsing can result in request smuggling security vulnerabilities if there are multiple recipients of the message and each has its own unique interpretation of robustness." --- RFC 9112 Section 3 - -This explains the security motivation for rejecting bare LF: when a front-end proxy accepts bare LF and a back-end server does not (or vice versa), they disagree on where the request-line ends, creating a smuggling vector. - -### Chain of reasoning - -1. **The payload:** The test sends `GET / HTTP/1.1\nHost: localhost:8080\r\n\r\n`. The request-line is terminated with a single `%x0A` (LF) instead of the required `%x0D %x0A` (CRLF). -2. **The ABNF violation:** The `HTTP-message` grammar requires `start-line CRLF`. A bare LF does not match the `CRLF` production (`CR LF`), so the message is syntactically non-conforming. -3. **The MAY exception:** RFC 9112 Section 2.2 says a recipient MAY recognize a single LF as a line terminator. This gives the server discretion: it can accept or reject. -4. **The security argument:** RFC 9112 Section 11.2 warns that lenient parsing leads to smuggling when intermediaries parse differently. A strict server that rejects bare LF eliminates this class of disagreement entirely. -5. **Conclusion:** A server that rejects with `400` or closes the connection is taking the safer posture. A server that accepts is exercising a valid MAY but introduces potential parser-differential risk. - -### Scored / Unscored justification - -This test is **scored (Pass/Warn)**: - -- **Pass** for `400` or connection close --- strict rejection eliminates parser-differential attacks. -- **Warn** for `2xx` --- the server accepted bare LF as a line terminator. This is permitted by the RFC (MAY) but introduces a smuggling vector in multi-hop architectures. - -The strict posture is rewarded because bare LF acceptance is a well-known source of parser disagreements that enable request smuggling. - -## Sources - -- [RFC 9112 Section 2.2 — Message Parsing](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) -- [RFC 9110 Section 16.3 — Intermediary Encapsulation Attacks](https://www.rfc-editor.org/rfc/rfc9110#section-16.3) +--- +title: "Bare LF Request Line — HTTP/1.1 Compliance" +description: "A GET / HTTP/1.1 request where the request-line is terminated with \\n (bare LF) instead of \\r\\n (CRLF). Tested against RFC 9112 Section 2.2." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-2.2-BARE-LF-REQUEST-LINE` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | +| **Requirement** | MAY | +| **Expected** | `400` or close (pass), `2xx` (warn) | + +## What it sends + +A `GET / HTTP/1.1` request where the request-line is terminated with `\n` (bare LF) instead of `\r\n` (CRLF). + +```http +GET / HTTP/1.1\n +Host: localhost:8080\r\n +\r\n +``` + + +## What the RFC says + +> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." — RFC 9112 Section 2.2 + +The sender MUST NOT generate bare LF, but the recipient is explicitly given permission to accept it. This is a MAY — not a MUST. Strict rejection (`400` or connection close) is the safer posture because it eliminates parser disagreements between hops. + +## Why it matters + +Bare LF acceptance is a common source of parser disagreements. If a front-end proxy accepts bare LF as a line terminator but a back-end server does not (or vice versa), the two may disagree on request boundaries — a prerequisite for request smuggling. + +Strict rejection is the safer choice, which is why Http11Probe scores it as a pass when the server rejects. + +## Deep Analysis + +### ABNF grammar for line endings + +The formal grammar mandates CRLF as the line terminator throughout the HTTP message structure. From RFC 9112 Section 2.1: + +``` +HTTP-message = start-line CRLF + *( field-line CRLF ) + CRLF + [ message-body ] +``` + +And from RFC 5234 Appendix B.1, the core ABNF definition: + +``` +CRLF = CR LF ; Internet standard newline +CR = %x0D ; carriage return +LF = %x0A ; linefeed +``` + +The request-line itself is defined in RFC 9112 Section 3: + +``` +request-line = method SP request-target SP HTTP-version +``` + +The `request-line` does not include a line terminator in its own production rule --- the termination comes from the `HTTP-message` grammar, which places `CRLF` after `start-line`. This means the **only** formally valid terminator for the request-line is the two-octet sequence `%x0D %x0A`. + +### RFC evidence + +**Quote 1 --- The canonical line terminator:** + +> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." --- RFC 9112 Section 2.2 + +This sentence establishes two things: (a) CRLF is the normative line terminator, and (b) recognizing bare LF is **permitted** but not required. The keyword is MAY (RFC 2119): the recipient has full discretion to accept or reject. + +**Quote 2 --- Parsing as octets:** + +> "A recipient MUST parse an HTTP message as a sequence of octets in an encoding that is a superset of US-ASCII. Parsing an HTTP message as a stream of Unicode characters, without regard for the specific encoding, creates security vulnerabilities due to the varying ways that string processing libraries handle invalid multibyte character sequences that contain the octet LF (%x0A)." --- RFC 9112 Section 2.2 + +This is directly relevant because bare LF (`%x0A`) is called out by name as a security-sensitive octet. The RFC warns that how parsers handle the LF octet is a source of vulnerabilities. + +**Quote 3 --- Request smuggling from lenient parsing:** + +> "Lenient parsing can result in request smuggling security vulnerabilities if there are multiple recipients of the message and each has its own unique interpretation of robustness." --- RFC 9112 Section 3 + +This explains the security motivation for rejecting bare LF: when a front-end proxy accepts bare LF and a back-end server does not (or vice versa), they disagree on where the request-line ends, creating a smuggling vector. + +### Chain of reasoning + +1. **The payload:** The test sends `GET / HTTP/1.1\nHost: localhost:8080\r\n\r\n`. The request-line is terminated with a single `%x0A` (LF) instead of the required `%x0D %x0A` (CRLF). +2. **The ABNF violation:** The `HTTP-message` grammar requires `start-line CRLF`. A bare LF does not match the `CRLF` production (`CR LF`), so the message is syntactically non-conforming. +3. **The MAY exception:** RFC 9112 Section 2.2 says a recipient MAY recognize a single LF as a line terminator. This gives the server discretion: it can accept or reject. +4. **The security argument:** RFC 9112 Section 11.2 warns that lenient parsing leads to smuggling when intermediaries parse differently. A strict server that rejects bare LF eliminates this class of disagreement entirely. +5. **Conclusion:** A server that rejects with `400` or closes the connection is taking the safer posture. A server that accepts is exercising a valid MAY but introduces potential parser-differential risk. + +### Scored / Unscored justification + +This test is **scored (Pass/Warn)**: + +- **Pass** for `400` or connection close --- strict rejection eliminates parser-differential attacks. +- **Warn** for `2xx` --- the server accepted bare LF as a line terminator. This is permitted by the RFC (MAY) but introduces a smuggling vector in multi-hop architectures. + +The strict posture is rewarded because bare LF acceptance is a well-known source of parser disagreements that enable request smuggling. + +## Sources + +- [RFC 9112 Section 2.2 — Message Parsing](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) +- [RFC 9110 Section 16.3 — Intermediary Encapsulation Attacks](https://www.rfc-editor.org/rfc/rfc9110#section-16.3) diff --git a/docs/content/docs/line-endings/cr-only-line-ending.md b/docs/content/docs/line-endings/cr-only-line-ending.md index b0cfb95..62eadef 100644 --- a/docs/content/docs/line-endings/cr-only-line-ending.md +++ b/docs/content/docs/line-endings/cr-only-line-ending.md @@ -1,101 +1,101 @@ ---- -title: "CR-ONLY-LINE-ENDING" -description: "CR-ONLY-LINE-ENDING test documentation" -weight: 3 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-3-CR-ONLY-LINE-ENDING` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | -| **Requirement** | MUST | -| **Expected** | `400` | - -## What it sends - -A request where lines are terminated with `\r` (bare CR) instead of `\r\n` (CRLF). - -```http -GET / HTTP/1.1\rHost: localhost:8080\r\n -\r\n -``` - -The request-line is terminated with bare `\r` (CR only) instead of `\r\n` (CRLF). The `Host:` header starts immediately after the CR. - - -## What the RFC says - -> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content. A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." — RFC 9112 Section 2.2 - -This is a MUST with two alternatives: consider the element invalid (reject with `400`), or replace each bare CR with SP before processing. Unlike bare LF, which is MAY-accept, bare CR has a mandatory handling requirement — the server cannot silently treat it as a line terminator. - -## Why it matters - -Bare CR that is silently ignored creates a discrepancy between what different parsers see. If one parser treats CR as a line ending and another ignores it, the resulting disagreement can be exploited for smuggling. - -## Deep Analysis - -### ABNF grammar for line endings - -The HTTP message grammar from RFC 9112 Section 2.1 requires CRLF throughout: - -``` -HTTP-message = start-line CRLF - *( field-line CRLF ) - CRLF - [ message-body ] -``` - -From RFC 5234 Appendix B.1, the core ABNF definitions: - -``` -CRLF = CR LF ; Internet standard newline -CR = %x0D ; carriage return -LF = %x0A ; linefeed -``` - -A bare CR (`%x0D` not immediately followed by `%x0A`) does not match any valid ABNF production for line termination. Unlike bare LF, which is addressed by a MAY-accept clause, bare CR has its own **mandatory** handling rule. - -### RFC evidence - -**Quote 1 --- The sender prohibition:** - -> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content." --- RFC 9112 Section 2.2 - -This establishes that a bare CR in the request-line or headers is a protocol violation at the sender level. The "MUST NOT" makes the sender non-conforming. - -**Quote 2 --- The mandatory recipient handling:** - -> "A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." --- RFC 9112 Section 2.2 - -This is the critical sentence. The keyword is MUST (RFC 2119), and it provides exactly two permitted behaviors: (a) consider the element invalid, or (b) replace each bare CR with SP (`%x20`). There is no third option --- silently treating bare CR as a line terminator is explicitly forbidden. - -**Quote 3 --- The bare LF contrast (showing bare CR is treated more strictly):** - -> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." --- RFC 9112 Section 2.2 - -This quote is relevant by contrast. Bare LF gets MAY-accept treatment, but bare CR gets MUST-reject-or-replace treatment. The RFC deliberately treats them differently: bare LF is a known legacy pattern with a tolerance path, while bare CR has no legitimate use as a line terminator and must be handled strictly. - -### Chain of reasoning - -1. **The payload:** The test sends `GET / HTTP/1.1\rHost: localhost:8080\r\n\r\n`. The request-line is terminated with bare CR (`%x0D`) instead of CRLF (`%x0D %x0A`). The `Host:` header starts immediately after the bare CR. -2. **The ABNF violation:** `%x0D` alone does not match `CRLF = CR LF`. The message is syntactically non-conforming. -3. **The MUST requirement:** RFC 9112 Section 2.2 mandates that the recipient MUST either (a) consider the element invalid, or (b) replace the bare CR with SP. There is no MAY-accept path. -4. **Option (a) --- invalid:** The server considers the request-line invalid and responds with `400 Bad Request`. This is the expected outcome. -5. **Option (b) --- replace with SP:** The server replaces the bare CR with a space, producing `GET / HTTP/1.1 Host: localhost:8080` as a single (malformed) line. This would likely result in a `400` anyway because the resulting request-line has extra tokens after the HTTP-version. -6. **What must NOT happen:** The server must not silently treat bare CR as a line terminator. If it did, it would see `GET / HTTP/1.1` as the request-line and `Host: localhost:8080` as a header --- appearing to work normally. This would violate the MUST in RFC 9112 Section 2.2. -7. **The smuggling angle:** If one parser treats bare CR as a line ending and another replaces it with SP, they will completely disagree on the message structure. The first sees two lines; the second sees one. This disagreement is directly exploitable. - -### Scored / Unscored justification - -This test is **scored (Pass/Fail)** at the MUST level: - -- **Pass** for `400` --- the server correctly considers the bare-CR element invalid, satisfying option (a) of the MUST requirement. -- **Fail** for `2xx` --- a `2xx` response means the server silently treated bare CR as a line terminator, violating the MUST in RFC 9112 Section 2.2. Neither of the two permitted behaviors (reject as invalid, replace with SP) would produce a successful response to this payload. -- **No Warn tier** --- MUST requirements are binary. The server either complies or it does not. -- **AllowConnectionClose is false** --- connection close is not an acceptable alternative because this is a MUST-level requirement where only `400` demonstrates correct handling. A silent close without a `400` does not prove the server identified the bare CR correctly. - -## Sources - -- [RFC 9112 Section 2.2 — Message Parsing](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) +--- +title: "CR Only Line Ending — HTTP/1.1 Compliance" +description: "A request where lines are terminated with \\r (bare CR) instead of \\r\\n (CRLF). Tested against RFC 9112 Section 2.2." +weight: 3 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-3-CR-ONLY-LINE-ENDING` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | +| **Requirement** | MUST | +| **Expected** | `400` | + +## What it sends + +A request where lines are terminated with `\r` (bare CR) instead of `\r\n` (CRLF). + +```http +GET / HTTP/1.1\rHost: localhost:8080\r\n +\r\n +``` + +The request-line is terminated with bare `\r` (CR only) instead of `\r\n` (CRLF). The `Host:` header starts immediately after the CR. + + +## What the RFC says + +> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content. A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." — RFC 9112 Section 2.2 + +This is a MUST with two alternatives: consider the element invalid (reject with `400`), or replace each bare CR with SP before processing. Unlike bare LF, which is MAY-accept, bare CR has a mandatory handling requirement — the server cannot silently treat it as a line terminator. + +## Why it matters + +Bare CR that is silently ignored creates a discrepancy between what different parsers see. If one parser treats CR as a line ending and another ignores it, the resulting disagreement can be exploited for smuggling. + +## Deep Analysis + +### ABNF grammar for line endings + +The HTTP message grammar from RFC 9112 Section 2.1 requires CRLF throughout: + +``` +HTTP-message = start-line CRLF + *( field-line CRLF ) + CRLF + [ message-body ] +``` + +From RFC 5234 Appendix B.1, the core ABNF definitions: + +``` +CRLF = CR LF ; Internet standard newline +CR = %x0D ; carriage return +LF = %x0A ; linefeed +``` + +A bare CR (`%x0D` not immediately followed by `%x0A`) does not match any valid ABNF production for line termination. Unlike bare LF, which is addressed by a MAY-accept clause, bare CR has its own **mandatory** handling rule. + +### RFC evidence + +**Quote 1 --- The sender prohibition:** + +> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content." --- RFC 9112 Section 2.2 + +This establishes that a bare CR in the request-line or headers is a protocol violation at the sender level. The "MUST NOT" makes the sender non-conforming. + +**Quote 2 --- The mandatory recipient handling:** + +> "A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." --- RFC 9112 Section 2.2 + +This is the critical sentence. The keyword is MUST (RFC 2119), and it provides exactly two permitted behaviors: (a) consider the element invalid, or (b) replace each bare CR with SP (`%x20`). There is no third option --- silently treating bare CR as a line terminator is explicitly forbidden. + +**Quote 3 --- The bare LF contrast (showing bare CR is treated more strictly):** + +> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." --- RFC 9112 Section 2.2 + +This quote is relevant by contrast. Bare LF gets MAY-accept treatment, but bare CR gets MUST-reject-or-replace treatment. The RFC deliberately treats them differently: bare LF is a known legacy pattern with a tolerance path, while bare CR has no legitimate use as a line terminator and must be handled strictly. + +### Chain of reasoning + +1. **The payload:** The test sends `GET / HTTP/1.1\rHost: localhost:8080\r\n\r\n`. The request-line is terminated with bare CR (`%x0D`) instead of CRLF (`%x0D %x0A`). The `Host:` header starts immediately after the bare CR. +2. **The ABNF violation:** `%x0D` alone does not match `CRLF = CR LF`. The message is syntactically non-conforming. +3. **The MUST requirement:** RFC 9112 Section 2.2 mandates that the recipient MUST either (a) consider the element invalid, or (b) replace the bare CR with SP. There is no MAY-accept path. +4. **Option (a) --- invalid:** The server considers the request-line invalid and responds with `400 Bad Request`. This is the expected outcome. +5. **Option (b) --- replace with SP:** The server replaces the bare CR with a space, producing `GET / HTTP/1.1 Host: localhost:8080` as a single (malformed) line. This would likely result in a `400` anyway because the resulting request-line has extra tokens after the HTTP-version. +6. **What must NOT happen:** The server must not silently treat bare CR as a line terminator. If it did, it would see `GET / HTTP/1.1` as the request-line and `Host: localhost:8080` as a header --- appearing to work normally. This would violate the MUST in RFC 9112 Section 2.2. +7. **The smuggling angle:** If one parser treats bare CR as a line ending and another replaces it with SP, they will completely disagree on the message structure. The first sees two lines; the second sees one. This disagreement is directly exploitable. + +### Scored / Unscored justification + +This test is **scored (Pass/Fail)** at the MUST level: + +- **Pass** for `400` --- the server correctly considers the bare-CR element invalid, satisfying option (a) of the MUST requirement. +- **Fail** for `2xx` --- a `2xx` response means the server silently treated bare CR as a line terminator, violating the MUST in RFC 9112 Section 2.2. Neither of the two permitted behaviors (reject as invalid, replace with SP) would produce a successful response to this payload. +- **No Warn tier** --- MUST requirements are binary. The server either complies or it does not. +- **AllowConnectionClose is false** --- connection close is not an acceptable alternative because this is a MUST-level requirement where only `400` demonstrates correct handling. A silent close without a `400` does not prove the server identified the bare CR correctly. + +## Sources + +- [RFC 9112 Section 2.2 — Message Parsing](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) diff --git a/docs/content/docs/line-endings/leading-crlf.md b/docs/content/docs/line-endings/leading-crlf.md index 896bfeb..caf64dd 100644 --- a/docs/content/docs/line-endings/leading-crlf.md +++ b/docs/content/docs/line-endings/leading-crlf.md @@ -1,113 +1,113 @@ ---- -title: "LEADING-CRLF" -description: "LEADING-CRLF test documentation" -weight: 4 ---- - -| | | -|---|---| -| **Test ID** | `COMP-LEADING-CRLF` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | -| **Requirement** | SHOULD ignore | -| **Expected** | `400` or `2xx` | - -## What it sends - -Two leading CRLF sequences before the request-line. - -```http -\r\n -\r\n -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -Two leading CRLF pairs precede the actual request-line. - - -## What the RFC says - -> "In the interest of robustness, a server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." — RFC 9112 Section 2.2 - -This is a SHOULD, not a MUST. The RFC recommends tolerance for robustness, but strict rejection is also acceptable. - -## Pass / Warn explanation - -| Response | Verdict | Reasoning | -|---|---|---| -| `400` or close | Pass | Strict rejection — valid because SHOULD is not MUST | -| `2xx` | Warn | Tolerant behavior — matches the RFC recommendation but flagged for awareness | - -## Why it matters - -Leading CRLFs can appear on persistent connections due to extra bytes after a previous response. The RFC encourages tolerance as a robustness measure. Both strict and tolerant behaviors are acceptable, which is why both produce non-failing verdicts. - -## Deep Analysis - -### ABNF grammar for line endings - -The HTTP message grammar from RFC 9112 Section 2.1: - -``` -HTTP-message = start-line CRLF - *( field-line CRLF ) - CRLF - [ message-body ] -``` - -From RFC 5234 Appendix B.1: - -``` -CRLF = CR LF ; Internet standard newline -CR = %x0D ; carriage return -LF = %x0A ; linefeed -``` - -The `HTTP-message` grammar starts with `start-line` --- there is no provision for leading whitespace or empty lines before the start-line. Any CRLF sequences before the request-line are extra-grammatical; they do not appear in the ABNF. - -### RFC evidence - -**Quote 1 --- The robustness recommendation:** - -> "In the interest of robustness, a server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." --- RFC 9112 Section 2.2 - -The keyword is SHOULD (RFC 2119): the server is recommended to ignore leading CRLFs but is not required to. The phrase "at least one" indicates that ignoring multiple leading CRLFs is also within scope of the recommendation. - -**Quote 2 --- The whitespace-before-header prohibition (by contrast):** - -> "A sender MUST NOT send whitespace between the start-line and the first header field." --- RFC 9112 Section 2.2 - -> "A recipient that receives whitespace between the start-line and the first header field MUST either reject the message as invalid or consume each whitespace-preceded line without further processing." --- RFC 9112 Section 2.2 - -These two sentences are relevant by contrast. The RFC is strict about whitespace **after** the start-line (MUST reject or consume) but lenient about empty lines **before** the start-line (SHOULD ignore). This asymmetry is deliberate: leading CRLFs on persistent connections are a known artifact, while whitespace after the start-line is an attack vector for response splitting. - -**Quote 3 --- The smuggling context:** - -> "Lenient parsing can result in request smuggling security vulnerabilities if there are multiple recipients of the message and each has its own unique interpretation of robustness." --- RFC 9112 Section 3 - -Even though the RFC recommends tolerance for leading CRLFs, the broader security context warns that any lenient parsing can create inconsistencies. If one parser in a chain ignores leading CRLFs and another treats them as the start of an empty request, the resulting disagreement could be exploitable --- though this is a lower-risk scenario compared to bare LF or bare CR. - -### Chain of reasoning - -1. **The payload:** The test sends `\r\n\r\nGET / HTTP/1.1\r\nHost: localhost:8080\r\n\r\n`. Two CRLF pairs precede the actual request-line. -2. **The ABNF mismatch:** The `HTTP-message` grammar starts with `start-line`. Leading CRLFs are not part of the grammar, so a strict parser would not expect them. -3. **The SHOULD recommendation:** RFC 9112 Section 2.2 says the server SHOULD ignore "at least one empty line (CRLF) received prior to the request-line." This payload has two leading CRLFs, which falls within "at least one." -4. **Why tolerance exists:** On persistent (keep-alive) connections, extra bytes after a previous response can manifest as leading CRLFs before the next request. The RFC accounts for this real-world artifact with the SHOULD recommendation. -5. **Why rejection is also valid:** SHOULD is not MUST. A server that rejects with `400` is non-conforming to the recommendation but not violating a requirement. Some security-focused servers may reject to avoid any ambiguity about message boundaries. -6. **Conclusion:** Both behaviors --- ignoring the leading CRLFs and processing the request normally (`2xx`), or rejecting with `400` --- are acceptable. Neither is a failure. - -### Scored / Unscored justification - -This test is **unscored (Pass/Warn, no Fail)**. The requirement level is SHOULD, and both acceptance and rejection are valid: - -- **Pass** for `400` or connection close --- strict rejection is valid because SHOULD is not MUST. The server is declining the robustness recommendation, which is its prerogative. -- **Warn** for `2xx` --- the server is following the RFC's SHOULD recommendation, which is the intended behavior. It receives a Warn (not a Pass) because Http11Probe flags tolerant behaviors for awareness, even when they match the RFC recommendation. -- The test **cannot produce a Fail** because no MUST requirement is at stake. - -The Pass/Warn asymmetry (strict = Pass, tolerant = Warn) may seem counterintuitive since the RFC recommends tolerance. The rationale is that Http11Probe prioritizes security awareness: any leniency in parsing, even RFC-recommended leniency, is worth flagging so operators can make informed decisions about their server's posture. - -## Sources - -- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) +--- +title: "Leading CRLF — HTTP/1.1 Compliance" +description: "Two leading CRLF sequences before the request-line. Tested against RFC 9112 Section 2.2." +weight: 4 +--- + +| | | +|---|---| +| **Test ID** | `COMP-LEADING-CRLF` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | +| **Requirement** | SHOULD ignore | +| **Expected** | `400` or `2xx` | + +## What it sends + +Two leading CRLF sequences before the request-line. + +```http +\r\n +\r\n +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +Two leading CRLF pairs precede the actual request-line. + + +## What the RFC says + +> "In the interest of robustness, a server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." — RFC 9112 Section 2.2 + +This is a SHOULD, not a MUST. The RFC recommends tolerance for robustness, but strict rejection is also acceptable. + +## Pass / Warn explanation + +| Response | Verdict | Reasoning | +|---|---|---| +| `400` or close | Pass | Strict rejection — valid because SHOULD is not MUST | +| `2xx` | Warn | Tolerant behavior — matches the RFC recommendation but flagged for awareness | + +## Why it matters + +Leading CRLFs can appear on persistent connections due to extra bytes after a previous response. The RFC encourages tolerance as a robustness measure. Both strict and tolerant behaviors are acceptable, which is why both produce non-failing verdicts. + +## Deep Analysis + +### ABNF grammar for line endings + +The HTTP message grammar from RFC 9112 Section 2.1: + +``` +HTTP-message = start-line CRLF + *( field-line CRLF ) + CRLF + [ message-body ] +``` + +From RFC 5234 Appendix B.1: + +``` +CRLF = CR LF ; Internet standard newline +CR = %x0D ; carriage return +LF = %x0A ; linefeed +``` + +The `HTTP-message` grammar starts with `start-line` --- there is no provision for leading whitespace or empty lines before the start-line. Any CRLF sequences before the request-line are extra-grammatical; they do not appear in the ABNF. + +### RFC evidence + +**Quote 1 --- The robustness recommendation:** + +> "In the interest of robustness, a server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." --- RFC 9112 Section 2.2 + +The keyword is SHOULD (RFC 2119): the server is recommended to ignore leading CRLFs but is not required to. The phrase "at least one" indicates that ignoring multiple leading CRLFs is also within scope of the recommendation. + +**Quote 2 --- The whitespace-before-header prohibition (by contrast):** + +> "A sender MUST NOT send whitespace between the start-line and the first header field." --- RFC 9112 Section 2.2 + +> "A recipient that receives whitespace between the start-line and the first header field MUST either reject the message as invalid or consume each whitespace-preceded line without further processing." --- RFC 9112 Section 2.2 + +These two sentences are relevant by contrast. The RFC is strict about whitespace **after** the start-line (MUST reject or consume) but lenient about empty lines **before** the start-line (SHOULD ignore). This asymmetry is deliberate: leading CRLFs on persistent connections are a known artifact, while whitespace after the start-line is an attack vector for response splitting. + +**Quote 3 --- The smuggling context:** + +> "Lenient parsing can result in request smuggling security vulnerabilities if there are multiple recipients of the message and each has its own unique interpretation of robustness." --- RFC 9112 Section 3 + +Even though the RFC recommends tolerance for leading CRLFs, the broader security context warns that any lenient parsing can create inconsistencies. If one parser in a chain ignores leading CRLFs and another treats them as the start of an empty request, the resulting disagreement could be exploitable --- though this is a lower-risk scenario compared to bare LF or bare CR. + +### Chain of reasoning + +1. **The payload:** The test sends `\r\n\r\nGET / HTTP/1.1\r\nHost: localhost:8080\r\n\r\n`. Two CRLF pairs precede the actual request-line. +2. **The ABNF mismatch:** The `HTTP-message` grammar starts with `start-line`. Leading CRLFs are not part of the grammar, so a strict parser would not expect them. +3. **The SHOULD recommendation:** RFC 9112 Section 2.2 says the server SHOULD ignore "at least one empty line (CRLF) received prior to the request-line." This payload has two leading CRLFs, which falls within "at least one." +4. **Why tolerance exists:** On persistent (keep-alive) connections, extra bytes after a previous response can manifest as leading CRLFs before the next request. The RFC accounts for this real-world artifact with the SHOULD recommendation. +5. **Why rejection is also valid:** SHOULD is not MUST. A server that rejects with `400` is non-conforming to the recommendation but not violating a requirement. Some security-focused servers may reject to avoid any ambiguity about message boundaries. +6. **Conclusion:** Both behaviors --- ignoring the leading CRLFs and processing the request normally (`2xx`), or rejecting with `400` --- are acceptable. Neither is a failure. + +### Scored / Unscored justification + +This test is **unscored (Pass/Warn, no Fail)**. The requirement level is SHOULD, and both acceptance and rejection are valid: + +- **Pass** for `400` or connection close --- strict rejection is valid because SHOULD is not MUST. The server is declining the robustness recommendation, which is its prerogative. +- **Warn** for `2xx` --- the server is following the RFC's SHOULD recommendation, which is the intended behavior. It receives a Warn (not a Pass) because Http11Probe flags tolerant behaviors for awareness, even when they match the RFC recommendation. +- The test **cannot produce a Fail** because no MUST requirement is at stake. + +The Pass/Warn asymmetry (strict = Pass, tolerant = Warn) may seem counterintuitive since the RFC recommends tolerance. The rationale is that Http11Probe prioritizes security awareness: any leniency in parsing, even RFC-recommended leniency, is worth flagging so operators can make informed decisions about their server's posture. + +## Sources + +- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) diff --git a/docs/content/docs/malformed-input/_index.md b/docs/content/docs/malformed-input/_index.md index 83fcf73..60e423a 100644 --- a/docs/content/docs/malformed-input/_index.md +++ b/docs/content/docs/malformed-input/_index.md @@ -1,6 +1,6 @@ --- title: Malformed Input -description: "Malformed Input — Http11Probe documentation" +description: "Robustness tests for oversized fields, binary garbage, control characters, and incomplete requests that a compliant server must reject without crashing." weight: 11 sidebar: open: false diff --git a/docs/content/docs/malformed-input/binary-garbage.md b/docs/content/docs/malformed-input/binary-garbage.md index 93e443a..2692679 100644 --- a/docs/content/docs/malformed-input/binary-garbage.md +++ b/docs/content/docs/malformed-input/binary-garbage.md @@ -1,86 +1,86 @@ ---- -title: "BINARY-GARBAGE" -description: "BINARY-GARBAGE test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `MAL-BINARY-GARBAGE` | -| **Category** | Malformed Input | -| **Expected** | `400`, close, or timeout | - -## What it sends - -Random binary bytes that do not constitute any valid HTTP message. - -``` -[256 bytes of pseudorandom binary data, seeded RNG(42)] -``` - -Not a valid HTTP request — raw binary bytes with no recognizable structure. - - -## What the RFC says - -> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 - -Random binary bytes do not match the `request-line` grammar (`method SP request-target SP HTTP-version`), so the server SHOULD respond with 400. - -> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error." — RFC 9110 Section 15.5.1 - -## Why timeout is acceptable - -The server receives bytes that cannot be parsed as an HTTP request-line. It may not even determine that a request was attempted. Waiting for more data (and eventually timing out) is valid. - -## Deep Analysis - -### ABNF violation - -The HTTP/1.1 message grammar requires a well-formed start-line as the first element: - -``` -HTTP-message = start-line CRLF - *( field-line CRLF ) - CRLF - [ message-body ] - -request-line = method SP request-target SP HTTP-version -method = token -token = 1*tchar -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" - / "~" / DIGIT / ALPHA -``` - -Random binary bytes will almost certainly contain octets outside the `tchar` set (e.g., NUL `0x00`, control characters `0x01-0x1F`, DEL `0x7F`, high bytes `0x80-0xFF`), and will lack the required `SP` delimiters and `HTTP-version` suffix. The data does not match `request-line` at all. - -### RFC evidence - -> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 - -> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 - -> "A server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." -- RFC 9112 Section 2.2 - -The robustness exception only covers leading blank lines (CRLF), not arbitrary binary content. Once the server encounters bytes that cannot begin a valid `method` token, the grammar match fails immediately. - -### Chain of reasoning - -1. The server opens a TCP connection and begins reading octets. -2. It attempts to match the incoming bytes against `request-line = method SP request-target SP HTTP-version`. -3. The `method` production requires `1*tchar`, but random binary data contains octets outside the `tchar` character set (control characters, high bytes, NUL). -4. The grammar match fails at the very first non-tchar octet. -5. Per RFC 9112 Section 2.2, the server SHOULD respond with 400 and close the connection. -6. Alternatively, if the server has not yet identified any request boundary, it may wait for more data and eventually time out -- this is also acceptable since no complete request was ever formed. - -### Security implications - -- **Protocol confusion**: Binary data could be an attempt to speak a non-HTTP protocol (e.g., TLS ClientHello, SMTP, or a custom binary protocol) on an HTTP port. Accepting and processing such data risks protocol-level confusion. -- **Parser exploitation**: Naive parsers that do not validate the `tchar` constraint may attempt to interpret binary data as HTTP, potentially triggering buffer overflows, out-of-bounds reads, or undefined behavior in string operations. -- **Resource exhaustion**: If the server buffers binary data waiting for CRLF delimiters that never arrive, it may consume memory indefinitely. Proper timeout and size limits are essential. - -## Sources - -- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — message parsing robustness -- [RFC 9110 Section 15.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.1) — 400 Bad Request +--- +title: "Binary Garbage — Malformed Input" +description: "Random binary bytes that do not constitute any valid HTTP message. Part of Http11Probe's malformed input robustness suite." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `MAL-BINARY-GARBAGE` | +| **Category** | Malformed Input | +| **Expected** | `400`, close, or timeout | + +## What it sends + +Random binary bytes that do not constitute any valid HTTP message. + +``` +[256 bytes of pseudorandom binary data, seeded RNG(42)] +``` + +Not a valid HTTP request — raw binary bytes with no recognizable structure. + + +## What the RFC says + +> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 + +Random binary bytes do not match the `request-line` grammar (`method SP request-target SP HTTP-version`), so the server SHOULD respond with 400. + +> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error." — RFC 9110 Section 15.5.1 + +## Why timeout is acceptable + +The server receives bytes that cannot be parsed as an HTTP request-line. It may not even determine that a request was attempted. Waiting for more data (and eventually timing out) is valid. + +## Deep Analysis + +### ABNF violation + +The HTTP/1.1 message grammar requires a well-formed start-line as the first element: + +``` +HTTP-message = start-line CRLF + *( field-line CRLF ) + CRLF + [ message-body ] + +request-line = method SP request-target SP HTTP-version +method = token +token = 1*tchar +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" + / "~" / DIGIT / ALPHA +``` + +Random binary bytes will almost certainly contain octets outside the `tchar` set (e.g., NUL `0x00`, control characters `0x01-0x1F`, DEL `0x7F`, high bytes `0x80-0xFF`), and will lack the required `SP` delimiters and `HTTP-version` suffix. The data does not match `request-line` at all. + +### RFC evidence + +> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 + +> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 + +> "A server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." -- RFC 9112 Section 2.2 + +The robustness exception only covers leading blank lines (CRLF), not arbitrary binary content. Once the server encounters bytes that cannot begin a valid `method` token, the grammar match fails immediately. + +### Chain of reasoning + +1. The server opens a TCP connection and begins reading octets. +2. It attempts to match the incoming bytes against `request-line = method SP request-target SP HTTP-version`. +3. The `method` production requires `1*tchar`, but random binary data contains octets outside the `tchar` character set (control characters, high bytes, NUL). +4. The grammar match fails at the very first non-tchar octet. +5. Per RFC 9112 Section 2.2, the server SHOULD respond with 400 and close the connection. +6. Alternatively, if the server has not yet identified any request boundary, it may wait for more data and eventually time out -- this is also acceptable since no complete request was ever formed. + +### Security implications + +- **Protocol confusion**: Binary data could be an attempt to speak a non-HTTP protocol (e.g., TLS ClientHello, SMTP, or a custom binary protocol) on an HTTP port. Accepting and processing such data risks protocol-level confusion. +- **Parser exploitation**: Naive parsers that do not validate the `tchar` constraint may attempt to interpret binary data as HTTP, potentially triggering buffer overflows, out-of-bounds reads, or undefined behavior in string operations. +- **Resource exhaustion**: If the server buffers binary data waiting for CRLF delimiters that never arrive, it may consume memory indefinitely. Proper timeout and size limits are essential. + +## Sources + +- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — message parsing robustness +- [RFC 9110 Section 15.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.1) — 400 Bad Request diff --git a/docs/content/docs/malformed-input/chunk-extension-long.md b/docs/content/docs/malformed-input/chunk-extension-long.md index 21e1c7a..9e31a30 100644 --- a/docs/content/docs/malformed-input/chunk-extension-long.md +++ b/docs/content/docs/malformed-input/chunk-extension-long.md @@ -1,101 +1,101 @@ ---- -title: "CHUNK-EXT-64K" -description: "CHUNK-EXT-64K test documentation" -weight: 18 ---- - -| | | -|---|---| -| **Test ID** | `MAL-CHUNK-EXT-64K` | -| **Category** | Malformed Input | -| **Expected** | `400`/`431` = Pass, `2xx` = Warn, close = Pass | - -## What it sends - -A chunked request with a chunk extension containing 64KB (65,536 bytes) of data. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -\r\n -5;ext=aaaa...{65,536 x 'a'}...\r\n -hello\r\n -0\r\n -\r\n -``` - -The chunk extension value is 65,536 bytes of `a` characters. - - -## What the RFC says - -Chunk extensions are syntactically valid per the ABNF: - -> `chunk-ext = *( BWS ";" BWS chunk-ext-name [ BWS "=" BWS chunk-ext-val ] )` — RFC 9112 Section 7.1.1 - -> `chunk-ext-name = token` — RFC 9112 Section 7.1.1 - -> `chunk-ext-val = token / quoted-string` — RFC 9112 Section 7.1.1 - -However, the RFC explicitly recommends limiting their size: - -> "A recipient MUST ignore unrecognized chunk extensions. A server ought to limit the total length of chunk extensions received in a request to an amount reasonable for the services provided, in the same way that it applies length limitations and timeouts for other parts of a message, and generate an appropriate 4xx (Client Error) response if that amount is exceeded." — RFC 9112 Section 7.1.1 - -## Pass/Warn explanation - -- **Pass (400/431):** The server rejects the oversized chunk extension, following the RFC recommendation to limit extension length. -- **Warn (2xx):** The server accepted the 64KB extension. While syntactically valid, accepting such large extensions without limits is a resource exhaustion risk. - -## Why it matters - -While chunk extensions are syntactically valid, a 64KB extension is pathological. CVE-2023-39326 demonstrated that Go's `net/http` library could be exploited via large chunk extensions to cause excessive memory consumption and DoS. A robust server should limit chunk extension size. - -## Deep Analysis - -### ABNF context - -The chunk extension grammar is syntactically permissive: - -``` -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-ext = *( BWS ";" BWS chunk-ext-name - [ BWS "=" BWS chunk-ext-val ] ) - -chunk-ext-name = token -chunk-ext-val = token / quoted-string -token = 1*tchar -``` - -A 64KB string of `a` characters is a valid `token` per the grammar -- each `a` is an `ALPHA` and therefore a valid `tchar`. The extension `ext=aaaa...` is syntactically correct. There is no ABNF upper bound on extension length. - -### RFC evidence - -> "A server ought to limit the total length of chunk extensions received in a request to an amount reasonable for the services provided, in the same way that it applies length limitations and timeouts for other parts of a message, and generate an appropriate 4xx (Client Error) response if that amount is exceeded." -- RFC 9112 Section 7.1.1 - -> "A recipient MUST ignore unrecognized chunk extensions." -- RFC 9112 Section 7.1.1 - -> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 - -The first quote is the key normative guidance: while the grammar allows unlimited extension length, the RFC explicitly instructs servers to impose practical limits and respond with a 4xx error when those limits are exceeded. A 64KB extension is far beyond any reasonable limit. - -### Chain of reasoning - -1. The client sends a chunked POST with `5;ext=aaaa...` where the extension value is 65,536 bytes. -2. The server parses the chunk-size (`5`) and then encounters the chunk-ext production. -3. Per the ABNF, it reads `; ext = aaaa...` -- syntactically valid but extraordinarily long. -4. A well-implemented server enforces a length limit on chunk extensions (just as it limits header sizes and request-line length) and rejects the request with 400 or 431. -5. A server that does not limit extension length will buffer 64KB of useless metadata per chunk, creating a denial-of-service vector. - -### Security implications - -- **Denial of service (CVE-2023-39326)**: Go's `net/http` library prior to the fix allowed attackers to send requests with very large chunk extensions, causing excessive memory allocation. An attacker could send many chunks, each with a large extension, amplifying memory consumption far beyond the actual body size. -- **Memory amplification**: The body data is only 5 bytes (`hello`), but the chunk metadata is 64KB. A stream of such chunks forces the server to allocate orders of magnitude more memory for metadata than for payload. -- **Slowloris-style attacks**: Large extensions slow down chunk parsing and keep connections open longer, reducing the server's capacity to handle legitimate requests. -- **Proxy bypass**: Intermediaries that strip or ignore chunk extensions may forward only the 5-byte body, while the origin server is still burdened by parsing the extension -- creating an asymmetric resource consumption attack. - -## Sources - -- [RFC 9112 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) — chunk extensions and size limits -- [CVE-2023-39326](https://nvd.nist.gov/vuln/detail/CVE-2023-39326) — Go net/http chunk extension DoS +--- +title: "Chunk Extension Long — Malformed Input" +description: "A chunked request with a chunk extension containing 64KB (65,536 bytes) of data. Part of Http11Probe's malformed input robustness suite." +weight: 18 +--- + +| | | +|---|---| +| **Test ID** | `MAL-CHUNK-EXT-64K` | +| **Category** | Malformed Input | +| **Expected** | `400`/`431` = Pass, `2xx` = Warn, close = Pass | + +## What it sends + +A chunked request with a chunk extension containing 64KB (65,536 bytes) of data. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +5;ext=aaaa...{65,536 x 'a'}...\r\n +hello\r\n +0\r\n +\r\n +``` + +The chunk extension value is 65,536 bytes of `a` characters. + + +## What the RFC says + +Chunk extensions are syntactically valid per the ABNF: + +> `chunk-ext = *( BWS ";" BWS chunk-ext-name [ BWS "=" BWS chunk-ext-val ] )` — RFC 9112 Section 7.1.1 + +> `chunk-ext-name = token` — RFC 9112 Section 7.1.1 + +> `chunk-ext-val = token / quoted-string` — RFC 9112 Section 7.1.1 + +However, the RFC explicitly recommends limiting their size: + +> "A recipient MUST ignore unrecognized chunk extensions. A server ought to limit the total length of chunk extensions received in a request to an amount reasonable for the services provided, in the same way that it applies length limitations and timeouts for other parts of a message, and generate an appropriate 4xx (Client Error) response if that amount is exceeded." — RFC 9112 Section 7.1.1 + +## Pass/Warn explanation + +- **Pass (400/431):** The server rejects the oversized chunk extension, following the RFC recommendation to limit extension length. +- **Warn (2xx):** The server accepted the 64KB extension. While syntactically valid, accepting such large extensions without limits is a resource exhaustion risk. + +## Why it matters + +While chunk extensions are syntactically valid, a 64KB extension is pathological. CVE-2023-39326 demonstrated that Go's `net/http` library could be exploited via large chunk extensions to cause excessive memory consumption and DoS. A robust server should limit chunk extension size. + +## Deep Analysis + +### ABNF context + +The chunk extension grammar is syntactically permissive: + +``` +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-ext = *( BWS ";" BWS chunk-ext-name + [ BWS "=" BWS chunk-ext-val ] ) + +chunk-ext-name = token +chunk-ext-val = token / quoted-string +token = 1*tchar +``` + +A 64KB string of `a` characters is a valid `token` per the grammar -- each `a` is an `ALPHA` and therefore a valid `tchar`. The extension `ext=aaaa...` is syntactically correct. There is no ABNF upper bound on extension length. + +### RFC evidence + +> "A server ought to limit the total length of chunk extensions received in a request to an amount reasonable for the services provided, in the same way that it applies length limitations and timeouts for other parts of a message, and generate an appropriate 4xx (Client Error) response if that amount is exceeded." -- RFC 9112 Section 7.1.1 + +> "A recipient MUST ignore unrecognized chunk extensions." -- RFC 9112 Section 7.1.1 + +> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 + +The first quote is the key normative guidance: while the grammar allows unlimited extension length, the RFC explicitly instructs servers to impose practical limits and respond with a 4xx error when those limits are exceeded. A 64KB extension is far beyond any reasonable limit. + +### Chain of reasoning + +1. The client sends a chunked POST with `5;ext=aaaa...` where the extension value is 65,536 bytes. +2. The server parses the chunk-size (`5`) and then encounters the chunk-ext production. +3. Per the ABNF, it reads `; ext = aaaa...` -- syntactically valid but extraordinarily long. +4. A well-implemented server enforces a length limit on chunk extensions (just as it limits header sizes and request-line length) and rejects the request with 400 or 431. +5. A server that does not limit extension length will buffer 64KB of useless metadata per chunk, creating a denial-of-service vector. + +### Security implications + +- **Denial of service (CVE-2023-39326)**: Go's `net/http` library prior to the fix allowed attackers to send requests with very large chunk extensions, causing excessive memory allocation. An attacker could send many chunks, each with a large extension, amplifying memory consumption far beyond the actual body size. +- **Memory amplification**: The body data is only 5 bytes (`hello`), but the chunk metadata is 64KB. A stream of such chunks forces the server to allocate orders of magnitude more memory for metadata than for payload. +- **Slowloris-style attacks**: Large extensions slow down chunk parsing and keep connections open longer, reducing the server's capacity to handle legitimate requests. +- **Proxy bypass**: Intermediaries that strip or ignore chunk extensions may forward only the 5-byte body, while the origin server is still burdened by parsing the extension -- creating an asymmetric resource consumption attack. + +## Sources + +- [RFC 9112 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) — chunk extensions and size limits +- [CVE-2023-39326](https://nvd.nist.gov/vuln/detail/CVE-2023-39326) — Go net/http chunk extension DoS diff --git a/docs/content/docs/malformed-input/chunk-size-overflow.md b/docs/content/docs/malformed-input/chunk-size-overflow.md index 54ae1fb..30fc399 100644 --- a/docs/content/docs/malformed-input/chunk-size-overflow.md +++ b/docs/content/docs/malformed-input/chunk-size-overflow.md @@ -1,93 +1,93 @@ ---- -title: "CHUNK-SIZE-OVERFLOW" -description: "CHUNK-SIZE-OVERFLOW test documentation" -weight: 16 ---- - -| | | -|---|---| -| **Test ID** | `MAL-CHUNK-SIZE-OVERFLOW` | -| **Category** | Malformed Input | -| **Expected** | `400` or close | - -## What it sends - -A chunked request with a chunk size of `FFFFFFFFFFFFFFFF0` — a value exceeding the maximum 64-bit unsigned integer. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -\r\n -FFFFFFFFFFFFFFFF0\r\n -hello\r\n -0\r\n -\r\n -``` - -The chunk size `FFFFFFFFFFFFFFFF0` (17 hex digits) exceeds the 64-bit unsigned integer range. - - -## What the RFC says - -> `chunk-size = 1*HEXDIG` — RFC 9112 Section 7.1 - -> "Recipients MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer representation." — RFC 9112 Section 7.1 - -`FFFFFFFFFFFFFFFF0` (17 hex digits) exceeds the 64-bit unsigned integer range (max `FFFFFFFFFFFFFFFF` = 16 hex digits), making it impossible to interpret as a valid chunk size. - -## Why it matters - -Integer overflow in chunk size parsing can lead to incorrect body length calculation, buffer overflows, or server crashes. A robust server must detect overflow and reject the request. - -## Deep Analysis - -### ABNF context - -The chunk-size grammar is deceptively simple: - -``` -chunked-body = *chunk - last-chunk - trailer-section - CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG -last-chunk = 1*("0") [ chunk-ext ] CRLF -``` - -The production `chunk-size = 1*HEXDIG` permits any number of hexadecimal digits with no upper bound on the string length. The value `FFFFFFFFFFFFFFFF0` (17 hex digits) is grammatically valid -- it matches `1*HEXDIG`. However, its numeric value (4,722,366,482,869,645,213,680 in decimal) exceeds the maximum representable 64-bit unsigned integer (`FFFFFFFFFFFFFFFF` = 18,446,744,073,709,551,615). - -### RFC evidence - -> "Recipients MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer representation." -- RFC 9112 Section 7.1 - -> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 - -> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 - -The first quote is critical: the RFC explicitly warns implementers about integer overflow in chunk-size parsing and mandates that recipients prevent parsing errors from overflow. This is a direct instruction to detect and handle this exact attack. - -### Chain of reasoning - -1. The server receives a chunked POST and begins parsing the chunk-size field. -2. It reads `FFFFFFFFFFFFFFFF0` -- 17 hex digits that match `1*HEXDIG` syntactically. -3. The server attempts to convert this hex string to an integer for use as the chunk-data length. -4. If the server uses a 64-bit unsigned integer, the value overflows (17 hex digits exceed 16-digit `FFFFFFFFFFFFFFFF`). -5. Per RFC 9112 Section 7.1, the server MUST anticipate this overflow and prevent parsing errors. -6. The only safe action is to reject the request with 400 and close the connection, since no valid chunk-data length can be derived. -7. If the server instead wraps the value (e.g., truncates to 64 bits), it would compute a much smaller chunk-data length, reading the wrong number of bytes and corrupting message framing. - -### Security implications - -- **Integer overflow exploitation**: If a parser silently wraps the overflowed value, the computed chunk-data length will be much smaller than intended. The server reads fewer bytes as chunk-data, then interprets the remaining bytes as the next chunk or as a new request -- a classic request smuggling vector. -- **Buffer overflow**: In languages without automatic bounds checking (C, C++), an overflowed chunk-size could lead to heap or stack buffer overflows if the truncated value is used to allocate or index memory. -- **Denial of service**: If the parser interprets the overflowed value as a very large allocation request, it may attempt to allocate gigabytes of memory, causing OOM crashes. -- **Differential parsing**: Different implementations may truncate the hex value at different widths (32-bit vs. 64-bit), leading to disagreements between a proxy and a backend about where the chunk data ends -- enabling smuggling through infrastructure. - -## Sources - -- [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) — chunk-size = 1*HEXDIG -- [RFC 9110 Section 15.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.1) — 400 Bad Request +--- +title: "Chunk Size Overflow — Malformed Input" +description: "A chunked request with a chunk size of FFFFFFFFFFFFFFFF0 — a value exceeding the maximum 64-bit unsigned integer. Part of Http11Probe's malformed input robustness suite." +weight: 16 +--- + +| | | +|---|---| +| **Test ID** | `MAL-CHUNK-SIZE-OVERFLOW` | +| **Category** | Malformed Input | +| **Expected** | `400` or close | + +## What it sends + +A chunked request with a chunk size of `FFFFFFFFFFFFFFFF0` — a value exceeding the maximum 64-bit unsigned integer. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +FFFFFFFFFFFFFFFF0\r\n +hello\r\n +0\r\n +\r\n +``` + +The chunk size `FFFFFFFFFFFFFFFF0` (17 hex digits) exceeds the 64-bit unsigned integer range. + + +## What the RFC says + +> `chunk-size = 1*HEXDIG` — RFC 9112 Section 7.1 + +> "Recipients MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer representation." — RFC 9112 Section 7.1 + +`FFFFFFFFFFFFFFFF0` (17 hex digits) exceeds the 64-bit unsigned integer range (max `FFFFFFFFFFFFFFFF` = 16 hex digits), making it impossible to interpret as a valid chunk size. + +## Why it matters + +Integer overflow in chunk size parsing can lead to incorrect body length calculation, buffer overflows, or server crashes. A robust server must detect overflow and reject the request. + +## Deep Analysis + +### ABNF context + +The chunk-size grammar is deceptively simple: + +``` +chunked-body = *chunk + last-chunk + trailer-section + CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG +last-chunk = 1*("0") [ chunk-ext ] CRLF +``` + +The production `chunk-size = 1*HEXDIG` permits any number of hexadecimal digits with no upper bound on the string length. The value `FFFFFFFFFFFFFFFF0` (17 hex digits) is grammatically valid -- it matches `1*HEXDIG`. However, its numeric value (4,722,366,482,869,645,213,680 in decimal) exceeds the maximum representable 64-bit unsigned integer (`FFFFFFFFFFFFFFFF` = 18,446,744,073,709,551,615). + +### RFC evidence + +> "Recipients MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer representation." -- RFC 9112 Section 7.1 + +> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 + +> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 + +The first quote is critical: the RFC explicitly warns implementers about integer overflow in chunk-size parsing and mandates that recipients prevent parsing errors from overflow. This is a direct instruction to detect and handle this exact attack. + +### Chain of reasoning + +1. The server receives a chunked POST and begins parsing the chunk-size field. +2. It reads `FFFFFFFFFFFFFFFF0` -- 17 hex digits that match `1*HEXDIG` syntactically. +3. The server attempts to convert this hex string to an integer for use as the chunk-data length. +4. If the server uses a 64-bit unsigned integer, the value overflows (17 hex digits exceed 16-digit `FFFFFFFFFFFFFFFF`). +5. Per RFC 9112 Section 7.1, the server MUST anticipate this overflow and prevent parsing errors. +6. The only safe action is to reject the request with 400 and close the connection, since no valid chunk-data length can be derived. +7. If the server instead wraps the value (e.g., truncates to 64 bits), it would compute a much smaller chunk-data length, reading the wrong number of bytes and corrupting message framing. + +### Security implications + +- **Integer overflow exploitation**: If a parser silently wraps the overflowed value, the computed chunk-data length will be much smaller than intended. The server reads fewer bytes as chunk-data, then interprets the remaining bytes as the next chunk or as a new request -- a classic request smuggling vector. +- **Buffer overflow**: In languages without automatic bounds checking (C, C++), an overflowed chunk-size could lead to heap or stack buffer overflows if the truncated value is used to allocate or index memory. +- **Denial of service**: If the parser interprets the overflowed value as a very large allocation request, it may attempt to allocate gigabytes of memory, causing OOM crashes. +- **Differential parsing**: Different implementations may truncate the hex value at different widths (32-bit vs. 64-bit), leading to disagreements between a proxy and a backend about where the chunk data ends -- enabling smuggling through infrastructure. + +## Sources + +- [RFC 9112 Section 7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) — chunk-size = 1*HEXDIG +- [RFC 9110 Section 15.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.1) — 400 Bad Request diff --git a/docs/content/docs/malformed-input/cl-empty.md b/docs/content/docs/malformed-input/cl-empty.md index 8f1c590..f727bc2 100644 --- a/docs/content/docs/malformed-input/cl-empty.md +++ b/docs/content/docs/malformed-input/cl-empty.md @@ -1,86 +1,86 @@ ---- -title: "CL-EMPTY" -description: "CL-EMPTY test documentation" -weight: 19 ---- - -| | | -|---|---| -| **Test ID** | `MAL-CL-EMPTY` | -| **Category** | Malformed Input | -| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -`Content-Length: ` — a Content-Length header with an empty value (just whitespace after the colon). - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: \r\n -\r\n -``` - -The Content-Length header has an empty value (no digits). - - -## What the RFC says - -> `Content-Length = 1*DIGIT` — RFC 9110 Section 8.6 - -The ABNF `1*DIGIT` requires at least one digit. An empty value (zero digits) does not match this grammar and indicates invalid message framing. - -> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error." — RFC 9110 Section 15.5.1 - -## Why it matters - -Parsers that treat an empty Content-Length as `0` will read no body, while others may reject it or wait for data. This disagreement between parsers can be exploited for smuggling when the request also carries a body. - -## Deep Analysis - -### ABNF violation - -The Content-Length field has a strict grammar: - -``` -Content-Length = 1*DIGIT - -field-line = field-name ":" OWS field-value OWS -OWS = *( SP / HTAB ) -``` - -After stripping OWS from `Content-Length: \r\n`, the field-value is the empty string `""`. The ABNF `1*DIGIT` requires **at least one** digit (`1*` means "one or more"). Zero digits does not match this production. The empty string is not a valid Content-Length value. - -### RFC evidence - -> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 - -> "A sender MUST NOT generate protocol elements that do not match the grammar defined by the corresponding ABNF rules." -- RFC 9110 Section 5.5 - -> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 - -> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 - -The empty Content-Length value is an unambiguous ABNF violation. The `1*DIGIT` production does not have a "zero digits means zero length" interpretation -- it simply does not match. - -### Chain of reasoning - -1. The server receives a POST request with `Content-Length: \r\n`. -2. It parses the field-line: `field-name` is `Content-Length`, then `:`, then OWS (the trailing space), then `field-value`, then OWS. -3. After stripping OWS, the field-value is the empty string. -4. The server attempts to match the empty string against `Content-Length = 1*DIGIT`. -5. The match fails: zero digits do not satisfy `1*DIGIT`. -6. Since the Content-Length value is invalid, the server cannot determine message framing -- it does not know how many body bytes to expect. -7. Per RFC 9112 Section 2.2, the server SHOULD reject with 400 and close the connection. - -### Security implications - -- **Request smuggling**: If the front-end proxy interprets an empty Content-Length as `0` (no body), but the back-end server waits for body data or rejects differently, the proxy may forward subsequent requests that the back-end interprets as part of the first request's body -- or vice versa. This Content-Length disagreement is a classic smuggling primitive. -- **Parser divergence**: Different implementations handle this edge case differently: some treat empty as `0`, some reject it, some treat it as a missing header. Each interpretation leads to different framing decisions, creating desynchronization opportunities. -- **Body confusion**: A POST with an empty Content-Length and an actual body payload creates ambiguity: does the body exist or not? If the server reads zero bytes but the client sent data, that data sits in the TCP buffer and may be interpreted as the next request. - -## Sources - -- [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +--- +title: "CL Empty — Malformed Input" +description: "Content-Length: — a Content-Length header with an empty value (just whitespace after the colon). Tested against RFC 9110 Section 8.6." +weight: 19 +--- + +| | | +|---|---| +| **Test ID** | `MAL-CL-EMPTY` | +| **Category** | Malformed Input | +| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +`Content-Length: ` — a Content-Length header with an empty value (just whitespace after the colon). + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: \r\n +\r\n +``` + +The Content-Length header has an empty value (no digits). + + +## What the RFC says + +> `Content-Length = 1*DIGIT` — RFC 9110 Section 8.6 + +The ABNF `1*DIGIT` requires at least one digit. An empty value (zero digits) does not match this grammar and indicates invalid message framing. + +> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error." — RFC 9110 Section 15.5.1 + +## Why it matters + +Parsers that treat an empty Content-Length as `0` will read no body, while others may reject it or wait for data. This disagreement between parsers can be exploited for smuggling when the request also carries a body. + +## Deep Analysis + +### ABNF violation + +The Content-Length field has a strict grammar: + +``` +Content-Length = 1*DIGIT + +field-line = field-name ":" OWS field-value OWS +OWS = *( SP / HTAB ) +``` + +After stripping OWS from `Content-Length: \r\n`, the field-value is the empty string `""`. The ABNF `1*DIGIT` requires **at least one** digit (`1*` means "one or more"). Zero digits does not match this production. The empty string is not a valid Content-Length value. + +### RFC evidence + +> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 + +> "A sender MUST NOT generate protocol elements that do not match the grammar defined by the corresponding ABNF rules." -- RFC 9110 Section 5.5 + +> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 + +> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 + +The empty Content-Length value is an unambiguous ABNF violation. The `1*DIGIT` production does not have a "zero digits means zero length" interpretation -- it simply does not match. + +### Chain of reasoning + +1. The server receives a POST request with `Content-Length: \r\n`. +2. It parses the field-line: `field-name` is `Content-Length`, then `:`, then OWS (the trailing space), then `field-value`, then OWS. +3. After stripping OWS, the field-value is the empty string. +4. The server attempts to match the empty string against `Content-Length = 1*DIGIT`. +5. The match fails: zero digits do not satisfy `1*DIGIT`. +6. Since the Content-Length value is invalid, the server cannot determine message framing -- it does not know how many body bytes to expect. +7. Per RFC 9112 Section 2.2, the server SHOULD reject with 400 and close the connection. + +### Security implications + +- **Request smuggling**: If the front-end proxy interprets an empty Content-Length as `0` (no body), but the back-end server waits for body data or rejects differently, the proxy may forward subsequent requests that the back-end interprets as part of the first request's body -- or vice versa. This Content-Length disagreement is a classic smuggling primitive. +- **Parser divergence**: Different implementations handle this edge case differently: some treat empty as `0`, some reject it, some treat it as a missing header. Each interpretation leads to different framing decisions, creating desynchronization opportunities. +- **Body confusion**: A POST with an empty Content-Length and an actual body payload creates ambiguity: does the body exist or not? If the server reads zero bytes but the client sent data, that data sits in the TCP buffer and may be interpreted as the next request. + +## Sources + +- [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) diff --git a/docs/content/docs/malformed-input/cl-overflow.md b/docs/content/docs/malformed-input/cl-overflow.md index 5fb4565..b704069 100644 --- a/docs/content/docs/malformed-input/cl-overflow.md +++ b/docs/content/docs/malformed-input/cl-overflow.md @@ -1,84 +1,84 @@ ---- -title: "CL-OVERFLOW" -description: "CL-OVERFLOW test documentation" -weight: 11 ---- - -| | | -|---|---| -| **Test ID** | `MAL-CL-OVERFLOW` | -| **Category** | Malformed Input | -| **Expected** | `400` or close | - -## What it sends - -A `Content-Length` value exceeding the 64-bit integer range (e.g., `99999999999999999999`). - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 99999999999999999999\r\n -\r\n -``` - -The Content-Length value exceeds 64-bit integer range. - - -## What the RFC says - -> `Content-Length = 1*DIGIT` — RFC 9110 Section 8.6 - -While the grammar allows any number of digits, the value must represent a valid decimal number for the message body length. A value like `99999999999999999999` exceeds the 64-bit unsigned integer range (max 18,446,744,073,709,551,615), making it impossible to interpret as a body length. - -> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error." — RFC 9110 Section 15.5.1 - -## Why it matters - -If a parser uses a fixed-width integer without overflow checking, the parsed value wraps around. This can lead to reading a different amount of body data than intended -- a smuggling vector. - -## Deep Analysis - -### ABNF context - -The Content-Length grammar is simple but has no upper bound: - -``` -Content-Length = 1*DIGIT -DIGIT = %x30-39 ; 0-9 -``` - -The value `99999999999999999999` (20 decimal digits) matches `1*DIGIT` syntactically -- every character is a valid digit. However, the numeric value (9.999... x 10^19) exceeds the maximum 64-bit unsigned integer (18,446,744,073,709,551,615 = ~1.844 x 10^19). While the ABNF is satisfied, the value cannot be represented in any standard integer type and cannot correspond to an actual body length. - -### RFC evidence - -> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 - -> "A sender MUST NOT send a Content-Length header field in any message that contains a Transfer-Encoding header field." -- RFC 9112 Section 6.1 - -> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 - -> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 - -While `1*DIGIT` has no syntactic upper bound, the value must represent a meaningful body length. A value that overflows the server's integer type constitutes "invalid request message framing" -- the server cannot determine where the body ends. - -### Chain of reasoning - -1. The server receives a POST with `Content-Length: 99999999999999999999`. -2. It parses the field-value and attempts to convert `99999999999999999999` to an integer. -3. The value exceeds the 64-bit unsigned integer maximum (2^64 - 1 = 18,446,744,073,709,551,615). -4. If the server detects the overflow, it rejects with 400 -- the correct behavior. -5. If the server does NOT detect the overflow and uses a 64-bit integer, the parsed value wraps around to `99999999999999999999 mod 2^64 = 3,553,255,926,290,448,383` -- a completely different body length. -6. The server would then attempt to read ~3.5 exabytes of body data (still impossibly large) or, if further truncated to 32 bits, a much smaller value that could actually be satisfied by subsequent data on the connection. -7. This framing corruption enables smuggling: the server reads a different number of bytes than the client intended, and leftover bytes become the "next request." - -### Security implications - -- **Request smuggling via integer wrap**: If the overflowed Content-Length wraps to a small value (especially on 32-bit systems where `99999999999999999999 mod 2^32 = 3,567,587,327`), an attacker can craft a payload where the first N bytes satisfy the wrapped Content-Length, and the remaining bytes are interpreted as a new smuggled request. -- **Memory exhaustion**: If the server trusts the parsed (pre-overflow) value and attempts to allocate a buffer of that size, it will try to allocate more memory than physically exists, causing an OOM crash. -- **Differential parsing**: A 64-bit proxy and a 32-bit backend will compute different wrapped values from the same Content-Length, leading to framing disagreement and smuggling opportunities. -- **Timeout-based DoS**: A server that accepts the value and begins waiting for ~10^19 bytes of body data will hold the connection open indefinitely, consuming a socket and associated resources. - -## Sources - -- [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) — Content-Length grammar -- [RFC 9110 Section 15.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.1) — 400 Bad Request +--- +title: "CL Overflow — Malformed Input" +description: "A Content-Length value exceeding the 64-bit integer range (e.g., 99999999999999999999). Part of Http11Probe's malformed input robustness suite." +weight: 11 +--- + +| | | +|---|---| +| **Test ID** | `MAL-CL-OVERFLOW` | +| **Category** | Malformed Input | +| **Expected** | `400` or close | + +## What it sends + +A `Content-Length` value exceeding the 64-bit integer range (e.g., `99999999999999999999`). + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 99999999999999999999\r\n +\r\n +``` + +The Content-Length value exceeds 64-bit integer range. + + +## What the RFC says + +> `Content-Length = 1*DIGIT` — RFC 9110 Section 8.6 + +While the grammar allows any number of digits, the value must represent a valid decimal number for the message body length. A value like `99999999999999999999` exceeds the 64-bit unsigned integer range (max 18,446,744,073,709,551,615), making it impossible to interpret as a body length. + +> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error." — RFC 9110 Section 15.5.1 + +## Why it matters + +If a parser uses a fixed-width integer without overflow checking, the parsed value wraps around. This can lead to reading a different amount of body data than intended -- a smuggling vector. + +## Deep Analysis + +### ABNF context + +The Content-Length grammar is simple but has no upper bound: + +``` +Content-Length = 1*DIGIT +DIGIT = %x30-39 ; 0-9 +``` + +The value `99999999999999999999` (20 decimal digits) matches `1*DIGIT` syntactically -- every character is a valid digit. However, the numeric value (9.999... x 10^19) exceeds the maximum 64-bit unsigned integer (18,446,744,073,709,551,615 = ~1.844 x 10^19). While the ABNF is satisfied, the value cannot be represented in any standard integer type and cannot correspond to an actual body length. + +### RFC evidence + +> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 + +> "A sender MUST NOT send a Content-Length header field in any message that contains a Transfer-Encoding header field." -- RFC 9112 Section 6.1 + +> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 + +> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 + +While `1*DIGIT` has no syntactic upper bound, the value must represent a meaningful body length. A value that overflows the server's integer type constitutes "invalid request message framing" -- the server cannot determine where the body ends. + +### Chain of reasoning + +1. The server receives a POST with `Content-Length: 99999999999999999999`. +2. It parses the field-value and attempts to convert `99999999999999999999` to an integer. +3. The value exceeds the 64-bit unsigned integer maximum (2^64 - 1 = 18,446,744,073,709,551,615). +4. If the server detects the overflow, it rejects with 400 -- the correct behavior. +5. If the server does NOT detect the overflow and uses a 64-bit integer, the parsed value wraps around to `99999999999999999999 mod 2^64 = 3,553,255,926,290,448,383` -- a completely different body length. +6. The server would then attempt to read ~3.5 exabytes of body data (still impossibly large) or, if further truncated to 32 bits, a much smaller value that could actually be satisfied by subsequent data on the connection. +7. This framing corruption enables smuggling: the server reads a different number of bytes than the client intended, and leftover bytes become the "next request." + +### Security implications + +- **Request smuggling via integer wrap**: If the overflowed Content-Length wraps to a small value (especially on 32-bit systems where `99999999999999999999 mod 2^32 = 3,567,587,327`), an attacker can craft a payload where the first N bytes satisfy the wrapped Content-Length, and the remaining bytes are interpreted as a new smuggled request. +- **Memory exhaustion**: If the server trusts the parsed (pre-overflow) value and attempts to allocate a buffer of that size, it will try to allocate more memory than physically exists, causing an OOM crash. +- **Differential parsing**: A 64-bit proxy and a 32-bit backend will compute different wrapped values from the same Content-Length, leading to framing disagreement and smuggling opportunities. +- **Timeout-based DoS**: A server that accepts the value and begins waiting for ~10^19 bytes of body data will hold the connection open indefinitely, consuming a socket and associated resources. + +## Sources + +- [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) — Content-Length grammar +- [RFC 9110 Section 15.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.1) — 400 Bad Request diff --git a/docs/content/docs/malformed-input/cl-tab-before-value.md b/docs/content/docs/malformed-input/cl-tab-before-value.md index 8542869..b7fec94 100644 --- a/docs/content/docs/malformed-input/cl-tab-before-value.md +++ b/docs/content/docs/malformed-input/cl-tab-before-value.md @@ -1,99 +1,99 @@ ---- -title: "CL-TAB-BEFORE-VALUE" -description: "CL-TAB-BEFORE-VALUE test documentation" -weight: 20 ---- - -| | | -|---|---| -| **Test ID** | `MAL-CL-TAB-BEFORE-VALUE` | -| **Category** | Malformed Input | -| **RFC** | [RFC 9110 Section 5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) | -| **Requirement** | valid per RFC | -| **Expected** | `400` preferred; `2xx` is a warning | - -## What it sends - -`Content-Length:\t5` — a Content-Length header where a horizontal tab character separates the colon from the value, instead of a space. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length:\t5\r\n -\r\n -hello -``` - -A horizontal tab character (`\t` / `0x09`) separates the colon from the value instead of a space. - - -## What the RFC says - -The field-line grammar explicitly includes optional whitespace between the colon and value: - -> `field-line = field-name ":" OWS field-value OWS` — RFC 9112 Section 5 - -And OWS permits both spaces and horizontal tabs: - -> `OWS = *( SP / HTAB )` — RFC 9110 Section 5.6.3 - -A tab character between the colon and value is technically valid per these grammars. - -## Pass/Warn explanation - -- **Pass (400):** The server rejects the request. While HTAB is valid OWS per the grammar, rejecting unusual whitespace is a defensively strict approach. -- **Warn (2xx):** The server accepted the tab as valid OWS and processed the request correctly. This is RFC-compliant behavior, but flagged as a warning because tab-separated Content-Length values are unusual in practice and may indicate parser inconsistencies. - -## Why it matters - -While tabs are valid OWS, they are rarely used in practice. Some parsers may not handle tab characters correctly -- for example, treating the tab as part of the value rather than whitespace, resulting in a failed integer parse or a different numeric interpretation. This edge case tests parser robustness. - -## Deep Analysis - -### ABNF context - -The field-line grammar explicitly permits HTAB in OWS: - -``` -field-line = field-name ":" OWS field-value OWS -OWS = *( SP / HTAB ) -RWS = 1*( SP / HTAB ) -BWS = OWS -``` - -After the colon, OWS consumes the tab character (`0x09`). The remaining field-value is `5`, which matches `Content-Length = 1*DIGIT`. The request `Content-Length:\t5` is **grammatically valid** per the combined ABNF of RFC 9112 Section 5 and RFC 9110 Section 5.6.3. - -### RFC evidence - -> "field-line = field-name ':' OWS field-value OWS" -- RFC 9112 Section 5 - -> "OWS = *( SP / HTAB )" -- RFC 9110 Section 5.6.3 - -> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 - -> "No whitespace is allowed between the field name and colon." -- RFC 9112 Section 5 - -The last quote is relevant context: the RFC is strict about whitespace *before* the colon but explicitly permissive *after* it via OWS. The tab character is part of OWS and therefore valid in this position. - -### Chain of reasoning - -1. The server receives `Content-Length:\t5\r\n`. -2. It parses the field-name (`Content-Length`), verifies no whitespace before the colon (correct), then reads OWS after the colon. -3. The OWS production matches the HTAB character (`0x09`). -4. The remaining field-value is `5`, which matches `1*DIGIT`. -5. The Content-Length value is correctly parsed as 5. -6. The server should read 5 bytes of body (`hello`) and respond with 2xx. -7. A server that rejects this request is being stricter than the RFC requires -- defensively conservative but not RFC-mandated. A server that accepts it is fully compliant. -8. The concern is not RFC compliance but parser robustness: does the server correctly treat HTAB as whitespace when extracting the numeric value? - -### Security implications - -- **Parser divergence on whitespace handling**: Some parsers may only strip SP (`0x20`) before the value, not HTAB (`0x09`). Such a parser would attempt to parse `\t5` as the Content-Length value, either failing (rejecting the request) or producing an unexpected numeric result. -- **Content-Length misinterpretation**: If one parser treats `\t5` as an invalid integer (rejecting or defaulting to 0) while another correctly strips the tab and reads `5`, the two systems disagree on body length. This disagreement between a proxy and a backend is a request smuggling primitive. -- **Character encoding confusion**: In some environments, HTAB may be converted to spaces or multiple spaces during processing. If the tab is converted to, say, 8 spaces before numeric parsing, the value becomes ` 5`, which is still `5` after stripping -- but the intermediate representation may cause issues in parsers that do not expect leading whitespace in numeric fields. -- **Evasion of WAF rules**: Web Application Firewalls that inspect Content-Length values may not account for HTAB as a separator, potentially allowing an attacker to bypass Content-Length-based filtering or validation rules. - -## Sources - -- [RFC 9112 Section 5](https://www.rfc-editor.org/rfc/rfc9112#section-5) — field-line grammar with OWS -- [RFC 9110 Section 5.6.3](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.3) — OWS = *( SP / HTAB ) +--- +title: "CL Tab Before Value — Malformed Input" +description: "Content-Length:\\t5 — a Content-Length header where a horizontal tab character separates the colon from the value, instead of a space. Tested against RFC 9110 Section 5.5." +weight: 20 +--- + +| | | +|---|---| +| **Test ID** | `MAL-CL-TAB-BEFORE-VALUE` | +| **Category** | Malformed Input | +| **RFC** | [RFC 9110 Section 5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) | +| **Requirement** | valid per RFC | +| **Expected** | `400` preferred; `2xx` is a warning | + +## What it sends + +`Content-Length:\t5` — a Content-Length header where a horizontal tab character separates the colon from the value, instead of a space. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length:\t5\r\n +\r\n +hello +``` + +A horizontal tab character (`\t` / `0x09`) separates the colon from the value instead of a space. + + +## What the RFC says + +The field-line grammar explicitly includes optional whitespace between the colon and value: + +> `field-line = field-name ":" OWS field-value OWS` — RFC 9112 Section 5 + +And OWS permits both spaces and horizontal tabs: + +> `OWS = *( SP / HTAB )` — RFC 9110 Section 5.6.3 + +A tab character between the colon and value is technically valid per these grammars. + +## Pass/Warn explanation + +- **Pass (400):** The server rejects the request. While HTAB is valid OWS per the grammar, rejecting unusual whitespace is a defensively strict approach. +- **Warn (2xx):** The server accepted the tab as valid OWS and processed the request correctly. This is RFC-compliant behavior, but flagged as a warning because tab-separated Content-Length values are unusual in practice and may indicate parser inconsistencies. + +## Why it matters + +While tabs are valid OWS, they are rarely used in practice. Some parsers may not handle tab characters correctly -- for example, treating the tab as part of the value rather than whitespace, resulting in a failed integer parse or a different numeric interpretation. This edge case tests parser robustness. + +## Deep Analysis + +### ABNF context + +The field-line grammar explicitly permits HTAB in OWS: + +``` +field-line = field-name ":" OWS field-value OWS +OWS = *( SP / HTAB ) +RWS = 1*( SP / HTAB ) +BWS = OWS +``` + +After the colon, OWS consumes the tab character (`0x09`). The remaining field-value is `5`, which matches `Content-Length = 1*DIGIT`. The request `Content-Length:\t5` is **grammatically valid** per the combined ABNF of RFC 9112 Section 5 and RFC 9110 Section 5.6.3. + +### RFC evidence + +> "field-line = field-name ':' OWS field-value OWS" -- RFC 9112 Section 5 + +> "OWS = *( SP / HTAB )" -- RFC 9110 Section 5.6.3 + +> "Content-Length = 1*DIGIT" -- RFC 9110 Section 8.6 + +> "No whitespace is allowed between the field name and colon." -- RFC 9112 Section 5 + +The last quote is relevant context: the RFC is strict about whitespace *before* the colon but explicitly permissive *after* it via OWS. The tab character is part of OWS and therefore valid in this position. + +### Chain of reasoning + +1. The server receives `Content-Length:\t5\r\n`. +2. It parses the field-name (`Content-Length`), verifies no whitespace before the colon (correct), then reads OWS after the colon. +3. The OWS production matches the HTAB character (`0x09`). +4. The remaining field-value is `5`, which matches `1*DIGIT`. +5. The Content-Length value is correctly parsed as 5. +6. The server should read 5 bytes of body (`hello`) and respond with 2xx. +7. A server that rejects this request is being stricter than the RFC requires -- defensively conservative but not RFC-mandated. A server that accepts it is fully compliant. +8. The concern is not RFC compliance but parser robustness: does the server correctly treat HTAB as whitespace when extracting the numeric value? + +### Security implications + +- **Parser divergence on whitespace handling**: Some parsers may only strip SP (`0x20`) before the value, not HTAB (`0x09`). Such a parser would attempt to parse `\t5` as the Content-Length value, either failing (rejecting the request) or producing an unexpected numeric result. +- **Content-Length misinterpretation**: If one parser treats `\t5` as an invalid integer (rejecting or defaulting to 0) while another correctly strips the tab and reads `5`, the two systems disagree on body length. This disagreement between a proxy and a backend is a request smuggling primitive. +- **Character encoding confusion**: In some environments, HTAB may be converted to spaces or multiple spaces during processing. If the tab is converted to, say, 8 spaces before numeric parsing, the value becomes ` 5`, which is still `5` after stripping -- but the intermediate representation may cause issues in parsers that do not expect leading whitespace in numeric fields. +- **Evasion of WAF rules**: Web Application Firewalls that inspect Content-Length values may not account for HTAB as a separator, potentially allowing an attacker to bypass Content-Length-based filtering or validation rules. + +## Sources + +- [RFC 9112 Section 5](https://www.rfc-editor.org/rfc/rfc9112#section-5) — field-line grammar with OWS +- [RFC 9110 Section 5.6.3](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.3) — OWS = *( SP / HTAB ) diff --git a/docs/content/docs/malformed-input/control-chars-header.md b/docs/content/docs/malformed-input/control-chars-header.md index 8d53d23..fca49d7 100644 --- a/docs/content/docs/malformed-input/control-chars-header.md +++ b/docs/content/docs/malformed-input/control-chars-header.md @@ -1,99 +1,99 @@ ---- -title: "CONTROL-CHARS-HEADER" -description: "CONTROL-CHARS-HEADER test documentation" -weight: 8 ---- - -| | | -|---|---| -| **Test ID** | `MAL-CONTROL-CHARS-HEADER` | -| **Category** | Malformed Input | -| **RFC** | [RFC 9110 Section 5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) | -| **Expected** | `400` or close | - -## What it sends - -A request with control characters (`\x01`-`\x08`, `\x0E`-`\x1F`) in a header field value. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -X-Test: abc\x07\x08\x0Bdef\r\n -\r\n -``` - -The header value contains BEL (`\x07`), BS (`\x08`), and VT (`\x0B`) control characters. - - -## What the RFC says - -The field-value ABNF permits only visible ASCII, SP, HTAB, and obs-text: - -> `field-value = *field-content` -> `field-content = field-vchar [ 1*( SP / HTAB / field-vchar ) field-vchar ]` -> `field-vchar = VCHAR / obs-text` — RFC 9110 Section 5.5 - -`VCHAR` is `%x21-7E` (printable ASCII). Control characters like BEL (`0x07`), BS (`0x08`), and VT (`0x0B`) are in the `%x00-1F` range and fall outside `VCHAR`, `SP` (`0x20`), and `HTAB` (`0x09`). - -> "Field values containing other CTL characters are also invalid; however, recipients MAY retain such characters for the sake of robustness when they appear within a safe context (e.g., an application-specific quoted string that will not be processed by any downstream HTTP parser)." — RFC 9110 Section 5.5 - -While the RFC allows recipients some leniency with non-NUL/CR/LF control characters, they are still grammatically invalid. - -## Deep Analysis - -### ABNF violation - -The field-value grammar strictly defines which characters are permitted: - -``` -field-value = *field-content -field-content = field-vchar - [ 1*( SP / HTAB / field-vchar ) field-vchar ] -field-vchar = VCHAR / obs-text -VCHAR = %x21-7E -obs-text = %x80-FF -SP = %x20 -HTAB = %x09 -``` - -The allowed byte ranges in a field-value are: -- `HTAB` = `0x09` -- `SP` = `0x20` -- `VCHAR` = `0x21-7E` (printable ASCII) -- `obs-text` = `0x80-FF` (high bytes, for legacy compatibility) - -Control characters BEL (`0x07`), BS (`0x08`), and VT (`0x0B`) fall in the range `0x00-0x08` and `0x0A-0x1F` (excluding `HTAB` at `0x09`). They are not `VCHAR`, not `SP`, not `HTAB`, and not `obs-text`. They do not match any production in the `field-value` grammar. - -### RFC evidence - -> "field-value = *field-content" -- RFC 9110 Section 5.5 - -> "field-vchar = VCHAR / obs-text" -- RFC 9110 Section 5.5 - -> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters; a recipient of field content containing those characters is typically unable to handle them properly and MUST either reject the message or replace each of those characters with SP before further processing or forwarding of that message." -- RFC 9110 Section 5.5 - -> "Field values containing other CTL characters are also invalid; however, recipients MAY retain such characters for the sake of robustness when they appear within a safe context (e.g., an application-specific quoted string that will not be processed by any downstream HTTP parser)." -- RFC 9110 Section 5.5 - -> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 - -The RFC makes a distinction: CR/LF/NUL are "invalid and dangerous" with a MUST reject-or-replace requirement, while other CTL characters are "also invalid" but with a MAY-retain exception for safe contexts. For a header like `X-Test` that has no application-specific quoted-string semantics, there is no "safe context" -- rejection is the appropriate response. - -### Chain of reasoning - -1. The server receives `X-Test: abc\x07\x08\x0Bdef\r\n`. -2. It parses the field-value: `abc` matches `field-vchar` characters (all `VCHAR`). -3. It then encounters `\x07` (BEL). This byte is `0x07`, which is not `VCHAR` (`0x21-7E`), not `SP` (`0x20`), not `HTAB` (`0x09`), and not `obs-text` (`0x80-FF`). -4. The `field-content` production cannot match `\x07`. The grammar match fails. -5. The RFC says these CTL characters are "also invalid." While the MAY-retain clause allows leniency in safe contexts, a generic header name like `X-Test` does not provide such a context. -6. The server SHOULD reject with 400 per RFC 9112 Section 2.2's general grammar-mismatch guidance. - -### Security implications - -- **Header injection**: Control characters can be used to manipulate how different parsers interpret header boundaries. For example, some parsers may treat VT (`0x0B`) as a line separator, effectively injecting a new header line within what appears to be a single header value. -- **Log poisoning**: BEL (`0x07`) causes terminal bells, BS (`0x08`) causes backspace overwriting in terminal displays. An attacker embedding these in headers can manipulate server log output, hiding malicious requests or injecting misleading log entries. -- **WAF bypass**: Web Application Firewalls that scan header values for malicious patterns may not account for interspersed control characters. An attacker could use `\x07` or `\x08` to break up signature patterns (e.g., ``) and evade detection. -- **Downstream parser confusion**: If a proxy retains the control characters (per the MAY-retain clause) and forwards them to a backend that interprets them differently, the two systems may disagree on header structure -- a potential header-smuggling vector. - -## Sources - -- [RFC 9110 Section 5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) — field values and control characters +--- +title: "Control Chars Header — Malformed Input" +description: "A request with control characters (\\x01-\\x08, \\x0E-\\x1F) in a header field value. Tested against RFC 9110 Section 5.5." +weight: 8 +--- + +| | | +|---|---| +| **Test ID** | `MAL-CONTROL-CHARS-HEADER` | +| **Category** | Malformed Input | +| **RFC** | [RFC 9110 Section 5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) | +| **Expected** | `400` or close | + +## What it sends + +A request with control characters (`\x01`-`\x08`, `\x0E`-`\x1F`) in a header field value. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +X-Test: abc\x07\x08\x0Bdef\r\n +\r\n +``` + +The header value contains BEL (`\x07`), BS (`\x08`), and VT (`\x0B`) control characters. + + +## What the RFC says + +The field-value ABNF permits only visible ASCII, SP, HTAB, and obs-text: + +> `field-value = *field-content` +> `field-content = field-vchar [ 1*( SP / HTAB / field-vchar ) field-vchar ]` +> `field-vchar = VCHAR / obs-text` — RFC 9110 Section 5.5 + +`VCHAR` is `%x21-7E` (printable ASCII). Control characters like BEL (`0x07`), BS (`0x08`), and VT (`0x0B`) are in the `%x00-1F` range and fall outside `VCHAR`, `SP` (`0x20`), and `HTAB` (`0x09`). + +> "Field values containing other CTL characters are also invalid; however, recipients MAY retain such characters for the sake of robustness when they appear within a safe context (e.g., an application-specific quoted string that will not be processed by any downstream HTTP parser)." — RFC 9110 Section 5.5 + +While the RFC allows recipients some leniency with non-NUL/CR/LF control characters, they are still grammatically invalid. + +## Deep Analysis + +### ABNF violation + +The field-value grammar strictly defines which characters are permitted: + +``` +field-value = *field-content +field-content = field-vchar + [ 1*( SP / HTAB / field-vchar ) field-vchar ] +field-vchar = VCHAR / obs-text +VCHAR = %x21-7E +obs-text = %x80-FF +SP = %x20 +HTAB = %x09 +``` + +The allowed byte ranges in a field-value are: +- `HTAB` = `0x09` +- `SP` = `0x20` +- `VCHAR` = `0x21-7E` (printable ASCII) +- `obs-text` = `0x80-FF` (high bytes, for legacy compatibility) + +Control characters BEL (`0x07`), BS (`0x08`), and VT (`0x0B`) fall in the range `0x00-0x08` and `0x0A-0x1F` (excluding `HTAB` at `0x09`). They are not `VCHAR`, not `SP`, not `HTAB`, and not `obs-text`. They do not match any production in the `field-value` grammar. + +### RFC evidence + +> "field-value = *field-content" -- RFC 9110 Section 5.5 + +> "field-vchar = VCHAR / obs-text" -- RFC 9110 Section 5.5 + +> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters; a recipient of field content containing those characters is typically unable to handle them properly and MUST either reject the message or replace each of those characters with SP before further processing or forwarding of that message." -- RFC 9110 Section 5.5 + +> "Field values containing other CTL characters are also invalid; however, recipients MAY retain such characters for the sake of robustness when they appear within a safe context (e.g., an application-specific quoted string that will not be processed by any downstream HTTP parser)." -- RFC 9110 Section 5.5 + +> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 + +The RFC makes a distinction: CR/LF/NUL are "invalid and dangerous" with a MUST reject-or-replace requirement, while other CTL characters are "also invalid" but with a MAY-retain exception for safe contexts. For a header like `X-Test` that has no application-specific quoted-string semantics, there is no "safe context" -- rejection is the appropriate response. + +### Chain of reasoning + +1. The server receives `X-Test: abc\x07\x08\x0Bdef\r\n`. +2. It parses the field-value: `abc` matches `field-vchar` characters (all `VCHAR`). +3. It then encounters `\x07` (BEL). This byte is `0x07`, which is not `VCHAR` (`0x21-7E`), not `SP` (`0x20`), not `HTAB` (`0x09`), and not `obs-text` (`0x80-FF`). +4. The `field-content` production cannot match `\x07`. The grammar match fails. +5. The RFC says these CTL characters are "also invalid." While the MAY-retain clause allows leniency in safe contexts, a generic header name like `X-Test` does not provide such a context. +6. The server SHOULD reject with 400 per RFC 9112 Section 2.2's general grammar-mismatch guidance. + +### Security implications + +- **Header injection**: Control characters can be used to manipulate how different parsers interpret header boundaries. For example, some parsers may treat VT (`0x0B`) as a line separator, effectively injecting a new header line within what appears to be a single header value. +- **Log poisoning**: BEL (`0x07`) causes terminal bells, BS (`0x08`) causes backspace overwriting in terminal displays. An attacker embedding these in headers can manipulate server log output, hiding malicious requests or injecting misleading log entries. +- **WAF bypass**: Web Application Firewalls that scan header values for malicious patterns may not account for interspersed control characters. An attacker could use `\x07` or `\x08` to break up signature patterns (e.g., ``) and evade detection. +- **Downstream parser confusion**: If a proxy retains the control characters (per the MAY-retain clause) and forwards them to a backend that interprets them differently, the two systems may disagree on header structure -- a potential header-smuggling vector. + +## Sources + +- [RFC 9110 Section 5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) — field values and control characters diff --git a/docs/content/docs/malformed-input/empty-request.md b/docs/content/docs/malformed-input/empty-request.md index 2bf21ac..00a67ae 100644 --- a/docs/content/docs/malformed-input/empty-request.md +++ b/docs/content/docs/malformed-input/empty-request.md @@ -1,85 +1,85 @@ ---- -title: "EMPTY-REQUEST" -description: "EMPTY-REQUEST test documentation" -weight: 13 ---- - -| | | -|---|---| -| **Test ID** | `MAL-EMPTY-REQUEST` | -| **Category** | Malformed Input | -| **Expected** | `400`, close, or timeout | - -## What it sends - -Zero bytes -- the TCP connection is established and then closed without sending any data. - -``` -(zero bytes — TCP connection opened, no data sent) -``` - - -## What the RFC says - -> "In the interest of robustness, a server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." — RFC 9112 Section 2.2 - -Zero bytes means no request-line was received at all. The server has no data to parse against the HTTP-message grammar. - -> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar...the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 - -## Why timeout is acceptable - -The server has no indication that a request was even attempted. With zero bytes received, the server cannot distinguish between a slow client and a connection that will never send data. - -## Deep Analysis - -### ABNF violation - -An HTTP/1.1 message requires a complete structure: - -``` -HTTP-message = start-line CRLF - *( field-line CRLF ) - CRLF - [ message-body ] - -start-line = request-line / status-line -request-line = method SP request-target SP HTTP-version -method = token -token = 1*tchar -``` - -Zero bytes means no `start-line` was received. The `method` production requires `1*tchar` -- at least one character. With no characters at all, the `HTTP-message` grammar cannot match even its first production. The message is not merely malformed; it is entirely absent. - -### RFC evidence - -> "In the interest of robustness, a server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." -- RFC 9112 Section 2.2 - -> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 - -> "An HTTP/1.1 user agent MUST NOT preface or follow a request with an extra CRLF." -- RFC 9112 Section 2.2 - -The robustness exception allows the server to ignore leading CRLFs -- but zero bytes is not a CRLF. There is no data to ignore. The server is waiting for a `request-line` that never arrives. - -### Chain of reasoning - -1. The TCP three-way handshake completes successfully. The connection is established. -2. The server enters its read loop, expecting the first byte of a `request-line`. -3. Zero bytes arrive. The client closes its end of the connection (or the test tool disconnects). -4. The server detects the connection closure (EOF / FIN). -5. At this point, the server has received no data at all. The `HTTP-message` grammar cannot match zero bytes. -6. Three legitimate server responses exist: - - **400 (Bad Request)**: The server recognizes that a connection was opened but no valid request was sent. It replies with 400 before closing. - - **Connection close**: The server silently closes its side. No bytes were exchanged, so no response is necessary. - - **Timeout**: The server never detects EOF (e.g., the client holds the connection open without sending). The server eventually times out waiting for the request-line. -7. All three outcomes are acceptable because the RFC's SHOULD-400 guidance applies to "a sequence of octets that does not match the HTTP-message grammar." Zero octets is arguably not even "a sequence" -- the server was never given anything to parse. - -### Security implications - -- **Slowloris / connection exhaustion**: An attacker opens thousands of TCP connections and sends zero bytes on each. If the server allocates per-connection resources (memory, file descriptors, thread pool slots) and waits indefinitely for data, the server's connection capacity is exhausted, denying service to legitimate clients. -- **Port scanning and fingerprinting**: Opening a connection and sending nothing, then observing whether the server responds with 400, closes immediately, or times out after a specific duration, reveals information about the server implementation and its timeout configuration. -- **Resource leak detection**: Servers that do not properly clean up connections with zero data may leak file descriptors or memory over time, eventually crashing under sustained connection-open attacks. - -## Sources - -- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — message parsing robustness +--- +title: "Empty Request — Malformed Input" +description: "Zero bytes -- the TCP connection is established and then closed without sending any data. Part of Http11Probe's malformed input robustness suite." +weight: 13 +--- + +| | | +|---|---| +| **Test ID** | `MAL-EMPTY-REQUEST` | +| **Category** | Malformed Input | +| **Expected** | `400`, close, or timeout | + +## What it sends + +Zero bytes -- the TCP connection is established and then closed without sending any data. + +``` +(zero bytes — TCP connection opened, no data sent) +``` + + +## What the RFC says + +> "In the interest of robustness, a server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." — RFC 9112 Section 2.2 + +Zero bytes means no request-line was received at all. The server has no data to parse against the HTTP-message grammar. + +> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar...the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 + +## Why timeout is acceptable + +The server has no indication that a request was even attempted. With zero bytes received, the server cannot distinguish between a slow client and a connection that will never send data. + +## Deep Analysis + +### ABNF violation + +An HTTP/1.1 message requires a complete structure: + +``` +HTTP-message = start-line CRLF + *( field-line CRLF ) + CRLF + [ message-body ] + +start-line = request-line / status-line +request-line = method SP request-target SP HTTP-version +method = token +token = 1*tchar +``` + +Zero bytes means no `start-line` was received. The `method` production requires `1*tchar` -- at least one character. With no characters at all, the `HTTP-message` grammar cannot match even its first production. The message is not merely malformed; it is entirely absent. + +### RFC evidence + +> "In the interest of robustness, a server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." -- RFC 9112 Section 2.2 + +> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 + +> "An HTTP/1.1 user agent MUST NOT preface or follow a request with an extra CRLF." -- RFC 9112 Section 2.2 + +The robustness exception allows the server to ignore leading CRLFs -- but zero bytes is not a CRLF. There is no data to ignore. The server is waiting for a `request-line` that never arrives. + +### Chain of reasoning + +1. The TCP three-way handshake completes successfully. The connection is established. +2. The server enters its read loop, expecting the first byte of a `request-line`. +3. Zero bytes arrive. The client closes its end of the connection (or the test tool disconnects). +4. The server detects the connection closure (EOF / FIN). +5. At this point, the server has received no data at all. The `HTTP-message` grammar cannot match zero bytes. +6. Three legitimate server responses exist: + - **400 (Bad Request)**: The server recognizes that a connection was opened but no valid request was sent. It replies with 400 before closing. + - **Connection close**: The server silently closes its side. No bytes were exchanged, so no response is necessary. + - **Timeout**: The server never detects EOF (e.g., the client holds the connection open without sending). The server eventually times out waiting for the request-line. +7. All three outcomes are acceptable because the RFC's SHOULD-400 guidance applies to "a sequence of octets that does not match the HTTP-message grammar." Zero octets is arguably not even "a sequence" -- the server was never given anything to parse. + +### Security implications + +- **Slowloris / connection exhaustion**: An attacker opens thousands of TCP connections and sends zero bytes on each. If the server allocates per-connection resources (memory, file descriptors, thread pool slots) and waits indefinitely for data, the server's connection capacity is exhausted, denying service to legitimate clients. +- **Port scanning and fingerprinting**: Opening a connection and sending nothing, then observing whether the server responds with 400, closes immediately, or times out after a specific duration, reveals information about the server implementation and its timeout configuration. +- **Resource leak detection**: Servers that do not properly clean up connections with zero data may leak file descriptors or memory over time, eventually crashing under sustained connection-open attacks. + +## Sources + +- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — message parsing robustness diff --git a/docs/content/docs/malformed-input/h2-preface.md b/docs/content/docs/malformed-input/h2-preface.md index ec2e378..83cd835 100644 --- a/docs/content/docs/malformed-input/h2-preface.md +++ b/docs/content/docs/malformed-input/h2-preface.md @@ -1,104 +1,104 @@ ---- -title: "H2-PREFACE" -description: "H2-PREFACE test documentation" -weight: 17 ---- - -| | | -|---|---| -| **Test ID** | `MAL-H2-PREFACE` | -| **Category** | Malformed Input | -| **Expected** | `400`/`505`, close, or timeout | - -## What it sends - -The HTTP/2 connection preface (`PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n`) to an HTTP/1.1 server. - -```http -PRI * HTTP/2.0\r\n -\r\n -SM\r\n -\r\n -``` - -The HTTP/2 connection preface, sent to an HTTP/1.1 server. - - -## What the RFC says - -The HTTP/2 connection preface starts with the string `PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n`: - -> "The client connection preface is selected so that a large proportion of HTTP/1.1 or HTTP/1.0 servers and intermediaries do not attempt to process further frames." — RFC 9113 Section 3.4 - -When parsed as HTTP/1.1, `PRI` is an unknown method and `HTTP/2.0` is an unsupported version. The request-line grammar is: - -> `request-line = method SP request-target SP HTTP-version` — RFC 9112 Section 3 - -> `HTTP-version = HTTP-name "/" DIGIT "." DIGIT` — RFC 9112 Section 2.3 - -`HTTP/2.0` is a valid version string syntactically, but HTTP/2.0 is not HTTP/1.1, so 505 (HTTP Version Not Supported) is appropriate. Alternatively: - -> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar...the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 - -## Why it matters - -An HTTP/1.1-only server receiving the H2 preface should recognize it is not a valid HTTP/1.1 request. Parsing it as HTTP/1.1 could lead to unexpected behavior. The server should reject with 400 or 505, close the connection, or timeout. - -## Deep Analysis - -### ABNF context - -When an HTTP/1.1 server parses the H2 preface as an HTTP/1.1 request, it sees: - -``` -request-line = method SP request-target SP HTTP-version -method = token -token = 1*tchar -HTTP-version = HTTP-name "/" DIGIT "." DIGIT -HTTP-name = %s"HTTP" -``` - -Parsing `PRI * HTTP/2.0\r\n`: -- `method` = `PRI` -- a valid token (all characters are `tchar`), but not a recognized HTTP method. -- `request-target` = `*` -- valid `asterisk-form`, typically only used with OPTIONS. -- `HTTP-version` = `HTTP/2.0` -- syntactically valid (`HTTP-name "/" DIGIT "." DIGIT`), but the major version `2` indicates HTTP/2, not HTTP/1.1. - -After the blank line (`\r\n`), the server encounters `SM\r\n\r\n` -- which looks like a truncated or malformed follow-up that does not match any HTTP/1.1 grammar production. - -### RFC evidence - -> "The client connection preface is selected so that a large proportion of HTTP/1.1 or HTTP/1.0 servers and intermediaries do not attempt to process further frames." -- RFC 9113 Section 3.4 - -> "This sequence MUST be followed by a SETTINGS frame (Section 6.5), which MAY be empty." -- RFC 9113 Section 3.4 - -> "Clients and servers MUST treat an invalid connection preface as a connection error of type PROTOCOL_ERROR." -- RFC 9113 Section 3.4 - -> "A server that receives a method longer than any that it implements SHOULD respond with a 501 (Not Implemented) status code." -- RFC 9112 Section 3 - -> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 - -The H2 preface was deliberately designed to cause HTTP/1.1 servers to reject it. The `PRI` method is not a standard HTTP method, `*` as a request-target is only valid with OPTIONS, and `HTTP/2.0` signals an unsupported major version. - -### Chain of reasoning - -1. The server receives `PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n` on an HTTP/1.1 connection. -2. It parses the request-line: `PRI` (method), `*` (request-target), `HTTP/2.0` (version). -3. Multiple rejection paths exist: - - **505 (HTTP Version Not Supported)**: The major version `2` indicates HTTP/2. An HTTP/1.1-only server does not support this version and may respond with 505. - - **501 (Not Implemented)**: `PRI` is not a recognized HTTP method. Per RFC 9112 Section 3, the server SHOULD respond with 501. - - **400 (Bad Request)**: After the empty line, `SM\r\n\r\n` does not constitute a valid HTTP/1.1 request or response. The overall byte sequence does not match the `HTTP-message` grammar when considered holistically. - - **Connection close or timeout**: The server may simply close the connection or wait for data it can parse. -4. The H2 preface was intentionally designed to trigger these rejection paths in HTTP/1.1 servers, ensuring that an HTTP/2 client connecting to an HTTP/1.1-only server fails cleanly rather than producing protocol confusion. - -### Security implications - -- **Protocol confusion**: If an HTTP/1.1 server does not reject the H2 preface and instead attempts to process `SM` as a second request or body data, it may enter an undefined state. The binary HTTP/2 frames that follow the preface would be misinterpreted as HTTP/1.1 data, potentially causing crashes or exploitable behavior. -- **Downgrade detection**: An attacker probing whether a server supports HTTP/2 can send the H2 preface. A server that responds with 400/505 confirms it is HTTP/1.1-only; a server that upgrades to HTTP/2 confirms dual-protocol support. This fingerprinting aids in targeted attacks. -- **Proxy confusion**: If a front-end proxy speaks HTTP/2 and a backend only speaks HTTP/1.1, sending the H2 preface directly to the backend (bypassing the proxy's protocol translation) could trigger unexpected behavior. The backend must cleanly reject it. -- **Frame injection**: If the server somehow processes past the preface, the binary HTTP/2 SETTINGS frame that follows could be misinterpreted as HTTP/1.1 content, injecting controlled bytes into the server's parsing state. - -## Sources - -- [RFC 9113 Section 3.4](https://www.rfc-editor.org/rfc/rfc9113#section-3.4) — HTTP/2 connection preface -- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — rejection of invalid messages -- [RFC 9112 Section 3](https://www.rfc-editor.org/rfc/rfc9112#section-3) — request-line grammar +--- +title: "HTTP/2 Preface — Malformed Input" +description: "The HTTP/2 connection preface (PRI * HTTP/2.0\\r\\n\\r\\nSM\\r\\n\\r\\n) to an HTTP/1.1 server. Part of Http11Probe's malformed input robustness suite." +weight: 17 +--- + +| | | +|---|---| +| **Test ID** | `MAL-H2-PREFACE` | +| **Category** | Malformed Input | +| **Expected** | `400`/`505`, close, or timeout | + +## What it sends + +The HTTP/2 connection preface (`PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n`) to an HTTP/1.1 server. + +```http +PRI * HTTP/2.0\r\n +\r\n +SM\r\n +\r\n +``` + +The HTTP/2 connection preface, sent to an HTTP/1.1 server. + + +## What the RFC says + +The HTTP/2 connection preface starts with the string `PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n`: + +> "The client connection preface is selected so that a large proportion of HTTP/1.1 or HTTP/1.0 servers and intermediaries do not attempt to process further frames." — RFC 9113 Section 3.4 + +When parsed as HTTP/1.1, `PRI` is an unknown method and `HTTP/2.0` is an unsupported version. The request-line grammar is: + +> `request-line = method SP request-target SP HTTP-version` — RFC 9112 Section 3 + +> `HTTP-version = HTTP-name "/" DIGIT "." DIGIT` — RFC 9112 Section 2.3 + +`HTTP/2.0` is a valid version string syntactically, but HTTP/2.0 is not HTTP/1.1, so 505 (HTTP Version Not Supported) is appropriate. Alternatively: + +> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar...the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 + +## Why it matters + +An HTTP/1.1-only server receiving the H2 preface should recognize it is not a valid HTTP/1.1 request. Parsing it as HTTP/1.1 could lead to unexpected behavior. The server should reject with 400 or 505, close the connection, or timeout. + +## Deep Analysis + +### ABNF context + +When an HTTP/1.1 server parses the H2 preface as an HTTP/1.1 request, it sees: + +``` +request-line = method SP request-target SP HTTP-version +method = token +token = 1*tchar +HTTP-version = HTTP-name "/" DIGIT "." DIGIT +HTTP-name = %s"HTTP" +``` + +Parsing `PRI * HTTP/2.0\r\n`: +- `method` = `PRI` -- a valid token (all characters are `tchar`), but not a recognized HTTP method. +- `request-target` = `*` -- valid `asterisk-form`, typically only used with OPTIONS. +- `HTTP-version` = `HTTP/2.0` -- syntactically valid (`HTTP-name "/" DIGIT "." DIGIT`), but the major version `2` indicates HTTP/2, not HTTP/1.1. + +After the blank line (`\r\n`), the server encounters `SM\r\n\r\n` -- which looks like a truncated or malformed follow-up that does not match any HTTP/1.1 grammar production. + +### RFC evidence + +> "The client connection preface is selected so that a large proportion of HTTP/1.1 or HTTP/1.0 servers and intermediaries do not attempt to process further frames." -- RFC 9113 Section 3.4 + +> "This sequence MUST be followed by a SETTINGS frame (Section 6.5), which MAY be empty." -- RFC 9113 Section 3.4 + +> "Clients and servers MUST treat an invalid connection preface as a connection error of type PROTOCOL_ERROR." -- RFC 9113 Section 3.4 + +> "A server that receives a method longer than any that it implements SHOULD respond with a 501 (Not Implemented) status code." -- RFC 9112 Section 3 + +> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 + +The H2 preface was deliberately designed to cause HTTP/1.1 servers to reject it. The `PRI` method is not a standard HTTP method, `*` as a request-target is only valid with OPTIONS, and `HTTP/2.0` signals an unsupported major version. + +### Chain of reasoning + +1. The server receives `PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n` on an HTTP/1.1 connection. +2. It parses the request-line: `PRI` (method), `*` (request-target), `HTTP/2.0` (version). +3. Multiple rejection paths exist: + - **505 (HTTP Version Not Supported)**: The major version `2` indicates HTTP/2. An HTTP/1.1-only server does not support this version and may respond with 505. + - **501 (Not Implemented)**: `PRI` is not a recognized HTTP method. Per RFC 9112 Section 3, the server SHOULD respond with 501. + - **400 (Bad Request)**: After the empty line, `SM\r\n\r\n` does not constitute a valid HTTP/1.1 request or response. The overall byte sequence does not match the `HTTP-message` grammar when considered holistically. + - **Connection close or timeout**: The server may simply close the connection or wait for data it can parse. +4. The H2 preface was intentionally designed to trigger these rejection paths in HTTP/1.1 servers, ensuring that an HTTP/2 client connecting to an HTTP/1.1-only server fails cleanly rather than producing protocol confusion. + +### Security implications + +- **Protocol confusion**: If an HTTP/1.1 server does not reject the H2 preface and instead attempts to process `SM` as a second request or body data, it may enter an undefined state. The binary HTTP/2 frames that follow the preface would be misinterpreted as HTTP/1.1 data, potentially causing crashes or exploitable behavior. +- **Downgrade detection**: An attacker probing whether a server supports HTTP/2 can send the H2 preface. A server that responds with 400/505 confirms it is HTTP/1.1-only; a server that upgrades to HTTP/2 confirms dual-protocol support. This fingerprinting aids in targeted attacks. +- **Proxy confusion**: If a front-end proxy speaks HTTP/2 and a backend only speaks HTTP/1.1, sending the H2 preface directly to the backend (bypassing the proxy's protocol translation) could trigger unexpected behavior. The backend must cleanly reject it. +- **Frame injection**: If the server somehow processes past the preface, the binary HTTP/2 SETTINGS frame that follows could be misinterpreted as HTTP/1.1 content, injecting controlled bytes into the server's parsing state. + +## Sources + +- [RFC 9113 Section 3.4](https://www.rfc-editor.org/rfc/rfc9113#section-3.4) — HTTP/2 connection preface +- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — rejection of invalid messages +- [RFC 9112 Section 3](https://www.rfc-editor.org/rfc/rfc9112#section-3) — request-line grammar diff --git a/docs/content/docs/malformed-input/incomplete-request.md b/docs/content/docs/malformed-input/incomplete-request.md index b6af32a..78fbce4 100644 --- a/docs/content/docs/malformed-input/incomplete-request.md +++ b/docs/content/docs/malformed-input/incomplete-request.md @@ -1,95 +1,95 @@ ---- -title: "INCOMPLETE-REQUEST" -description: "INCOMPLETE-REQUEST test documentation" -weight: 12 ---- - -| | | -|---|---| -| **Test ID** | `MAL-INCOMPLETE-REQUEST` | -| **Category** | Malformed Input | -| **Expected** | `400`, close, or timeout | - -## What it sends - -A partial HTTP request -- the request-line and some headers, but the connection is closed before the final CRLF. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -X-Test: value -``` - -The request ends abruptly after the `X-Test` header value — no `\r\n` line terminator and no blank line to signal end of headers. - - -## What the RFC says - -An HTTP/1.1 message requires a complete header section terminated by an empty line (CRLF CRLF). The field section grammar from RFC 9112 is: - -> `HTTP-message = start-line CRLF *( field-line CRLF ) CRLF [ message-body ]` — RFC 9112 Section 2.1 - -Without the final blank line, the message is incomplete and does not match this grammar. - -> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar...the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 - -## Why timeout is acceptable - -The server may be waiting for the rest of the headers. It has received a valid prefix but not a complete request. The connection was closed by the client before the message was finished, so the server may respond with 400, close, or timeout. - -## Deep Analysis - -### ABNF violation - -The HTTP/1.1 message grammar requires a complete structure with explicit delimiters: - -``` -HTTP-message = start-line CRLF - *( field-line CRLF ) - CRLF - [ message-body ] - -field-line = field-name ":" OWS field-value OWS -``` - -The test sends: -- `GET / HTTP/1.1\r\n` -- valid `start-line CRLF` -- `Host: localhost:8080\r\n` -- valid `field-line CRLF` -- `X-Test: value` -- **no trailing CRLF**, and **no empty-line delimiter** - -The grammar requires each `field-line` to be followed by `CRLF`, and the header section must end with a bare `CRLF` (the empty line). Without the terminating `\r\n` on the last header line, the `field-line CRLF` production does not match. Without the subsequent empty `CRLF`, the message structure is incomplete. - -### RFC evidence - -> "HTTP-message = start-line CRLF *( field-line CRLF ) CRLF [ message-body ]" -- RFC 9112 Section 2.1 - -> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 - -> "The normal procedure for parsing an HTTP message is to read the start-line into a structure, read each header field line into a hash table by field name until the empty line, and then use the parsed data to determine if a message body is expected." -- RFC 9112 Section 2.2 - -The third quote describes the expected parsing flow: the server reads header lines "until the empty line." If the connection closes before the empty line arrives, the parsing procedure cannot complete -- the message is incomplete. - -### Chain of reasoning - -1. The server receives a valid request-line (`GET / HTTP/1.1\r\n`) and begins reading headers. -2. It reads `Host: localhost:8080\r\n` -- a complete field-line. -3. It begins reading the next line: `X-Test: value`. -4. The server expects a `\r\n` to terminate this field-line, followed by either another field-line or the empty-line delimiter. -5. Instead, the connection closes (EOF). The server has a partial field-line with no terminator. -6. The `HTTP-message` grammar is not satisfied: the `CRLF` after the last `field-line` is missing, and the mandatory empty `CRLF` delimiter is missing. -7. The server may: - - **Return 400**: It has enough data to recognize a request was started but not completed. Per RFC 9112 Section 2.2, this is the SHOULD response. - - **Close silently**: The request was never complete, so no response is strictly required. - - **Timeout**: If the connection was not closed by the client but merely stalled, the server waits for more data until its timeout expires. - -### Security implications - -- **Slowloris attack**: Sending partial requests and never completing them is the core technique of the Slowloris denial-of-service attack. The server keeps the connection open, waiting for the rest of the headers, consuming a connection slot. Thousands of such partial connections exhaust the server's connection pool. -- **Resource leak**: Each incomplete request consumes memory (for the partially parsed headers) and a file descriptor (for the socket). Without proper timeouts and cleanup, these resources are never freed. -- **Timeout calibration probing**: An attacker can measure how long the server waits before timing out an incomplete request. This reveals the server's timeout configuration, which informs the attacker how to tune a Slowloris attack for maximum effectiveness. -- **Parser state confusion**: Some servers may attempt to process a partial request upon connection close, using whatever headers were received. If the incomplete header `X-Test: value` is used without its terminator, the parser may include trailing garbage or buffer contents in the header value. - -## Sources - -- [RFC 9112 Section 2.1](https://www.rfc-editor.org/rfc/rfc9112#section-2.1) — HTTP message grammar -- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — message parsing robustness +--- +title: "Incomplete Request — Malformed Input" +description: "A partial HTTP request -- the request-line and some headers, but the connection is closed before the final CRLF. Part of Http11Probe's malformed input robustness suite." +weight: 12 +--- + +| | | +|---|---| +| **Test ID** | `MAL-INCOMPLETE-REQUEST` | +| **Category** | Malformed Input | +| **Expected** | `400`, close, or timeout | + +## What it sends + +A partial HTTP request -- the request-line and some headers, but the connection is closed before the final CRLF. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +X-Test: value +``` + +The request ends abruptly after the `X-Test` header value — no `\r\n` line terminator and no blank line to signal end of headers. + + +## What the RFC says + +An HTTP/1.1 message requires a complete header section terminated by an empty line (CRLF CRLF). The field section grammar from RFC 9112 is: + +> `HTTP-message = start-line CRLF *( field-line CRLF ) CRLF [ message-body ]` — RFC 9112 Section 2.1 + +Without the final blank line, the message is incomplete and does not match this grammar. + +> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar...the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 + +## Why timeout is acceptable + +The server may be waiting for the rest of the headers. It has received a valid prefix but not a complete request. The connection was closed by the client before the message was finished, so the server may respond with 400, close, or timeout. + +## Deep Analysis + +### ABNF violation + +The HTTP/1.1 message grammar requires a complete structure with explicit delimiters: + +``` +HTTP-message = start-line CRLF + *( field-line CRLF ) + CRLF + [ message-body ] + +field-line = field-name ":" OWS field-value OWS +``` + +The test sends: +- `GET / HTTP/1.1\r\n` -- valid `start-line CRLF` +- `Host: localhost:8080\r\n` -- valid `field-line CRLF` +- `X-Test: value` -- **no trailing CRLF**, and **no empty-line delimiter** + +The grammar requires each `field-line` to be followed by `CRLF`, and the header section must end with a bare `CRLF` (the empty line). Without the terminating `\r\n` on the last header line, the `field-line CRLF` production does not match. Without the subsequent empty `CRLF`, the message structure is incomplete. + +### RFC evidence + +> "HTTP-message = start-line CRLF *( field-line CRLF ) CRLF [ message-body ]" -- RFC 9112 Section 2.1 + +> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 + +> "The normal procedure for parsing an HTTP message is to read the start-line into a structure, read each header field line into a hash table by field name until the empty line, and then use the parsed data to determine if a message body is expected." -- RFC 9112 Section 2.2 + +The third quote describes the expected parsing flow: the server reads header lines "until the empty line." If the connection closes before the empty line arrives, the parsing procedure cannot complete -- the message is incomplete. + +### Chain of reasoning + +1. The server receives a valid request-line (`GET / HTTP/1.1\r\n`) and begins reading headers. +2. It reads `Host: localhost:8080\r\n` -- a complete field-line. +3. It begins reading the next line: `X-Test: value`. +4. The server expects a `\r\n` to terminate this field-line, followed by either another field-line or the empty-line delimiter. +5. Instead, the connection closes (EOF). The server has a partial field-line with no terminator. +6. The `HTTP-message` grammar is not satisfied: the `CRLF` after the last `field-line` is missing, and the mandatory empty `CRLF` delimiter is missing. +7. The server may: + - **Return 400**: It has enough data to recognize a request was started but not completed. Per RFC 9112 Section 2.2, this is the SHOULD response. + - **Close silently**: The request was never complete, so no response is strictly required. + - **Timeout**: If the connection was not closed by the client but merely stalled, the server waits for more data until its timeout expires. + +### Security implications + +- **Slowloris attack**: Sending partial requests and never completing them is the core technique of the Slowloris denial-of-service attack. The server keeps the connection open, waiting for the rest of the headers, consuming a connection slot. Thousands of such partial connections exhaust the server's connection pool. +- **Resource leak**: Each incomplete request consumes memory (for the partially parsed headers) and a file descriptor (for the socket). Without proper timeouts and cleanup, these resources are never freed. +- **Timeout calibration probing**: An attacker can measure how long the server waits before timing out an incomplete request. This reveals the server's timeout configuration, which informs the attacker how to tune a Slowloris attack for maximum effectiveness. +- **Parser state confusion**: Some servers may attempt to process a partial request upon connection close, using whatever headers were received. If the incomplete header `X-Test: value` is used without its terminator, the parser may include trailing garbage or buffer contents in the header value. + +## Sources + +- [RFC 9112 Section 2.1](https://www.rfc-editor.org/rfc/rfc9112#section-2.1) — HTTP message grammar +- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — message parsing robustness diff --git a/docs/content/docs/malformed-input/long-header-name.md b/docs/content/docs/malformed-input/long-header-name.md index 35369ec..b1e1e78 100644 --- a/docs/content/docs/malformed-input/long-header-name.md +++ b/docs/content/docs/malformed-input/long-header-name.md @@ -1,86 +1,86 @@ ---- -title: "LONG-HEADER-NAME" -description: "LONG-HEADER-NAME test documentation" -weight: 3 ---- - -| | | -|---|---| -| **Test ID** | `MAL-LONG-HEADER-NAME` | -| **Category** | Malformed Input | -| **Expected** | `400`, `431`, or close | - -## What it sends - -A request with a ~100 KB header field name. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -AAAA...{100,000 × 'A'}...: val\r\n -\r\n -``` - -The header name is 100,000 bytes of `A` characters. - - -## What the RFC says - -> "The 431 status code indicates that the server is unwilling to process the request because its header fields are too large. The request MAY be resubmitted after reducing the size of the request header fields." — RFC 6585 Section 5 - -> "It can be used both when the set of request header fields in total is too large, and when a single header field is at fault. In the latter case, the response representation SHOULD specify which header field was too large." — RFC 6585 Section 5 - -A 100KB header name is a single field at fault. The server may respond with 431, 400, or close the connection. - -## Deep Analysis - -### ABNF context - -The header field grammar places no upper bound on field-name length: - -``` -field-line = field-name ":" OWS field-value OWS -field-name = token -token = 1*tchar -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" - / "~" / DIGIT / ALPHA -``` - -A field-name of 100,000 `A` characters is syntactically valid: each `A` is `ALPHA`, which is a `tchar`, and `token = 1*tchar` has no maximum length. The ABNF is satisfied. However, the RFC provides explicit mechanisms for servers to reject oversized fields. - -### RFC evidence - -> "The 431 status code indicates that the server is unwilling to process the request because its header fields are too large. The request MAY be resubmitted after reducing the size of the request header fields." -- RFC 6585 Section 5 - -> "It can be used both when the set of request header fields in total is too large, and when a single header field is at fault. In the latter case, the response representation SHOULD specify which header field was too large." -- RFC 6585 Section 5 - -> "Responses with the 431 status code MUST NOT be stored by a cache." -- RFC 6585 Section 5 - -> "It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets." -- RFC 9112 Section 3 - -> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 - -While RFC 9112 Section 3's 8000-octet recommendation applies to the request-line specifically, it establishes a precedent for reasonable size limits. A single 100KB header field-name dwarfs even generous limits. RFC 6585 Section 5 provides the specific 431 status code for exactly this scenario. - -### Chain of reasoning - -1. The server begins reading the header section after the request-line. -2. It encounters a field-line starting with 100,000 `A` characters followed by `: val`. -3. The server attempts to read the field-name, which requires buffering 100,000 bytes before reaching the colon. -4. A well-implemented server enforces a maximum field-line or field-name length (typically 8KB-64KB). -5. When the limit is exceeded, the server stops reading and responds with 431 (single header too large) or 400 (general client error). -6. Per RFC 6585 Section 5, if the server uses 431, it SHOULD indicate which header was at fault. -7. Closing the connection without a response is also acceptable as a protective measure. - -### Security implications - -- **Memory exhaustion (DoS)**: If the server buffers the entire field-name before checking its validity, a single request consumes 100KB of memory just for one header name. An attacker sending many such requests can rapidly exhaust server memory. -- **Hash-table performance degradation**: Servers typically store headers in a hash table keyed by field-name. A 100KB key causes excessive memory allocation for the hash entry, and if multiple such headers are sent, the hash table's memory usage grows proportionally. -- **Buffer overflow**: In languages without automatic bounds checking (C, C++), reading an unbounded field-name into a fixed-size buffer causes a classic stack or heap buffer overflow, potentially enabling code execution. -- **Log injection and storage exhaustion**: If the server logs unrecognized headers, a 100KB header name fills log files rapidly, potentially exhausting disk space and disrupting log rotation and monitoring systems. -- **Proxy forwarding amplification**: Some proxies buffer and forward all headers. A 100KB header name is forwarded to the backend, amplifying the attack through the infrastructure chain. - -## Sources - -- [RFC 6585 Section 5](https://www.rfc-editor.org/rfc/rfc6585#section-5) — 431 Request Header Fields Too Large +--- +title: "Long Header Name — Malformed Input" +description: "A request with a ~100 KB header field name. Part of Http11Probe's malformed input robustness suite." +weight: 3 +--- + +| | | +|---|---| +| **Test ID** | `MAL-LONG-HEADER-NAME` | +| **Category** | Malformed Input | +| **Expected** | `400`, `431`, or close | + +## What it sends + +A request with a ~100 KB header field name. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +AAAA...{100,000 × 'A'}...: val\r\n +\r\n +``` + +The header name is 100,000 bytes of `A` characters. + + +## What the RFC says + +> "The 431 status code indicates that the server is unwilling to process the request because its header fields are too large. The request MAY be resubmitted after reducing the size of the request header fields." — RFC 6585 Section 5 + +> "It can be used both when the set of request header fields in total is too large, and when a single header field is at fault. In the latter case, the response representation SHOULD specify which header field was too large." — RFC 6585 Section 5 + +A 100KB header name is a single field at fault. The server may respond with 431, 400, or close the connection. + +## Deep Analysis + +### ABNF context + +The header field grammar places no upper bound on field-name length: + +``` +field-line = field-name ":" OWS field-value OWS +field-name = token +token = 1*tchar +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" + / "~" / DIGIT / ALPHA +``` + +A field-name of 100,000 `A` characters is syntactically valid: each `A` is `ALPHA`, which is a `tchar`, and `token = 1*tchar` has no maximum length. The ABNF is satisfied. However, the RFC provides explicit mechanisms for servers to reject oversized fields. + +### RFC evidence + +> "The 431 status code indicates that the server is unwilling to process the request because its header fields are too large. The request MAY be resubmitted after reducing the size of the request header fields." -- RFC 6585 Section 5 + +> "It can be used both when the set of request header fields in total is too large, and when a single header field is at fault. In the latter case, the response representation SHOULD specify which header field was too large." -- RFC 6585 Section 5 + +> "Responses with the 431 status code MUST NOT be stored by a cache." -- RFC 6585 Section 5 + +> "It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets." -- RFC 9112 Section 3 + +> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 + +While RFC 9112 Section 3's 8000-octet recommendation applies to the request-line specifically, it establishes a precedent for reasonable size limits. A single 100KB header field-name dwarfs even generous limits. RFC 6585 Section 5 provides the specific 431 status code for exactly this scenario. + +### Chain of reasoning + +1. The server begins reading the header section after the request-line. +2. It encounters a field-line starting with 100,000 `A` characters followed by `: val`. +3. The server attempts to read the field-name, which requires buffering 100,000 bytes before reaching the colon. +4. A well-implemented server enforces a maximum field-line or field-name length (typically 8KB-64KB). +5. When the limit is exceeded, the server stops reading and responds with 431 (single header too large) or 400 (general client error). +6. Per RFC 6585 Section 5, if the server uses 431, it SHOULD indicate which header was at fault. +7. Closing the connection without a response is also acceptable as a protective measure. + +### Security implications + +- **Memory exhaustion (DoS)**: If the server buffers the entire field-name before checking its validity, a single request consumes 100KB of memory just for one header name. An attacker sending many such requests can rapidly exhaust server memory. +- **Hash-table performance degradation**: Servers typically store headers in a hash table keyed by field-name. A 100KB key causes excessive memory allocation for the hash entry, and if multiple such headers are sent, the hash table's memory usage grows proportionally. +- **Buffer overflow**: In languages without automatic bounds checking (C, C++), reading an unbounded field-name into a fixed-size buffer causes a classic stack or heap buffer overflow, potentially enabling code execution. +- **Log injection and storage exhaustion**: If the server logs unrecognized headers, a 100KB header name fills log files rapidly, potentially exhausting disk space and disrupting log rotation and monitoring systems. +- **Proxy forwarding amplification**: Some proxies buffer and forward all headers. A 100KB header name is forwarded to the backend, amplifying the attack through the infrastructure chain. + +## Sources + +- [RFC 6585 Section 5](https://www.rfc-editor.org/rfc/rfc6585#section-5) — 431 Request Header Fields Too Large diff --git a/docs/content/docs/malformed-input/long-header-value.md b/docs/content/docs/malformed-input/long-header-value.md index 41d6408..9b4a94f 100644 --- a/docs/content/docs/malformed-input/long-header-value.md +++ b/docs/content/docs/malformed-input/long-header-value.md @@ -1,85 +1,85 @@ ---- -title: "LONG-HEADER-VALUE" -description: "LONG-HEADER-VALUE test documentation" -weight: 4 ---- - -| | | -|---|---| -| **Test ID** | `MAL-LONG-HEADER-VALUE` | -| **Category** | Malformed Input | -| **Expected** | `400`, `431`, or close | - -## What it sends - -A request with a ~100 KB header field value. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -X-Big: BBBB...{100,000 × 'B'}...\r\n -\r\n -``` - -The `X-Big` header value is 100,000 bytes of `B` characters. - - -## What the RFC says - -> "The 431 status code indicates that the server is unwilling to process the request because its header fields are too large. The request MAY be resubmitted after reducing the size of the request header fields." — RFC 6585 Section 5 - -> "It can be used both when the set of request header fields in total is too large, and when a single header field is at fault. In the latter case, the response representation SHOULD specify which header field was too large." — RFC 6585 Section 5 - -A 100KB header value far exceeds any reasonable limit. The server may respond with 431, 400 (general client error), or close the connection. - -## Deep Analysis - -### ABNF context - -The field-value grammar places no upper bound on value length: - -``` -field-line = field-name ":" OWS field-value OWS -field-value = *field-content -field-content = field-vchar - [ 1*( SP / HTAB / field-vchar ) field-vchar ] -field-vchar = VCHAR / obs-text -VCHAR = %x21-7E -``` - -A field-value of 100,000 `B` characters is syntactically valid: each `B` is `VCHAR` (`0x42`), therefore `field-vchar`, and `field-value = *field-content` permits any number of `field-content` repetitions. The ABNF has no maximum length. However, the RFC provides explicit mechanisms for servers to reject oversized fields. - -### RFC evidence - -> "The 431 status code indicates that the server is unwilling to process the request because its header fields are too large. The request MAY be resubmitted after reducing the size of the request header fields." -- RFC 6585 Section 5 - -> "It can be used both when the set of request header fields in total is too large, and when a single header field is at fault. In the latter case, the response representation SHOULD specify which header field was too large." -- RFC 6585 Section 5 - -> "Responses with the 431 status code MUST NOT be stored by a cache." -- RFC 6585 Section 5 - -> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 - -> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 - -RFC 6585 Section 5 was specifically designed for this scenario. The 431 status code applies both to the aggregate size of all headers and to a single header field at fault. A 100KB value for a single header clearly falls into the "single header field is at fault" category. - -### Chain of reasoning - -1. The server reads the header section and encounters `X-Big: BBBB...` (100,000 bytes of `B`). -2. It begins buffering the field-value, reading `VCHAR` characters until it reaches `\r\n`. -3. A well-implemented server enforces a maximum field-value or total-field-line length. -4. After exceeding the limit (e.g., 8KB, 16KB, or 64KB depending on implementation), the server stops reading. -5. It responds with 431 (indicating the `X-Big` header is too large) or 400 (general rejection) and closes the connection. -6. If the server has no limit and buffers the entire 100KB value, it has consumed significant memory for a single header of a single request -- a resource that an attacker can exploit at scale. - -### Security implications - -- **Memory exhaustion (DoS)**: Each request with a 100KB header value consumes 100KB of memory. At 10,000 concurrent connections, that is 1GB just for one header per request. Servers without limits are vulnerable to memory-based denial of service. -- **Hash-table storage amplification**: The header value is typically stored in the server's header hash table. A 100KB value plus its key and metadata consumes far more memory than a typical header, and the server may allocate this memory before processing or rejecting the request. -- **Buffer overflow**: Fixed-size buffers in C/C++ servers that do not check lengths before copying header values are vulnerable to heap or stack overflows, potentially leading to remote code execution. -- **Application-layer impact**: Even if the server's HTTP parser handles the oversized header correctly, the application layer may process it unsafely. For example, if `X-Big` is logged, displayed, or stored in a database, the 100KB value can cause log bloat, UI rendering issues, or database column overflow. -- **Proxy amplification**: Proxies that forward all headers verbatim pass the 100KB value to the backend, multiplying the memory impact across the infrastructure chain. If the proxy has different limits than the backend, the disagreement creates a potential for differential behavior exploitation. - -## Sources - -- [RFC 6585 Section 5](https://www.rfc-editor.org/rfc/rfc6585#section-5) — 431 Request Header Fields Too Large +--- +title: "Long Header Value — Malformed Input" +description: "A request with a ~100 KB header field value. Part of Http11Probe's malformed input robustness suite." +weight: 4 +--- + +| | | +|---|---| +| **Test ID** | `MAL-LONG-HEADER-VALUE` | +| **Category** | Malformed Input | +| **Expected** | `400`, `431`, or close | + +## What it sends + +A request with a ~100 KB header field value. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +X-Big: BBBB...{100,000 × 'B'}...\r\n +\r\n +``` + +The `X-Big` header value is 100,000 bytes of `B` characters. + + +## What the RFC says + +> "The 431 status code indicates that the server is unwilling to process the request because its header fields are too large. The request MAY be resubmitted after reducing the size of the request header fields." — RFC 6585 Section 5 + +> "It can be used both when the set of request header fields in total is too large, and when a single header field is at fault. In the latter case, the response representation SHOULD specify which header field was too large." — RFC 6585 Section 5 + +A 100KB header value far exceeds any reasonable limit. The server may respond with 431, 400 (general client error), or close the connection. + +## Deep Analysis + +### ABNF context + +The field-value grammar places no upper bound on value length: + +``` +field-line = field-name ":" OWS field-value OWS +field-value = *field-content +field-content = field-vchar + [ 1*( SP / HTAB / field-vchar ) field-vchar ] +field-vchar = VCHAR / obs-text +VCHAR = %x21-7E +``` + +A field-value of 100,000 `B` characters is syntactically valid: each `B` is `VCHAR` (`0x42`), therefore `field-vchar`, and `field-value = *field-content` permits any number of `field-content` repetitions. The ABNF has no maximum length. However, the RFC provides explicit mechanisms for servers to reject oversized fields. + +### RFC evidence + +> "The 431 status code indicates that the server is unwilling to process the request because its header fields are too large. The request MAY be resubmitted after reducing the size of the request header fields." -- RFC 6585 Section 5 + +> "It can be used both when the set of request header fields in total is too large, and when a single header field is at fault. In the latter case, the response representation SHOULD specify which header field was too large." -- RFC 6585 Section 5 + +> "Responses with the 431 status code MUST NOT be stored by a cache." -- RFC 6585 Section 5 + +> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." -- RFC 9110 Section 15.5.1 + +> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 + +RFC 6585 Section 5 was specifically designed for this scenario. The 431 status code applies both to the aggregate size of all headers and to a single header field at fault. A 100KB value for a single header clearly falls into the "single header field is at fault" category. + +### Chain of reasoning + +1. The server reads the header section and encounters `X-Big: BBBB...` (100,000 bytes of `B`). +2. It begins buffering the field-value, reading `VCHAR` characters until it reaches `\r\n`. +3. A well-implemented server enforces a maximum field-value or total-field-line length. +4. After exceeding the limit (e.g., 8KB, 16KB, or 64KB depending on implementation), the server stops reading. +5. It responds with 431 (indicating the `X-Big` header is too large) or 400 (general rejection) and closes the connection. +6. If the server has no limit and buffers the entire 100KB value, it has consumed significant memory for a single header of a single request -- a resource that an attacker can exploit at scale. + +### Security implications + +- **Memory exhaustion (DoS)**: Each request with a 100KB header value consumes 100KB of memory. At 10,000 concurrent connections, that is 1GB just for one header per request. Servers without limits are vulnerable to memory-based denial of service. +- **Hash-table storage amplification**: The header value is typically stored in the server's header hash table. A 100KB value plus its key and metadata consumes far more memory than a typical header, and the server may allocate this memory before processing or rejecting the request. +- **Buffer overflow**: Fixed-size buffers in C/C++ servers that do not check lengths before copying header values are vulnerable to heap or stack overflows, potentially leading to remote code execution. +- **Application-layer impact**: Even if the server's HTTP parser handles the oversized header correctly, the application layer may process it unsafely. For example, if `X-Big` is logged, displayed, or stored in a database, the 100KB value can cause log bloat, UI rendering issues, or database column overflow. +- **Proxy amplification**: Proxies that forward all headers verbatim pass the 100KB value to the backend, multiplying the memory impact across the infrastructure chain. If the proxy has different limits than the backend, the disagreement creates a potential for differential behavior exploitation. + +## Sources + +- [RFC 6585 Section 5](https://www.rfc-editor.org/rfc/rfc6585#section-5) — 431 Request Header Fields Too Large diff --git a/docs/content/docs/malformed-input/long-method.md b/docs/content/docs/malformed-input/long-method.md index be1b75b..9fdf36e 100644 --- a/docs/content/docs/malformed-input/long-method.md +++ b/docs/content/docs/malformed-input/long-method.md @@ -1,91 +1,91 @@ ---- -title: "LONG-METHOD" -description: "LONG-METHOD test documentation" -weight: 5 ---- - -| | | -|---|---| -| **Test ID** | `MAL-LONG-METHOD` | -| **Category** | Malformed Input | -| **Expected** | `400` or close | - -## What it sends - -A request with a ~100 KB method name. - -```http -AAAA...{100,000 × 'A'}... / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -The HTTP method is 100,000 bytes of `A` characters. - - -## What the RFC says - -> "A server that receives a method longer than any that it implements SHOULD respond with a 501 (Not Implemented) status code." — RFC 9112 Section 3 - -> "It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets." — RFC 9112 Section 3 - -The method is part of the request-line. A 100KB method vastly exceeds the recommended 8000-octet minimum. The server may respond with 400, 501, or close the connection. - -> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error." — RFC 9110 Section 15.5.1 - -## Why it matters - -Methods are tokens with no defined maximum length, but 100 KB exceeds any reasonable limit. A server that buffers this risks memory exhaustion. - -## Deep Analysis - -### ABNF context - -The method is a token with no upper bound: - -``` -request-line = method SP request-target SP HTTP-version -method = token -token = 1*tchar -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" - / "~" / DIGIT / ALPHA -``` - -A method of 100,000 `A` characters matches `token = 1*tchar` perfectly -- each `A` is `ALPHA`, which is a `tchar`. The ABNF places no upper bound. However, the entire request-line (method + SP + request-target + SP + HTTP-version) is 100,000 + 1 + 1 + 1 + 8 = 100,011 bytes, far exceeding the RFC's recommended minimum of 8,000 octets. - -### RFC evidence - -> "A server that receives a method longer than any that it implements SHOULD respond with a 501 (Not Implemented) status code." -- RFC 9112 Section 3 - -> "It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets." -- RFC 9112 Section 3 - -> "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 Section 3 - -> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 - -The RFC provides multiple rejection mechanisms: 501 for unrecognized methods, 414 for oversized request-targets (and by analogy, oversized request-lines), and 400 for general grammar violations. The RECOMMENDED 8,000-octet minimum means a server that supports only 8,000 octets is conformant -- and a 100,011-byte request-line exceeds that by a factor of 12.5. - -### Chain of reasoning - -1. The server begins reading the request-line, expecting `method SP request-target SP HTTP-version`. -2. It reads `tchar` bytes for the method: `AAAA...` (100,000 bytes). -3. A well-implemented server enforces a maximum request-line length. After exceeding its limit (e.g., 8KB), it stops reading. -4. Multiple appropriate responses exist: - - **400**: The request-line exceeds the server's length limit, making it unparseable. - - **501**: The method is longer than any implemented method (GET, POST, etc. are all under 10 bytes), triggering the SHOULD-501 from RFC 9112 Section 3. - - **Connection close**: The server may simply close the connection without responding, especially if it cannot even parse enough of the request to formulate a response. -5. The 100KB method is not merely an unrecognized method name -- it is a resource exhaustion attack disguised as a method token. - -### Security implications - -- **Memory exhaustion (DoS)**: If the server buffers the entire method before checking if it is recognized, each request consumes 100KB just for the method string. An attacker can send thousands of such requests to exhaust server memory. -- **Buffer overflow**: In C/C++ servers, a fixed-size buffer for the method (e.g., `char method[256]`) will overflow when reading 100,000 bytes, potentially enabling remote code execution. -- **Request-line parsing stall**: The server reads `tchar` bytes looking for the first `SP` delimiter. With 100,000 bytes before the space, the parser spends significant CPU time in the read loop, reducing throughput for legitimate requests. -- **Log injection and storage exhaustion**: If the server logs the method name (as many access logs do), a 100KB method fills log storage rapidly. At 1,000 requests per second, that is 100MB/s of log data from the method alone. -- **WAF and IDS evasion**: Security devices that inspect the method field may have their own buffer limits. A 100KB method may cause the security device to truncate or skip inspection, while the backend server processes it differently. - -## Sources - -- [RFC 9112 Section 3](https://www.rfc-editor.org/rfc/rfc9112#section-3) — request-line and method length -- [RFC 9110 Section 15.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.1) — 400 Bad Request +--- +title: "Long Method — Malformed Input" +description: "A request with a ~100 KB method name. Part of Http11Probe's malformed input robustness suite." +weight: 5 +--- + +| | | +|---|---| +| **Test ID** | `MAL-LONG-METHOD` | +| **Category** | Malformed Input | +| **Expected** | `400` or close | + +## What it sends + +A request with a ~100 KB method name. + +```http +AAAA...{100,000 × 'A'}... / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +The HTTP method is 100,000 bytes of `A` characters. + + +## What the RFC says + +> "A server that receives a method longer than any that it implements SHOULD respond with a 501 (Not Implemented) status code." — RFC 9112 Section 3 + +> "It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets." — RFC 9112 Section 3 + +The method is part of the request-line. A 100KB method vastly exceeds the recommended 8000-octet minimum. The server may respond with 400, 501, or close the connection. + +> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error." — RFC 9110 Section 15.5.1 + +## Why it matters + +Methods are tokens with no defined maximum length, but 100 KB exceeds any reasonable limit. A server that buffers this risks memory exhaustion. + +## Deep Analysis + +### ABNF context + +The method is a token with no upper bound: + +``` +request-line = method SP request-target SP HTTP-version +method = token +token = 1*tchar +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" + / "~" / DIGIT / ALPHA +``` + +A method of 100,000 `A` characters matches `token = 1*tchar` perfectly -- each `A` is `ALPHA`, which is a `tchar`. The ABNF places no upper bound. However, the entire request-line (method + SP + request-target + SP + HTTP-version) is 100,000 + 1 + 1 + 1 + 8 = 100,011 bytes, far exceeding the RFC's recommended minimum of 8,000 octets. + +### RFC evidence + +> "A server that receives a method longer than any that it implements SHOULD respond with a 501 (Not Implemented) status code." -- RFC 9112 Section 3 + +> "It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets." -- RFC 9112 Section 3 + +> "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 Section 3 + +> "When a server listening only for HTTP request messages, or processing what appears from the start-line to be an HTTP request message, receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -- RFC 9112 Section 2.2 + +The RFC provides multiple rejection mechanisms: 501 for unrecognized methods, 414 for oversized request-targets (and by analogy, oversized request-lines), and 400 for general grammar violations. The RECOMMENDED 8,000-octet minimum means a server that supports only 8,000 octets is conformant -- and a 100,011-byte request-line exceeds that by a factor of 12.5. + +### Chain of reasoning + +1. The server begins reading the request-line, expecting `method SP request-target SP HTTP-version`. +2. It reads `tchar` bytes for the method: `AAAA...` (100,000 bytes). +3. A well-implemented server enforces a maximum request-line length. After exceeding its limit (e.g., 8KB), it stops reading. +4. Multiple appropriate responses exist: + - **400**: The request-line exceeds the server's length limit, making it unparseable. + - **501**: The method is longer than any implemented method (GET, POST, etc. are all under 10 bytes), triggering the SHOULD-501 from RFC 9112 Section 3. + - **Connection close**: The server may simply close the connection without responding, especially if it cannot even parse enough of the request to formulate a response. +5. The 100KB method is not merely an unrecognized method name -- it is a resource exhaustion attack disguised as a method token. + +### Security implications + +- **Memory exhaustion (DoS)**: If the server buffers the entire method before checking if it is recognized, each request consumes 100KB just for the method string. An attacker can send thousands of such requests to exhaust server memory. +- **Buffer overflow**: In C/C++ servers, a fixed-size buffer for the method (e.g., `char method[256]`) will overflow when reading 100,000 bytes, potentially enabling remote code execution. +- **Request-line parsing stall**: The server reads `tchar` bytes looking for the first `SP` delimiter. With 100,000 bytes before the space, the parser spends significant CPU time in the read loop, reducing throughput for legitimate requests. +- **Log injection and storage exhaustion**: If the server logs the method name (as many access logs do), a 100KB method fills log storage rapidly. At 1,000 requests per second, that is 100MB/s of log data from the method alone. +- **WAF and IDS evasion**: Security devices that inspect the method field may have their own buffer limits. A 100KB method may cause the security device to truncate or skip inspection, while the backend server processes it differently. + +## Sources + +- [RFC 9112 Section 3](https://www.rfc-editor.org/rfc/rfc9112#section-3) — request-line and method length +- [RFC 9110 Section 15.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.1) — 400 Bad Request diff --git a/docs/content/docs/malformed-input/long-url.md b/docs/content/docs/malformed-input/long-url.md index 5bb0bdf..eb15415 100644 --- a/docs/content/docs/malformed-input/long-url.md +++ b/docs/content/docs/malformed-input/long-url.md @@ -1,75 +1,75 @@ ---- -title: "LONG-URL" -description: "LONG-URL test documentation" -weight: 2 ---- - -| | | -|---|---| -| **Test ID** | `MAL-LONG-URL` | -| **Category** | Malformed Input | -| **RFC** | [RFC 9112 Section 3](https://www.rfc-editor.org/rfc/rfc9112#section-3) | -| **Expected** | `400`, `414`, `431`, or close | - -## What it sends - -A request with a ~100 KB URL. - -```http -GET /AAAA...{100,000 × 'A'}... HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -The URL path is 100,001 bytes long (a `/` followed by 100,000 `A` characters). - - -## What the RFC says - -> "The 414 (URI Too Long) status code indicates that the server is refusing to service the request because the target URI is longer than the server is willing to interpret." — RFC 9110 Section 15.5.15 - -> "It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets." — RFC 9112 Section 3 - -A 100KB URL far exceeds the recommended minimum. The server may also respond with 400 (general client error) or close the connection. - -## Deep Analysis - -### Relevant ABNF - -``` -request-line = method SP request-target SP HTTP-version -request-target = origin-form / absolute-form / authority-form / asterisk-form -pchar = unreserved / pct-encoded / sub-delims / ":" / "@" -unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" -``` - -### RFC Evidence - -> "HTTP does not place a predefined limit on the length of a request-line." -> -- RFC 9112 Section 3 - -> "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 Section 3 - -> "It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets." -> -- RFC 9112 Section 3 - -> "The 414 (URI Too Long) status code indicates that the server is refusing to service the request because the target URI is longer than the server is willing to interpret." -> -- RFC 9110 Section 15.5.15 - -### Chain of Reasoning - -1. **The request is syntactically valid.** A 100,000-character path of `A` characters is composed entirely of `ALPHA`, which satisfies `unreserved` and therefore `pchar`. The request-line grammar itself is not violated. - -2. **No maximum length is mandated.** RFC 9112 Section 3 explicitly states that HTTP does not place a predefined limit on request-line length. However, servers are free to impose their own limits. - -3. **The MUST-level requirement applies.** When a server encounters a request-target longer than it is willing to parse, RFC 9112 Section 3 uses MUST-level language: the server "MUST respond with a 414 (URI Too Long) status code." This is one of the few places the RFC mandates a specific status code for length violations. - -4. **The 8,000-octet recommendation sets a floor.** The RECOMMENDED minimum of 8,000 octets means a 100,001-byte URL (100,000 `A` + leading `/`) exceeds the recommended minimum by over 12x. Any server implementing the recommended minimum would reject this. - -5. **Alternative responses are acceptable.** A server may also respond with 400 (general syntax error), 431 (header fields too large, if the entire request-line is counted toward header limits), or simply close the connection. All indicate the server is protecting itself from oversized input. - -## Sources - -- [RFC 9110 Section 15.5.15](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.15) — 414 URI Too Long -- [RFC 9112 Section 3](https://www.rfc-editor.org/rfc/rfc9112#section-3) — request-line length recommendation +--- +title: "Long URL — Malformed Input" +description: "A request with a ~100 KB URL. Tested against RFC 9112 Section 3." +weight: 2 +--- + +| | | +|---|---| +| **Test ID** | `MAL-LONG-URL` | +| **Category** | Malformed Input | +| **RFC** | [RFC 9112 Section 3](https://www.rfc-editor.org/rfc/rfc9112#section-3) | +| **Expected** | `400`, `414`, `431`, or close | + +## What it sends + +A request with a ~100 KB URL. + +```http +GET /AAAA...{100,000 × 'A'}... HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +The URL path is 100,001 bytes long (a `/` followed by 100,000 `A` characters). + + +## What the RFC says + +> "The 414 (URI Too Long) status code indicates that the server is refusing to service the request because the target URI is longer than the server is willing to interpret." — RFC 9110 Section 15.5.15 + +> "It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets." — RFC 9112 Section 3 + +A 100KB URL far exceeds the recommended minimum. The server may also respond with 400 (general client error) or close the connection. + +## Deep Analysis + +### Relevant ABNF + +``` +request-line = method SP request-target SP HTTP-version +request-target = origin-form / absolute-form / authority-form / asterisk-form +pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +``` + +### RFC Evidence + +> "HTTP does not place a predefined limit on the length of a request-line." +> -- RFC 9112 Section 3 + +> "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 Section 3 + +> "It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets." +> -- RFC 9112 Section 3 + +> "The 414 (URI Too Long) status code indicates that the server is refusing to service the request because the target URI is longer than the server is willing to interpret." +> -- RFC 9110 Section 15.5.15 + +### Chain of Reasoning + +1. **The request is syntactically valid.** A 100,000-character path of `A` characters is composed entirely of `ALPHA`, which satisfies `unreserved` and therefore `pchar`. The request-line grammar itself is not violated. + +2. **No maximum length is mandated.** RFC 9112 Section 3 explicitly states that HTTP does not place a predefined limit on request-line length. However, servers are free to impose their own limits. + +3. **The MUST-level requirement applies.** When a server encounters a request-target longer than it is willing to parse, RFC 9112 Section 3 uses MUST-level language: the server "MUST respond with a 414 (URI Too Long) status code." This is one of the few places the RFC mandates a specific status code for length violations. + +4. **The 8,000-octet recommendation sets a floor.** The RECOMMENDED minimum of 8,000 octets means a 100,001-byte URL (100,000 `A` + leading `/`) exceeds the recommended minimum by over 12x. Any server implementing the recommended minimum would reject this. + +5. **Alternative responses are acceptable.** A server may also respond with 400 (general syntax error), 431 (header fields too large, if the entire request-line is counted toward header limits), or simply close the connection. All indicate the server is protecting itself from oversized input. + +## Sources + +- [RFC 9110 Section 15.5.15](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.15) — 414 URI Too Long +- [RFC 9112 Section 3](https://www.rfc-editor.org/rfc/rfc9112#section-3) — request-line length recommendation diff --git a/docs/content/docs/malformed-input/many-headers.md b/docs/content/docs/malformed-input/many-headers.md index 10fd0a0..c1400e9 100644 --- a/docs/content/docs/malformed-input/many-headers.md +++ b/docs/content/docs/malformed-input/many-headers.md @@ -1,77 +1,77 @@ ---- -title: "MANY-HEADERS" -description: "MANY-HEADERS test documentation" -weight: 6 ---- - -| | | -|---|---| -| **Test ID** | `MAL-MANY-HEADERS` | -| **Category** | Malformed Input | -| **Expected** | `400`, `431`, or close | - -## What it sends - -A request with 10,000 header fields. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -X-H-0: value\r\n -X-H-1: value\r\n -X-H-2: value\r\n -... (10,000 headers total) ...\r\n -X-H-9999: value\r\n -\r\n -``` - - -## What the RFC says - -> "The 431 status code indicates that the server is unwilling to process the request because its header fields are too large. The request MAY be resubmitted after reducing the size of the request header fields." — RFC 6585 Section 5 - -> "It can be used both when the set of request header fields in total is too large, and when a single header field is at fault." — RFC 6585 Section 5 - -With 10,000 header fields, the set of request headers in total is too large. The server may respond with 431, 400, or close the connection. - -## Why it matters - -Servers typically allocate data structures for each header. 10,000 headers can cause excessive memory allocation, hash table collisions, or O(n^2) lookup behavior. - -## Deep Analysis - -### Relevant ABNF - -``` -HTTP-message = start-line CRLF *( field-line CRLF ) CRLF [ message-body ] -field-line = field-name ":" OWS field-value OWS -field-name = token -token = 1*tchar -``` - -### RFC Evidence - -> "The 431 status code indicates that the server is unwilling to process the request because its header fields are too large." -> -- RFC 6585 Section 5 - -> "It can be used both when the set of request header fields in total is too large, and when a single header field is at fault." -> -- RFC 6585 Section 5 - -> "Responses with the 431 status code MUST NOT be stored by a cache." -> -- RFC 6585 Section 5 - -### Chain of Reasoning - -1. **Each header is individually valid.** Every `X-H-N: value` header conforms to the `field-line` grammar: `X-H-N` is a valid `token` (composed of `tchar` characters) and `value` is valid `field-content`. - -2. **The HTTP grammar allows unlimited headers.** The `*( field-line CRLF )` production uses the `*` (zero or more) repetition operator with no upper bound. The grammar alone does not restrict the number of header fields. - -3. **RFC 6585 provides the rejection mechanism.** The 431 status code was created specifically for this scenario. It applies to both the total size of all headers and to an excessive number of individual fields. With 10,000 headers, the aggregate size easily exceeds any reasonable limit. - -4. **Server resource exhaustion is the concern.** Each header field requires parsing, memory allocation, and storage in internal data structures. 10,000 headers can trigger O(n) or O(n^2) behavior in hash table implementations, excessive memory allocation, and slow header lookup during request processing. - -5. **400 and connection close are also acceptable.** A server may choose to respond with 400 (Bad Request) as a general rejection, or close the connection outright if it detects the header section exceeds its configured limits before reading the complete request. - -## Sources - -- [RFC 6585 Section 5](https://www.rfc-editor.org/rfc/rfc6585#section-5) — 431 Request Header Fields Too Large +--- +title: "Many Headers — Malformed Input" +description: "A request with 10,000 header fields. Part of Http11Probe's malformed input robustness suite." +weight: 6 +--- + +| | | +|---|---| +| **Test ID** | `MAL-MANY-HEADERS` | +| **Category** | Malformed Input | +| **Expected** | `400`, `431`, or close | + +## What it sends + +A request with 10,000 header fields. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +X-H-0: value\r\n +X-H-1: value\r\n +X-H-2: value\r\n +... (10,000 headers total) ...\r\n +X-H-9999: value\r\n +\r\n +``` + + +## What the RFC says + +> "The 431 status code indicates that the server is unwilling to process the request because its header fields are too large. The request MAY be resubmitted after reducing the size of the request header fields." — RFC 6585 Section 5 + +> "It can be used both when the set of request header fields in total is too large, and when a single header field is at fault." — RFC 6585 Section 5 + +With 10,000 header fields, the set of request headers in total is too large. The server may respond with 431, 400, or close the connection. + +## Why it matters + +Servers typically allocate data structures for each header. 10,000 headers can cause excessive memory allocation, hash table collisions, or O(n^2) lookup behavior. + +## Deep Analysis + +### Relevant ABNF + +``` +HTTP-message = start-line CRLF *( field-line CRLF ) CRLF [ message-body ] +field-line = field-name ":" OWS field-value OWS +field-name = token +token = 1*tchar +``` + +### RFC Evidence + +> "The 431 status code indicates that the server is unwilling to process the request because its header fields are too large." +> -- RFC 6585 Section 5 + +> "It can be used both when the set of request header fields in total is too large, and when a single header field is at fault." +> -- RFC 6585 Section 5 + +> "Responses with the 431 status code MUST NOT be stored by a cache." +> -- RFC 6585 Section 5 + +### Chain of Reasoning + +1. **Each header is individually valid.** Every `X-H-N: value` header conforms to the `field-line` grammar: `X-H-N` is a valid `token` (composed of `tchar` characters) and `value` is valid `field-content`. + +2. **The HTTP grammar allows unlimited headers.** The `*( field-line CRLF )` production uses the `*` (zero or more) repetition operator with no upper bound. The grammar alone does not restrict the number of header fields. + +3. **RFC 6585 provides the rejection mechanism.** The 431 status code was created specifically for this scenario. It applies to both the total size of all headers and to an excessive number of individual fields. With 10,000 headers, the aggregate size easily exceeds any reasonable limit. + +4. **Server resource exhaustion is the concern.** Each header field requires parsing, memory allocation, and storage in internal data structures. 10,000 headers can trigger O(n) or O(n^2) behavior in hash table implementations, excessive memory allocation, and slow header lookup during request processing. + +5. **400 and connection close are also acceptable.** A server may choose to respond with 400 (Bad Request) as a general rejection, or close the connection outright if it detects the header section exceeds its configured limits before reading the complete request. + +## Sources + +- [RFC 6585 Section 5](https://www.rfc-editor.org/rfc/rfc6585#section-5) — 431 Request Header Fields Too Large diff --git a/docs/content/docs/malformed-input/non-ascii-header-name.md b/docs/content/docs/malformed-input/non-ascii-header-name.md index 672d112..0186844 100644 --- a/docs/content/docs/malformed-input/non-ascii-header-name.md +++ b/docs/content/docs/malformed-input/non-ascii-header-name.md @@ -1,82 +1,82 @@ ---- -title: "NON-ASCII-HEADER-NAME" -description: "NON-ASCII-HEADER-NAME test documentation" -weight: 9 ---- - -| | | -|---|---| -| **Test ID** | `MAL-NON-ASCII-HEADER-NAME` | -| **Category** | Malformed Input | -| **Expected** | `400` or close | - -## What it sends - -A request with non-ASCII bytes (`\x80`-`\xFF`) in a header field name. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -X-T\xC3\xABst: value\r\n -\r\n -``` - -The header name `X-Tëst` contains UTF-8 encoded `ë` (`\xC3\xAB`) — non-ASCII bytes in a header name. - - -## What the RFC says - -> `field-name = token` — RFC 9110 Section 5.1 - -> `token = 1*tchar` — RFC 9110 Section 5.6.2 - -> `tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA` — RFC 9110 Section 5.6.2 - -The `tchar` production is restricted to a specific set of ASCII characters. The bytes `0xC3 0xAB` (UTF-8 `e` with diaeresis) are outside this set, making the header name invalid. - -> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar...the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 - -## Deep Analysis - -### Relevant ABNF - -``` -field-line = field-name ":" OWS field-value OWS -field-name = token -token = 1*tchar -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" - / DIGIT / ALPHA - ; ALPHA = %x41-5A / %x61-7A (ASCII letters only) - ; DIGIT = %x30-39 (ASCII digits only) -``` - -### RFC Evidence - -> "A field name is a token." -> -- RFC 9110 Section 5.1 - -> `token = 1*tchar` -> `tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA` -> -- RFC 9110 Section 5.6.2 - -> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -> -- RFC 9112 Section 2.2 - -### Chain of Reasoning - -1. **The `tchar` set is exclusively ASCII.** The `ALPHA` rule in ABNF core rules (RFC 5234) covers only `%x41-5A` (A-Z) and `%x61-7A` (a-z). The `DIGIT` rule covers only `%x30-39` (0-9). The explicitly listed special characters are all ASCII. No byte above `0x7E` is included. - -2. **The header name contains bytes `0xC3 0xAB`.** These two bytes are the UTF-8 encoding of U+00EB (Latin small letter e with diaeresis). Both `0xC3` and `0xAB` fall outside the `tchar` character set, which only spans a subset of `%x21-7E`. - -3. **The grammar violation is unambiguous.** Since `field-name = token = 1*tchar`, and `0xC3` is not a `tchar`, the parser fails at the first non-ASCII byte. The entire `field-line` does not match the HTTP-message grammar. - -4. **The SHOULD-level rejection applies.** RFC 9112 Section 2.2 instructs servers to respond with 400 and close the connection when receiving octets that do not match the HTTP-message grammar. This is a direct, unambiguous application of that rule. - -5. **No autocorrection is appropriate.** RFC 9112 Section 3 states that "A recipient SHOULD NOT attempt to autocorrect and then process the request without a redirect, since the invalid request-line might be deliberately crafted to bypass security filters." The same principle extends to header field parsing -- silently accepting non-ASCII header names could enable filter bypasses. - -## Sources - -- [RFC 9110 Section 5.1](https://www.rfc-editor.org/rfc/rfc9110#section-5.1) — field-name = token -- [RFC 9110 Section 5.6.2](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.2) — token and tchar ABNF -- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — rejection of invalid messages +--- +title: "Non Ascii Header Name — Malformed Input" +description: "A request with non-ASCII bytes (\\x80-\\xFF) in a header field name. Part of Http11Probe's malformed input robustness suite." +weight: 9 +--- + +| | | +|---|---| +| **Test ID** | `MAL-NON-ASCII-HEADER-NAME` | +| **Category** | Malformed Input | +| **Expected** | `400` or close | + +## What it sends + +A request with non-ASCII bytes (`\x80`-`\xFF`) in a header field name. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +X-T\xC3\xABst: value\r\n +\r\n +``` + +The header name `X-Tëst` contains UTF-8 encoded `ë` (`\xC3\xAB`) — non-ASCII bytes in a header name. + + +## What the RFC says + +> `field-name = token` — RFC 9110 Section 5.1 + +> `token = 1*tchar` — RFC 9110 Section 5.6.2 + +> `tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA` — RFC 9110 Section 5.6.2 + +The `tchar` production is restricted to a specific set of ASCII characters. The bytes `0xC3 0xAB` (UTF-8 `e` with diaeresis) are outside this set, making the header name invalid. + +> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar...the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 + +## Deep Analysis + +### Relevant ABNF + +``` +field-line = field-name ":" OWS field-value OWS +field-name = token +token = 1*tchar +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + / DIGIT / ALPHA + ; ALPHA = %x41-5A / %x61-7A (ASCII letters only) + ; DIGIT = %x30-39 (ASCII digits only) +``` + +### RFC Evidence + +> "A field name is a token." +> -- RFC 9110 Section 5.1 + +> `token = 1*tchar` +> `tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA` +> -- RFC 9110 Section 5.6.2 + +> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." +> -- RFC 9112 Section 2.2 + +### Chain of Reasoning + +1. **The `tchar` set is exclusively ASCII.** The `ALPHA` rule in ABNF core rules (RFC 5234) covers only `%x41-5A` (A-Z) and `%x61-7A` (a-z). The `DIGIT` rule covers only `%x30-39` (0-9). The explicitly listed special characters are all ASCII. No byte above `0x7E` is included. + +2. **The header name contains bytes `0xC3 0xAB`.** These two bytes are the UTF-8 encoding of U+00EB (Latin small letter e with diaeresis). Both `0xC3` and `0xAB` fall outside the `tchar` character set, which only spans a subset of `%x21-7E`. + +3. **The grammar violation is unambiguous.** Since `field-name = token = 1*tchar`, and `0xC3` is not a `tchar`, the parser fails at the first non-ASCII byte. The entire `field-line` does not match the HTTP-message grammar. + +4. **The SHOULD-level rejection applies.** RFC 9112 Section 2.2 instructs servers to respond with 400 and close the connection when receiving octets that do not match the HTTP-message grammar. This is a direct, unambiguous application of that rule. + +5. **No autocorrection is appropriate.** RFC 9112 Section 3 states that "A recipient SHOULD NOT attempt to autocorrect and then process the request without a redirect, since the invalid request-line might be deliberately crafted to bypass security filters." The same principle extends to header field parsing -- silently accepting non-ASCII header names could enable filter bypasses. + +## Sources + +- [RFC 9110 Section 5.1](https://www.rfc-editor.org/rfc/rfc9110#section-5.1) — field-name = token +- [RFC 9110 Section 5.6.2](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.2) — token and tchar ABNF +- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — rejection of invalid messages diff --git a/docs/content/docs/malformed-input/non-ascii-url.md b/docs/content/docs/malformed-input/non-ascii-url.md index e225072..6c60c91 100644 --- a/docs/content/docs/malformed-input/non-ascii-url.md +++ b/docs/content/docs/malformed-input/non-ascii-url.md @@ -1,82 +1,82 @@ ---- -title: "NON-ASCII-URL" -description: "NON-ASCII-URL test documentation" -weight: 10 ---- - -| | | -|---|---| -| **Test ID** | `MAL-NON-ASCII-URL` | -| **Category** | Malformed Input | -| **Expected** | `400` or close | - -## What it sends - -A request with non-ASCII bytes in the URL. - -```http -GET /caf\xC3\xA9 HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -The URL `/café` contains UTF-8 encoded `é` (`\xC3\xA9`) — non-ASCII bytes in the request-target. - - -## What the RFC says - -> "A URI is composed from a limited set of characters consisting of digits, letters, and a few graphic symbols." — RFC 3986 Section 2 - -The path component of a URI is constrained to `pchar`: - -> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` — RFC 3986 Section 3.3 - -> `unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"` — RFC 3986 Section 2.3 - -All allowed characters are ASCII. Raw bytes `0xC3 0xA9` (UTF-8 for `e` with acute accent) are not valid URI characters -- they must be percent-encoded as `%C3%A9`. - -> "A percent-encoded octet is encoded as a character triplet, consisting of the percent character '%' followed by the two hexadecimal digits representing that octet's numeric value." — RFC 3986 Section 2.1 - -## Deep Analysis - -### Relevant ABNF - -``` -request-target = origin-form / absolute-form / authority-form / asterisk-form -origin-form = absolute-path [ "?" query ] -absolute-path = 1*( "/" segment ) -segment = *pchar -pchar = unreserved / pct-encoded / sub-delims / ":" / "@" -unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" -pct-encoded = "%" HEXDIG HEXDIG -``` - -### RFC Evidence - -> "A URI is composed from a limited set of characters consisting of digits, letters, and a few graphic symbols." -> -- RFC 3986 Section 2 - -> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` -> `unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"` -> -- RFC 3986 Section 3.3 - -> "A recipient SHOULD NOT attempt to autocorrect and then process the request without a redirect, since the invalid request-line might be deliberately crafted to bypass security filters along the request chain." -> -- RFC 9112 Section 3 - -### Chain of Reasoning - -1. **The URI character set is strictly ASCII.** RFC 3986 Section 2 establishes that URIs are composed from a "limited set of characters." Every production rule -- `unreserved`, `reserved`, `sub-delims`, `gen-delims` -- references only characters in the ASCII range (`%x00-7F`). Non-ASCII octets have no place in a raw URI. - -2. **The bytes `0xC3 0xA9` are not valid `pchar`.** These are the UTF-8 encoding of U+00E9 (e with acute accent). Neither `0xC3` nor `0xA9` match `unreserved` (limited to ASCII letters, digits, and four symbols), `sub-delims`, `:`, `@`, or `pct-encoded` (which requires a leading `%`). They fail to match any alternative in the `pchar` production. - -3. **Percent-encoding is the correct representation.** The character `e` should appear as `%C3%A9` in the URI. RFC 3986 Section 2.1 specifies that "A percent-encoded octet is encoded as a character triplet, consisting of the percent character '%' followed by the two hexadecimal digits representing that octet's numeric value." - -4. **The request-line grammar is violated.** Since the `request-target` contains bytes that do not match the `origin-form` production, the entire `request-line` fails to match the grammar defined in RFC 9112 Section 3. - -5. **The server must not silently accept.** RFC 9112 Section 3 explicitly warns against autocorrection. A server that silently normalizes raw UTF-8 bytes into percent-encoded form and processes the request could be exploited by attackers crafting invalid URIs to bypass security filters in intermediaries. - -## Sources - -- [RFC 3986 Section 2](https://www.rfc-editor.org/rfc/rfc3986#section-2) — URI character set -- [RFC 3986 Section 2.1](https://www.rfc-editor.org/rfc/rfc3986#section-2.1) — percent-encoding -- [RFC 3986 Section 3.3](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) — path and pchar ABNF +--- +title: "Non Ascii URL — Malformed Input" +description: "A request with non-ASCII bytes in the URL. Part of Http11Probe's malformed input robustness suite." +weight: 10 +--- + +| | | +|---|---| +| **Test ID** | `MAL-NON-ASCII-URL` | +| **Category** | Malformed Input | +| **Expected** | `400` or close | + +## What it sends + +A request with non-ASCII bytes in the URL. + +```http +GET /caf\xC3\xA9 HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +The URL `/café` contains UTF-8 encoded `é` (`\xC3\xA9`) — non-ASCII bytes in the request-target. + + +## What the RFC says + +> "A URI is composed from a limited set of characters consisting of digits, letters, and a few graphic symbols." — RFC 3986 Section 2 + +The path component of a URI is constrained to `pchar`: + +> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` — RFC 3986 Section 3.3 + +> `unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"` — RFC 3986 Section 2.3 + +All allowed characters are ASCII. Raw bytes `0xC3 0xA9` (UTF-8 for `e` with acute accent) are not valid URI characters -- they must be percent-encoded as `%C3%A9`. + +> "A percent-encoded octet is encoded as a character triplet, consisting of the percent character '%' followed by the two hexadecimal digits representing that octet's numeric value." — RFC 3986 Section 2.1 + +## Deep Analysis + +### Relevant ABNF + +``` +request-target = origin-form / absolute-form / authority-form / asterisk-form +origin-form = absolute-path [ "?" query ] +absolute-path = 1*( "/" segment ) +segment = *pchar +pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +pct-encoded = "%" HEXDIG HEXDIG +``` + +### RFC Evidence + +> "A URI is composed from a limited set of characters consisting of digits, letters, and a few graphic symbols." +> -- RFC 3986 Section 2 + +> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` +> `unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"` +> -- RFC 3986 Section 3.3 + +> "A recipient SHOULD NOT attempt to autocorrect and then process the request without a redirect, since the invalid request-line might be deliberately crafted to bypass security filters along the request chain." +> -- RFC 9112 Section 3 + +### Chain of Reasoning + +1. **The URI character set is strictly ASCII.** RFC 3986 Section 2 establishes that URIs are composed from a "limited set of characters." Every production rule -- `unreserved`, `reserved`, `sub-delims`, `gen-delims` -- references only characters in the ASCII range (`%x00-7F`). Non-ASCII octets have no place in a raw URI. + +2. **The bytes `0xC3 0xA9` are not valid `pchar`.** These are the UTF-8 encoding of U+00E9 (e with acute accent). Neither `0xC3` nor `0xA9` match `unreserved` (limited to ASCII letters, digits, and four symbols), `sub-delims`, `:`, `@`, or `pct-encoded` (which requires a leading `%`). They fail to match any alternative in the `pchar` production. + +3. **Percent-encoding is the correct representation.** The character `e` should appear as `%C3%A9` in the URI. RFC 3986 Section 2.1 specifies that "A percent-encoded octet is encoded as a character triplet, consisting of the percent character '%' followed by the two hexadecimal digits representing that octet's numeric value." + +4. **The request-line grammar is violated.** Since the `request-target` contains bytes that do not match the `origin-form` production, the entire `request-line` fails to match the grammar defined in RFC 9112 Section 3. + +5. **The server must not silently accept.** RFC 9112 Section 3 explicitly warns against autocorrection. A server that silently normalizes raw UTF-8 bytes into percent-encoded form and processes the request could be exploited by attackers crafting invalid URIs to bypass security filters in intermediaries. + +## Sources + +- [RFC 3986 Section 2](https://www.rfc-editor.org/rfc/rfc3986#section-2) — URI character set +- [RFC 3986 Section 2.1](https://www.rfc-editor.org/rfc/rfc3986#section-2.1) — percent-encoding +- [RFC 3986 Section 3.3](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) — path and pchar ABNF diff --git a/docs/content/docs/malformed-input/nul-in-header-value.md b/docs/content/docs/malformed-input/nul-in-header-value.md index 07e2a53..5342517 100644 --- a/docs/content/docs/malformed-input/nul-in-header-value.md +++ b/docs/content/docs/malformed-input/nul-in-header-value.md @@ -1,81 +1,81 @@ ---- -title: "NUL-IN-HEADER-VALUE" -description: "NUL-IN-HEADER-VALUE test documentation" -weight: 15 ---- - -| | | -|---|---| -| **Test ID** | `MAL-NUL-IN-HEADER-VALUE` | -| **Category** | Malformed Input | -| **Expected** | `400` or close | - -## What it sends - -A request with a NUL byte (0x00) embedded in a header value. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -X-Test: val\x00ue\r\n -\r\n -``` - -The header value contains a NUL byte (`\x00`) between `val` and `ue`. - - -## What the RFC says - -> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters; a recipient of CR, LF, or NUL within a field value MUST either reject the message or replace each of those characters with SP before further processing or forwarding of that message." — RFC 9110 Section 5.5 - -The field-value ABNF grammar also confirms NUL is excluded: - -> `field-value = *field-content` -> `field-content = field-vchar [ 1*( SP / HTAB / field-vchar ) field-vchar ]` -> `field-vchar = VCHAR / obs-text` — RFC 9110 Section 5.5 - -`VCHAR` is `%x21-7E` (printable ASCII) and `obs-text` is `%x80-FF`. NUL (`0x00`) falls outside both ranges. - -## Why it matters - -NUL bytes are not valid in HTTP header field values. They can cause string truncation in C-based parsers, potentially hiding or injecting header content. A robust server must reject any request containing NUL bytes in headers. - -## Deep Analysis - -### Relevant ABNF - -``` -field-line = field-name ":" OWS field-value OWS -field-value = *field-content -field-content = field-vchar [ 1*( SP / HTAB / field-vchar ) field-vchar ] -field-vchar = VCHAR / obs-text -VCHAR = %x21-7E ; visible (printing) characters -obs-text = %x80-FF ; obsolete text (non-ASCII bytes) -``` - -### RFC Evidence - -> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters; a recipient of CR, LF, or NUL within a field value MUST either reject the message or replace each of those characters with SP before further processing or forwarding of that message." -> -- RFC 9110 Section 5.5 - -> `field-vchar = VCHAR / obs-text` -> -- RFC 9110 Section 5.5 - -> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -> -- RFC 9112 Section 2.2 - -### Chain of Reasoning - -1. **NUL (`0x00`) falls outside all valid field-value ranges.** The `field-vchar` production covers `VCHAR` (`%x21-7E`) and `obs-text` (`%x80-FF`). The only other characters allowed within `field-content` are `SP` (`%x20`) and `HTAB` (`%x09`). NUL (`%x00`) is below all of these ranges and matches no alternative in the grammar. - -2. **The RFC uses MUST-level language.** RFC 9110 Section 5.5 states that a recipient "MUST either reject the message or replace each of those characters with SP." This is not a SHOULD -- it is an absolute requirement. There is no third option of silently accepting the NUL. - -3. **Rejection is the safer choice.** While the RFC permits replacement with SP as an alternative to rejection, replacement changes the semantics of the header value. For a server processing incoming requests, rejecting with 400 is the more conservative and secure response, as it avoids the risk of processing altered header values whose original intent was to exploit parser inconsistencies. - -4. **NUL is explicitly called out as "dangerous."** The RFC singles out CR, LF, and NUL by name as "invalid and dangerous" due to "the varying ways that implementations might parse and interpret those characters." This language reflects real-world attacks where NUL bytes cause C-based string functions to truncate values, potentially hiding malicious content after the NUL. - -5. **The grammar violation is independent of the semantic rule.** Even without the explicit MUST in Section 5.5, the NUL byte fails the `field-vchar` grammar, making the entire `field-line` syntactically invalid under the HTTP-message grammar. - -## Sources - -- [RFC 9110 Section 5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) — field values and prohibited characters +--- +title: "NUL In Header Value — Malformed Input" +description: "A request with a NUL byte (0x00) embedded in a header value. Part of Http11Probe's malformed input robustness suite." +weight: 15 +--- + +| | | +|---|---| +| **Test ID** | `MAL-NUL-IN-HEADER-VALUE` | +| **Category** | Malformed Input | +| **Expected** | `400` or close | + +## What it sends + +A request with a NUL byte (0x00) embedded in a header value. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +X-Test: val\x00ue\r\n +\r\n +``` + +The header value contains a NUL byte (`\x00`) between `val` and `ue`. + + +## What the RFC says + +> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters; a recipient of CR, LF, or NUL within a field value MUST either reject the message or replace each of those characters with SP before further processing or forwarding of that message." — RFC 9110 Section 5.5 + +The field-value ABNF grammar also confirms NUL is excluded: + +> `field-value = *field-content` +> `field-content = field-vchar [ 1*( SP / HTAB / field-vchar ) field-vchar ]` +> `field-vchar = VCHAR / obs-text` — RFC 9110 Section 5.5 + +`VCHAR` is `%x21-7E` (printable ASCII) and `obs-text` is `%x80-FF`. NUL (`0x00`) falls outside both ranges. + +## Why it matters + +NUL bytes are not valid in HTTP header field values. They can cause string truncation in C-based parsers, potentially hiding or injecting header content. A robust server must reject any request containing NUL bytes in headers. + +## Deep Analysis + +### Relevant ABNF + +``` +field-line = field-name ":" OWS field-value OWS +field-value = *field-content +field-content = field-vchar [ 1*( SP / HTAB / field-vchar ) field-vchar ] +field-vchar = VCHAR / obs-text +VCHAR = %x21-7E ; visible (printing) characters +obs-text = %x80-FF ; obsolete text (non-ASCII bytes) +``` + +### RFC Evidence + +> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters; a recipient of CR, LF, or NUL within a field value MUST either reject the message or replace each of those characters with SP before further processing or forwarding of that message." +> -- RFC 9110 Section 5.5 + +> `field-vchar = VCHAR / obs-text` +> -- RFC 9110 Section 5.5 + +> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." +> -- RFC 9112 Section 2.2 + +### Chain of Reasoning + +1. **NUL (`0x00`) falls outside all valid field-value ranges.** The `field-vchar` production covers `VCHAR` (`%x21-7E`) and `obs-text` (`%x80-FF`). The only other characters allowed within `field-content` are `SP` (`%x20`) and `HTAB` (`%x09`). NUL (`%x00`) is below all of these ranges and matches no alternative in the grammar. + +2. **The RFC uses MUST-level language.** RFC 9110 Section 5.5 states that a recipient "MUST either reject the message or replace each of those characters with SP." This is not a SHOULD -- it is an absolute requirement. There is no third option of silently accepting the NUL. + +3. **Rejection is the safer choice.** While the RFC permits replacement with SP as an alternative to rejection, replacement changes the semantics of the header value. For a server processing incoming requests, rejecting with 400 is the more conservative and secure response, as it avoids the risk of processing altered header values whose original intent was to exploit parser inconsistencies. + +4. **NUL is explicitly called out as "dangerous."** The RFC singles out CR, LF, and NUL by name as "invalid and dangerous" due to "the varying ways that implementations might parse and interpret those characters." This language reflects real-world attacks where NUL bytes cause C-based string functions to truncate values, potentially hiding malicious content after the NUL. + +5. **The grammar violation is independent of the semantic rule.** Even without the explicit MUST in Section 5.5, the NUL byte fails the `field-vchar` grammar, making the entire `field-line` syntactically invalid under the HTTP-message grammar. + +## Sources + +- [RFC 9110 Section 5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) — field values and prohibited characters diff --git a/docs/content/docs/malformed-input/nul-in-url.md b/docs/content/docs/malformed-input/nul-in-url.md index 9743677..5f7c7e5 100644 --- a/docs/content/docs/malformed-input/nul-in-url.md +++ b/docs/content/docs/malformed-input/nul-in-url.md @@ -1,84 +1,84 @@ ---- -title: "NUL-IN-URL" -description: "NUL-IN-URL test documentation" -weight: 7 ---- - -| | | -|---|---| -| **Test ID** | `MAL-NUL-IN-URL` | -| **Category** | Malformed Input | -| **Expected** | `400` or close | - -## What it sends - -A request with a NUL byte (`\x00`) embedded in the URL. - -```http -GET /\x00test HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -The URL contains a NUL byte (`\x00`) between `/` and `test`. - - -## What the RFC says - -The request-target in an HTTP/1.1 request must conform to the URI grammar from RFC 3986. The `pchar` rule defines valid path characters: - -> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` — RFC 3986 Section 3.3 - -> `unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"` — RFC 3986 Section 2.3 - -A raw NUL byte (`0x00`) is not included in any of these productions, making it an invalid character in a URI path. - -> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar...the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 - -## Why it matters - -NUL bytes terminate strings in C/C++. A NUL in the URL could cause path truncation in backend systems, allowing path traversal or access to unintended resources. - -## Deep Analysis - -### Relevant ABNF - -``` -request-line = method SP request-target SP HTTP-version -origin-form = absolute-path [ "?" query ] -absolute-path = 1*( "/" segment ) -segment = *pchar -pchar = unreserved / pct-encoded / sub-delims / ":" / "@" -unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" -pct-encoded = "%" HEXDIG HEXDIG -sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - / "*" / "+" / "," / ";" / "=" -``` - -### RFC Evidence - -> "A URI is composed from a limited set of characters consisting of digits, letters, and a few graphic symbols." -> -- RFC 3986 Section 2 - -> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` -> -- RFC 3986 Section 3.3 - -> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -> -- RFC 9112 Section 2.2 - -### Chain of Reasoning - -1. **NUL (`0x00`) matches no URI production.** Walking through every alternative in `pchar`: `unreserved` covers only `ALPHA` (`%x41-5A`, `%x61-7A`), `DIGIT` (`%x30-39`), and four specific symbols; `pct-encoded` requires a leading `%`; `sub-delims` lists specific ASCII punctuation; `:` is `%x3A`; `@` is `%x40`. The byte `0x00` is below the lowest value in any of these sets. - -2. **NUL is not even a valid request-line octet.** The `request-line` grammar requires `method SP request-target SP HTTP-version`. The `SP` character is `%x20`. The `method` is a `token` (requires `tchar`, minimum `%x21`). There is no production in the HTTP/1.1 grammar that accommodates `%x00` anywhere in the request-line. - -3. **The grammar violation triggers the rejection rule.** Since the `request-target` contains an octet that does not match the URI grammar, the entire request-line fails to match `HTTP-message`. RFC 9112 Section 2.2 instructs the server to respond with 400 and close. - -4. **C-string truncation is the primary exploit vector.** In C and C++, strings are NUL-terminated. A NUL byte at position 1 in `/\x00test` would cause `strlen()` to return 1, making the path appear as just `/`. This can bypass path-based access controls, allow directory listing where only specific files should be served, or truncate filenames to access unintended resources. - -5. **No robustness exception applies.** The only robustness exception in RFC 9112 Section 2.2 is ignoring empty CRLF lines before the request-line. NUL bytes receive no such exception. - -## Sources - -- [RFC 3986 Section 3.3](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) — URI path and pchar grammar -- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — rejection of invalid messages +--- +title: "NUL In URL — Malformed Input" +description: "A request with a NUL byte (\\x00) embedded in the URL. Part of Http11Probe's malformed input robustness suite." +weight: 7 +--- + +| | | +|---|---| +| **Test ID** | `MAL-NUL-IN-URL` | +| **Category** | Malformed Input | +| **Expected** | `400` or close | + +## What it sends + +A request with a NUL byte (`\x00`) embedded in the URL. + +```http +GET /\x00test HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +The URL contains a NUL byte (`\x00`) between `/` and `test`. + + +## What the RFC says + +The request-target in an HTTP/1.1 request must conform to the URI grammar from RFC 3986. The `pchar` rule defines valid path characters: + +> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` — RFC 3986 Section 3.3 + +> `unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"` — RFC 3986 Section 2.3 + +A raw NUL byte (`0x00`) is not included in any of these productions, making it an invalid character in a URI path. + +> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar...the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 + +## Why it matters + +NUL bytes terminate strings in C/C++. A NUL in the URL could cause path truncation in backend systems, allowing path traversal or access to unintended resources. + +## Deep Analysis + +### Relevant ABNF + +``` +request-line = method SP request-target SP HTTP-version +origin-form = absolute-path [ "?" query ] +absolute-path = 1*( "/" segment ) +segment = *pchar +pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +pct-encoded = "%" HEXDIG HEXDIG +sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + / "*" / "+" / "," / ";" / "=" +``` + +### RFC Evidence + +> "A URI is composed from a limited set of characters consisting of digits, letters, and a few graphic symbols." +> -- RFC 3986 Section 2 + +> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` +> -- RFC 3986 Section 3.3 + +> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." +> -- RFC 9112 Section 2.2 + +### Chain of Reasoning + +1. **NUL (`0x00`) matches no URI production.** Walking through every alternative in `pchar`: `unreserved` covers only `ALPHA` (`%x41-5A`, `%x61-7A`), `DIGIT` (`%x30-39`), and four specific symbols; `pct-encoded` requires a leading `%`; `sub-delims` lists specific ASCII punctuation; `:` is `%x3A`; `@` is `%x40`. The byte `0x00` is below the lowest value in any of these sets. + +2. **NUL is not even a valid request-line octet.** The `request-line` grammar requires `method SP request-target SP HTTP-version`. The `SP` character is `%x20`. The `method` is a `token` (requires `tchar`, minimum `%x21`). There is no production in the HTTP/1.1 grammar that accommodates `%x00` anywhere in the request-line. + +3. **The grammar violation triggers the rejection rule.** Since the `request-target` contains an octet that does not match the URI grammar, the entire request-line fails to match `HTTP-message`. RFC 9112 Section 2.2 instructs the server to respond with 400 and close. + +4. **C-string truncation is the primary exploit vector.** In C and C++, strings are NUL-terminated. A NUL byte at position 1 in `/\x00test` would cause `strlen()` to return 1, making the path appear as just `/`. This can bypass path-based access controls, allow directory listing where only specific files should be served, or truncate filenames to access unintended resources. + +5. **No robustness exception applies.** The only robustness exception in RFC 9112 Section 2.2 is ignoring empty CRLF lines before the request-line. NUL bytes receive no such exception. + +## Sources + +- [RFC 3986 Section 3.3](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) — URI path and pchar grammar +- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — rejection of invalid messages diff --git a/docs/content/docs/malformed-input/post-cl-huge-no-body.md b/docs/content/docs/malformed-input/post-cl-huge-no-body.md index 8f49de1..132f09e 100644 --- a/docs/content/docs/malformed-input/post-cl-huge-no-body.md +++ b/docs/content/docs/malformed-input/post-cl-huge-no-body.md @@ -1,82 +1,82 @@ ---- -title: "POST-CL-HUGE-NO-BODY" -description: "POST-CL-HUGE-NO-BODY test documentation" -weight: 26 ---- - -| | | -|---|---| -| **Test ID** | `MAL-POST-CL-HUGE-NO-BODY` | -| **Category** | Malformed Input | -| **RFC** | [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) | -| **Expected** | `400`/`413`/close/timeout | - -## What it sends - -A POST request declaring a ~1GB body via Content-Length but sending no body data at all. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 999999999\r\n -\r\n -``` - -No body follows the empty line. The connection remains open. - -## What the RFC says - -> `Content-Length = 1*DIGIT` — RFC 9110 Section 8.6 - -> "When a message does not have a Transfer-Encoding header field, a Content-Length header field can provide the anticipated size, as a decimal number of octets, for potential content." — RFC 9112 Section 6.2 - -The value `999999999` (~1GB) is a syntactically valid Content-Length, but no body data follows. The server must determine the message body length from Content-Length and wait for that many bytes: - -> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error." — RFC 9110 Section 15.5.1 - -A server may reject the request with 400 or 413 if the declared body size exceeds its limits, close the connection, or timeout waiting for body data that never arrives. - -## Why it matters - -Tests whether the server pre-allocates memory for the declared body size or waits for data to arrive. A server that allocates 1GB upfront from a Content-Length header is vulnerable to memory exhaustion DoS -- an attacker can send many such requests cheaply to exhaust server memory. The correct behavior is to either stream the body incrementally, reject absurdly large Content-Length values, or timeout waiting for the body data that never arrives. - -## Deep Analysis - -### Relevant ABNF - -``` -Content-Length = 1*DIGIT -message-body = *OCTET -``` - -### RFC Evidence - -> "When a valid Content-Length header field is present without Transfer-Encoding, its decimal value defines the expected message body length in octets." -> -- RFC 9112 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." -> -- RFC 9112 Section 6.2 - -> "A server MAY reject a request that contains a message body but not a Content-Length by responding with 411 (Length Required)." -> -- RFC 9112 Section 6.2 - -### Chain of Reasoning - -1. **The request is syntactically valid.** `Content-Length: 999999999` conforms to the `1*DIGIT` production. The headers are well-formed. The request-line `POST / HTTP/1.1` is valid. There is no grammar violation in the request itself. - -2. **The server expects ~1 GB of body data.** The `Content-Length` value of 999,999,999 declares that approximately 953 MB of body data should follow the empty line. Per RFC 9112 Section 6.2, this value "defines the expected message body length in octets." - -3. **No body data arrives.** The connection remains open but no octets are sent after the CRLF CRLF that terminates the header section. The server is left waiting for data that will never come. - -4. **The timeout/incompleteness rule applies.** RFC 9112 Section 6.2 states that if "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." This MUST-level requirement ensures the server eventually reclaims resources. - -5. **Pre-allocation is the vulnerability.** A naive server implementation that allocates a buffer of `Content-Length` bytes upon receiving the headers would allocate ~1 GB for a single request. An attacker sending many such requests with no body can exhaust server memory with minimal bandwidth. This maps directly to [CWE-770](https://cwe.mitre.org/data/definitions/770.html) (Allocation of Resources Without Limits or Throttling). - -6. **The correct defensive behavior is one of three options**: (a) reject the request immediately with 400 because the Content-Length exceeds a configured maximum; (b) stream the body incrementally without pre-allocation, eventually timing out; or (c) respond with 413 (Content Too Large) indicating the declared body exceeds server limits. - -## Sources - -- [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) — Content-Length grammar -- [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) — Content-Length body framing -- [RFC 9110 Section 15.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.1) — 400 Bad Request -- [CWE-770](https://cwe.mitre.org/data/definitions/770.html) — Allocation of Resources Without Limits +--- +title: "POST CL Huge No Body — Malformed Input" +description: "A POST request declaring a ~1GB body via Content-Length but sending no body data at all. Tested against RFC 9112 Section 6.2." +weight: 26 +--- + +| | | +|---|---| +| **Test ID** | `MAL-POST-CL-HUGE-NO-BODY` | +| **Category** | Malformed Input | +| **RFC** | [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) | +| **Expected** | `400`/`413`/close/timeout | + +## What it sends + +A POST request declaring a ~1GB body via Content-Length but sending no body data at all. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 999999999\r\n +\r\n +``` + +No body follows the empty line. The connection remains open. + +## What the RFC says + +> `Content-Length = 1*DIGIT` — RFC 9110 Section 8.6 + +> "When a message does not have a Transfer-Encoding header field, a Content-Length header field can provide the anticipated size, as a decimal number of octets, for potential content." — RFC 9112 Section 6.2 + +The value `999999999` (~1GB) is a syntactically valid Content-Length, but no body data follows. The server must determine the message body length from Content-Length and wait for that many bytes: + +> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error." — RFC 9110 Section 15.5.1 + +A server may reject the request with 400 or 413 if the declared body size exceeds its limits, close the connection, or timeout waiting for body data that never arrives. + +## Why it matters + +Tests whether the server pre-allocates memory for the declared body size or waits for data to arrive. A server that allocates 1GB upfront from a Content-Length header is vulnerable to memory exhaustion DoS -- an attacker can send many such requests cheaply to exhaust server memory. The correct behavior is to either stream the body incrementally, reject absurdly large Content-Length values, or timeout waiting for the body data that never arrives. + +## Deep Analysis + +### Relevant ABNF + +``` +Content-Length = 1*DIGIT +message-body = *OCTET +``` + +### RFC Evidence + +> "When a valid Content-Length header field is present without Transfer-Encoding, its decimal value defines the expected message body length in octets." +> -- RFC 9112 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." +> -- RFC 9112 Section 6.2 + +> "A server MAY reject a request that contains a message body but not a Content-Length by responding with 411 (Length Required)." +> -- RFC 9112 Section 6.2 + +### Chain of Reasoning + +1. **The request is syntactically valid.** `Content-Length: 999999999` conforms to the `1*DIGIT` production. The headers are well-formed. The request-line `POST / HTTP/1.1` is valid. There is no grammar violation in the request itself. + +2. **The server expects ~1 GB of body data.** The `Content-Length` value of 999,999,999 declares that approximately 953 MB of body data should follow the empty line. Per RFC 9112 Section 6.2, this value "defines the expected message body length in octets." + +3. **No body data arrives.** The connection remains open but no octets are sent after the CRLF CRLF that terminates the header section. The server is left waiting for data that will never come. + +4. **The timeout/incompleteness rule applies.** RFC 9112 Section 6.2 states that if "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." This MUST-level requirement ensures the server eventually reclaims resources. + +5. **Pre-allocation is the vulnerability.** A naive server implementation that allocates a buffer of `Content-Length` bytes upon receiving the headers would allocate ~1 GB for a single request. An attacker sending many such requests with no body can exhaust server memory with minimal bandwidth. This maps directly to [CWE-770](https://cwe.mitre.org/data/definitions/770.html) (Allocation of Resources Without Limits or Throttling). + +6. **The correct defensive behavior is one of three options**: (a) reject the request immediately with 400 because the Content-Length exceeds a configured maximum; (b) stream the body incrementally without pre-allocation, eventually timing out; or (c) respond with 413 (Content Too Large) indicating the declared body exceeds server limits. + +## Sources + +- [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) — Content-Length grammar +- [RFC 9112 Section 6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) — Content-Length body framing +- [RFC 9110 Section 15.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.1) — 400 Bad Request +- [CWE-770](https://cwe.mitre.org/data/definitions/770.html) — Allocation of Resources Without Limits diff --git a/docs/content/docs/malformed-input/range-overlapping.md b/docs/content/docs/malformed-input/range-overlapping.md index 518900e..ded2221 100644 --- a/docs/content/docs/malformed-input/range-overlapping.md +++ b/docs/content/docs/malformed-input/range-overlapping.md @@ -1,85 +1,85 @@ ---- -title: "RANGE-OVERLAPPING" -description: "RANGE-OVERLAPPING test documentation" -weight: 25 ---- - -| | | -|---|---| -| **Test ID** | `MAL-RANGE-OVERLAPPING` (unscored) | -| **Category** | Malformed Input | -| **RFC** | [RFC 9110 Section 14.2](https://www.rfc-editor.org/rfc/rfc9110#section-14.2) | -| **Expected** | Any response = Warn | - -## What it sends - -A GET request with a Range header containing 1,000 overlapping range values. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Range: bytes=0-,0-,0-,...{1000 total}...\r\n -\r\n -``` - -The `Range` header contains 1,000 repetitions of `0-`, each requesting the entire resource. - -## What the RFC says - -RFC 9110 Section 14.2 defines the Range header. The RFC does not prohibit overlapping or duplicated range values. Servers have broad discretion in how they respond: - -> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error." — RFC 9110 Section 15.5.1 - -A server may respond with 200 (ignoring the Range header), 206 (Partial Content), 416 (Range Not Satisfiable), or 400 (rejecting the request). All are valid approaches. - -## Why this test is unscored - -The RFC does not forbid overlapping ranges. A server may legitimately respond with `200` (ignoring the Range header), `206` (honoring the ranges), `400` (rejecting as abusive), or `416` (Range Not Satisfiable). Because all of these responses are defensible, no single outcome can be graded as correct, so this test is reported as Warn for any response. - -## Why it matters - -CVE-2011-3192 (Apache Range header DoS) showed that servers that expand each range independently can consume massive memory. A single request with 1,000 overlapping ranges could cause the server to generate a multipart response containing thousands of copies of the same content, exhausting memory and CPU. A robust server should either ignore the Range header, merge overlapping ranges, or reject the request. - -## Deep Analysis - -### Relevant ABNF - -``` -Range = ranges-specifier -ranges-specifier = range-unit "=" range-set -range-set = 1#range-spec -range-spec = int-range / suffix-range / other-range -int-range = first-pos "-" [ last-pos ] -byte-ranges-specifier = bytes-unit "=" byte-range-set -byte-range-set = 1#( byte-range-spec / suffix-byte-range-spec ) -byte-range-spec = first-byte-pos "-" [ last-byte-pos ] -``` - -### RFC Evidence - -> "A server that supports range requests MAY ignore or reject a Range header field that contains an invalid ranges-specifier, a ranges-specifier with more than two overlapping ranges, or a set of many small ranges that are not listed in ascending order, since these are indications of either a broken client or a deliberate denial-of-service attack." -> -- RFC 9110 Section 14.2 - -> "The 'Range' header field on a GET request modifies the method semantics to request transfer of only one or more subranges of the selected representation data, rather than the entire selected representation." -> -- RFC 9110 Section 14.2 - -> "A recipient SHOULD parse a received protocol element defensively, with only marginal expectations that the element will conform to its ABNF grammar and fit within a reasonable buffer size." -> -- RFC 9110 Section 2.3 - -### Chain of Reasoning - -1. **The request is syntactically valid.** Each `0-` range spec conforms to `byte-range-spec = first-byte-pos "-" [ last-byte-pos ]` where `first-byte-pos` is `0` and `last-byte-pos` is omitted (meaning "to the end"). The comma-separated list satisfies the `1#` (one or more) list rule. - -2. **This test is unscored because the RFC explicitly permits ignoring Range.** RFC 9110 Section 14.2 states servers "MAY ignore or reject" a Range header that indicates "a deliberate denial-of-service attack." Since the RFC uses MAY-level language, there is no single correct behavior: a server may ignore the Range header and serve a normal 200 response, merge the overlapping ranges into a single range, reject the request with 400 or 416, or close the connection. All behaviors are RFC-compliant, which is why this test cannot be objectively scored as Pass or Fail. - -3. **1,000 identical `0-` ranges is a clear DoS indicator.** The RFC specifically calls out "a set of many small ranges" as an indication of "a deliberate denial-of-service attack." While the test uses overlapping full-resource ranges rather than small ranges, the principle is the same: the number of ranges is unreasonable. - -4. **CVE-2011-3192 demonstrated the real-world impact.** The Apache HTTPD "Range header DoS" vulnerability (also known as the "Apache Killer") allowed a single request with many overlapping byte ranges to cause Apache to generate a multipart response containing thousands of copies of the resource content, exhausting memory and CPU. This CVE directly motivates this test case. - -5. **Any response earns Warn.** Because the test is unscored, the probe records a Warn regardless of the server's response. The purpose is informational: to observe how the server handles this known attack pattern, not to enforce a specific behavior. - -## Sources - -- [RFC 9110 Section 14.2](https://www.rfc-editor.org/rfc/rfc9110#section-14.2) — Range header field -- [RFC 9110 Section 15.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.1) — 400 Bad Request -- [CVE-2011-3192](https://nvd.nist.gov/vuln/detail/CVE-2011-3192) — Apache Range header DoS +--- +title: "Range Overlapping — Malformed Input" +description: "A GET request with a Range header containing 1,000 overlapping range values. Tested against RFC 9110 Section 14.2." +weight: 25 +--- + +| | | +|---|---| +| **Test ID** | `MAL-RANGE-OVERLAPPING` (unscored) | +| **Category** | Malformed Input | +| **RFC** | [RFC 9110 Section 14.2](https://www.rfc-editor.org/rfc/rfc9110#section-14.2) | +| **Expected** | Any response = Warn | + +## What it sends + +A GET request with a Range header containing 1,000 overlapping range values. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Range: bytes=0-,0-,0-,...{1000 total}...\r\n +\r\n +``` + +The `Range` header contains 1,000 repetitions of `0-`, each requesting the entire resource. + +## What the RFC says + +RFC 9110 Section 14.2 defines the Range header. The RFC does not prohibit overlapping or duplicated range values. Servers have broad discretion in how they respond: + +> "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error." — RFC 9110 Section 15.5.1 + +A server may respond with 200 (ignoring the Range header), 206 (Partial Content), 416 (Range Not Satisfiable), or 400 (rejecting the request). All are valid approaches. + +## Why this test is unscored + +The RFC does not forbid overlapping ranges. A server may legitimately respond with `200` (ignoring the Range header), `206` (honoring the ranges), `400` (rejecting as abusive), or `416` (Range Not Satisfiable). Because all of these responses are defensible, no single outcome can be graded as correct, so this test is reported as Warn for any response. + +## Why it matters + +CVE-2011-3192 (Apache Range header DoS) showed that servers that expand each range independently can consume massive memory. A single request with 1,000 overlapping ranges could cause the server to generate a multipart response containing thousands of copies of the same content, exhausting memory and CPU. A robust server should either ignore the Range header, merge overlapping ranges, or reject the request. + +## Deep Analysis + +### Relevant ABNF + +``` +Range = ranges-specifier +ranges-specifier = range-unit "=" range-set +range-set = 1#range-spec +range-spec = int-range / suffix-range / other-range +int-range = first-pos "-" [ last-pos ] +byte-ranges-specifier = bytes-unit "=" byte-range-set +byte-range-set = 1#( byte-range-spec / suffix-byte-range-spec ) +byte-range-spec = first-byte-pos "-" [ last-byte-pos ] +``` + +### RFC Evidence + +> "A server that supports range requests MAY ignore or reject a Range header field that contains an invalid ranges-specifier, a ranges-specifier with more than two overlapping ranges, or a set of many small ranges that are not listed in ascending order, since these are indications of either a broken client or a deliberate denial-of-service attack." +> -- RFC 9110 Section 14.2 + +> "The 'Range' header field on a GET request modifies the method semantics to request transfer of only one or more subranges of the selected representation data, rather than the entire selected representation." +> -- RFC 9110 Section 14.2 + +> "A recipient SHOULD parse a received protocol element defensively, with only marginal expectations that the element will conform to its ABNF grammar and fit within a reasonable buffer size." +> -- RFC 9110 Section 2.3 + +### Chain of Reasoning + +1. **The request is syntactically valid.** Each `0-` range spec conforms to `byte-range-spec = first-byte-pos "-" [ last-byte-pos ]` where `first-byte-pos` is `0` and `last-byte-pos` is omitted (meaning "to the end"). The comma-separated list satisfies the `1#` (one or more) list rule. + +2. **This test is unscored because the RFC explicitly permits ignoring Range.** RFC 9110 Section 14.2 states servers "MAY ignore or reject" a Range header that indicates "a deliberate denial-of-service attack." Since the RFC uses MAY-level language, there is no single correct behavior: a server may ignore the Range header and serve a normal 200 response, merge the overlapping ranges into a single range, reject the request with 400 or 416, or close the connection. All behaviors are RFC-compliant, which is why this test cannot be objectively scored as Pass or Fail. + +3. **1,000 identical `0-` ranges is a clear DoS indicator.** The RFC specifically calls out "a set of many small ranges" as an indication of "a deliberate denial-of-service attack." While the test uses overlapping full-resource ranges rather than small ranges, the principle is the same: the number of ranges is unreasonable. + +4. **CVE-2011-3192 demonstrated the real-world impact.** The Apache HTTPD "Range header DoS" vulnerability (also known as the "Apache Killer") allowed a single request with many overlapping byte ranges to cause Apache to generate a multipart response containing thousands of copies of the resource content, exhausting memory and CPU. This CVE directly motivates this test case. + +5. **Any response earns Warn.** Because the test is unscored, the probe records a Warn regardless of the server's response. The purpose is informational: to observe how the server handles this known attack pattern, not to enforce a specific behavior. + +## Sources + +- [RFC 9110 Section 14.2](https://www.rfc-editor.org/rfc/rfc9110#section-14.2) — Range header field +- [RFC 9110 Section 15.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.1) — 400 Bad Request +- [CVE-2011-3192](https://nvd.nist.gov/vuln/detail/CVE-2011-3192) — Apache Range header DoS diff --git a/docs/content/docs/malformed-input/url-backslash.md b/docs/content/docs/malformed-input/url-backslash.md index b4ddf2c..2b618a3 100644 --- a/docs/content/docs/malformed-input/url-backslash.md +++ b/docs/content/docs/malformed-input/url-backslash.md @@ -1,87 +1,87 @@ ---- -title: "URL-BACKSLASH" -description: "URL-BACKSLASH test documentation" -weight: 21 ---- - -| | | -|---|---| -| **Test ID** | `MAL-URL-BACKSLASH` | -| **Category** | Malformed Input | -| **Expected** | `400` = Pass, `2xx`/`404` = Warn | - -## What it sends - -A GET request with a backslash in the URL path. - -```http -GET /path\file HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -The valid characters in a URI path segment are defined by `pchar`: - -> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` — RFC 3986 Section 3.3 - -> `unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"` — RFC 3986 Section 2.3 - -> `sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="` — RFC 3986 Section 2.2 - -Backslash (`\`, `0x5C`) is not included in `unreserved`, `sub-delims`, `pct-encoded`, `":"`, or `"@"`. It is therefore not a valid URI path character. - -## Pass/Warn explanation - -- **Pass (400):** The server rejects the request because backslash is not a valid URI character. -- **Warn (2xx/404):** The server processed the request despite the invalid URI character. This may indicate the server normalizes `\` to `/`, which is a path traversal risk. - -## Why it matters - -Some servers (especially on Windows) normalize `\` to `/`, which can enable path traversal attacks if used to bypass URL filters. For example, a WAF blocking `../` might not block `..\`, allowing an attacker to traverse directories on servers that treat backslash as a path separator. - -## Deep Analysis - -### Relevant ABNF - -``` -request-line = method SP request-target SP HTTP-version -origin-form = absolute-path [ "?" query ] -absolute-path = 1*( "/" segment ) -segment = *pchar -pchar = unreserved / pct-encoded / sub-delims / ":" / "@" -unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" -sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - / "*" / "+" / "," / ";" / "=" -``` - -### RFC Evidence - -> "A URI is composed from a limited set of characters consisting of digits, letters, and a few graphic symbols." -> -- RFC 3986 Section 2 - -> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` -> `unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"` -> -- RFC 3986 Section 3.3 - -> "A recipient SHOULD NOT attempt to autocorrect and then process the request without a redirect, since the invalid request-line might be deliberately crafted to bypass security filters along the request chain." -> -- RFC 9112 Section 3 - -### Chain of Reasoning - -1. **Backslash (`\`, `0x5C`) is not a valid URI character.** Enumerating every alternative in `pchar`: `unreserved` allows only `ALPHA`, `DIGIT`, `-`, `.`, `_`, `~`; `pct-encoded` requires a leading `%`; `sub-delims` lists `!`, `$`, `&`, `'`, `(`, `)`, `*`, `+`, `,`, `;`, `=`; the remaining alternatives are `:` and `@`. Backslash appears in none of these. It is also absent from `gen-delims` (`:`, `/`, `?`, `#`, `[`, `]`, `@`) and `reserved`. - -2. **The request-target violates the URI grammar.** Since `\` at `0x5C` is not a `pchar`, the `segment` containing `path\file` fails to parse. The entire `origin-form` production fails, and therefore the `request-line` does not match the grammar. - -3. **Autocorrection is explicitly discouraged.** RFC 9112 Section 3 warns that a recipient "SHOULD NOT attempt to autocorrect and then process the request without a redirect, since the invalid request-line might be deliberately crafted to bypass security filters." A server that silently normalizes `\` to `/` is performing exactly the kind of autocorrection the RFC warns against. - -4. **Windows path separator confusion is the exploit vector.** On Windows systems, `\` and `/` are interchangeable path separators. A WAF or reverse proxy that blocks `../` in URLs will not match `..\` -- but if the backend server normalizes backslash to forward slash, the attacker achieves path traversal through the filter gap. - -5. **Warn for 2xx/404 reflects the ambiguity.** A server that returns 2xx or 404 has processed the request despite the invalid character. It may have handled the backslash safely (e.g., treated it as a literal filename character), but the acceptance of non-URI characters is still a concern because it indicates the parser is more permissive than the grammar allows. - -## Sources - -- [RFC 3986 Section 3.3](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) — path and pchar grammar -- [RFC 3986 Section 2.2](https://www.rfc-editor.org/rfc/rfc3986#section-2.2) — reserved characters -- [RFC 3986 Section 2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3) — unreserved characters +--- +title: "URL Backslash — Malformed Input" +description: "A GET request with a backslash in the URL path. Part of Http11Probe's malformed input robustness suite." +weight: 21 +--- + +| | | +|---|---| +| **Test ID** | `MAL-URL-BACKSLASH` | +| **Category** | Malformed Input | +| **Expected** | `400` = Pass, `2xx`/`404` = Warn | + +## What it sends + +A GET request with a backslash in the URL path. + +```http +GET /path\file HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +The valid characters in a URI path segment are defined by `pchar`: + +> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` — RFC 3986 Section 3.3 + +> `unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"` — RFC 3986 Section 2.3 + +> `sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="` — RFC 3986 Section 2.2 + +Backslash (`\`, `0x5C`) is not included in `unreserved`, `sub-delims`, `pct-encoded`, `":"`, or `"@"`. It is therefore not a valid URI path character. + +## Pass/Warn explanation + +- **Pass (400):** The server rejects the request because backslash is not a valid URI character. +- **Warn (2xx/404):** The server processed the request despite the invalid URI character. This may indicate the server normalizes `\` to `/`, which is a path traversal risk. + +## Why it matters + +Some servers (especially on Windows) normalize `\` to `/`, which can enable path traversal attacks if used to bypass URL filters. For example, a WAF blocking `../` might not block `..\`, allowing an attacker to traverse directories on servers that treat backslash as a path separator. + +## Deep Analysis + +### Relevant ABNF + +``` +request-line = method SP request-target SP HTTP-version +origin-form = absolute-path [ "?" query ] +absolute-path = 1*( "/" segment ) +segment = *pchar +pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + / "*" / "+" / "," / ";" / "=" +``` + +### RFC Evidence + +> "A URI is composed from a limited set of characters consisting of digits, letters, and a few graphic symbols." +> -- RFC 3986 Section 2 + +> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` +> `unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"` +> -- RFC 3986 Section 3.3 + +> "A recipient SHOULD NOT attempt to autocorrect and then process the request without a redirect, since the invalid request-line might be deliberately crafted to bypass security filters along the request chain." +> -- RFC 9112 Section 3 + +### Chain of Reasoning + +1. **Backslash (`\`, `0x5C`) is not a valid URI character.** Enumerating every alternative in `pchar`: `unreserved` allows only `ALPHA`, `DIGIT`, `-`, `.`, `_`, `~`; `pct-encoded` requires a leading `%`; `sub-delims` lists `!`, `$`, `&`, `'`, `(`, `)`, `*`, `+`, `,`, `;`, `=`; the remaining alternatives are `:` and `@`. Backslash appears in none of these. It is also absent from `gen-delims` (`:`, `/`, `?`, `#`, `[`, `]`, `@`) and `reserved`. + +2. **The request-target violates the URI grammar.** Since `\` at `0x5C` is not a `pchar`, the `segment` containing `path\file` fails to parse. The entire `origin-form` production fails, and therefore the `request-line` does not match the grammar. + +3. **Autocorrection is explicitly discouraged.** RFC 9112 Section 3 warns that a recipient "SHOULD NOT attempt to autocorrect and then process the request without a redirect, since the invalid request-line might be deliberately crafted to bypass security filters." A server that silently normalizes `\` to `/` is performing exactly the kind of autocorrection the RFC warns against. + +4. **Windows path separator confusion is the exploit vector.** On Windows systems, `\` and `/` are interchangeable path separators. A WAF or reverse proxy that blocks `../` in URLs will not match `..\` -- but if the backend server normalizes backslash to forward slash, the attacker achieves path traversal through the filter gap. + +5. **Warn for 2xx/404 reflects the ambiguity.** A server that returns 2xx or 404 has processed the request despite the invalid character. It may have handled the backslash safely (e.g., treated it as a literal filename character), but the acceptance of non-URI characters is still a concern because it indicates the parser is more permissive than the grammar allows. + +## Sources + +- [RFC 3986 Section 3.3](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) — path and pchar grammar +- [RFC 3986 Section 2.2](https://www.rfc-editor.org/rfc/rfc3986#section-2.2) — reserved characters +- [RFC 3986 Section 2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3) — unreserved characters diff --git a/docs/content/docs/malformed-input/url-overlong-utf8.md b/docs/content/docs/malformed-input/url-overlong-utf8.md index 1934433..c6a0c90 100644 --- a/docs/content/docs/malformed-input/url-overlong-utf8.md +++ b/docs/content/docs/malformed-input/url-overlong-utf8.md @@ -1,83 +1,83 @@ ---- -title: "URL-OVERLONG-UTF8" -description: "URL-OVERLONG-UTF8 test documentation" -weight: 22 ---- - -| | | -|---|---| -| **Test ID** | `MAL-URL-OVERLONG-UTF8` | -| **Category** | Malformed Input | -| **Expected** | `400` or close | - -## What it sends - -A GET request with raw overlong UTF-8 bytes in the URL path. The bytes `0xC0 0xAF` are an overlong encoding of `/` (U+002F). - -```http -GET /\xC0\xAF HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -The two bytes after `/` are `0xC0 0xAF` -- an illegal two-byte UTF-8 sequence that decodes to the ASCII forward slash character. - -## What the RFC says - -Raw bytes `0xC0` and `0xAF` are not valid URI characters. URI paths are limited to `pchar`: - -> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` — RFC 3986 Section 3.3 - -All `unreserved` and `sub-delims` characters are ASCII (`0x21-0x7E`). Bytes `0xC0` and `0xAF` fall outside this range and are not percent-encoded, so they violate the URI grammar. - -Additionally, RFC 3629 requires rejection of overlong encodings: - -> "Implementations of the decoding algorithm above MUST protect against decoding invalid sequences." — RFC 3629 Section 3 - -The bytes `0xC0 0xAF` are an overlong UTF-8 encoding of `U+002F` (forward slash `/`), which must be encoded as the single byte `0x2F`. - -## Why it matters - -Overlong UTF-8 sequences encode characters using more bytes than necessary. If a server decodes `0xC0 0xAF` as `/` during path resolution, it can bypass path traversal filters (e.g., `..%c0%af..` becomes `../../`). This was the basis of the infamous IIS Unicode directory traversal exploit (CVE-2000-0884). - -## Deep Analysis - -### Relevant ABNF - -``` -request-line = method SP request-target SP HTTP-version -origin-form = absolute-path [ "?" query ] -segment = *pchar -pchar = unreserved / pct-encoded / sub-delims / ":" / "@" -unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" -``` - -### RFC Evidence - -> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` -> -- RFC 3986 Section 3.3 - -> "Implementations of the decoding algorithm above MUST protect against decoding invalid sequences." -> -- RFC 3629 Section 3 - -> "The security threat is very real. [...] a widespread virus attacking Web servers in 2001 relied on the mishandling of overlong UTF-8 sequences to compromise vulnerable systems." -> -- RFC 3629 Section 10 - -### Chain of Reasoning - -1. **The raw bytes are not valid URI characters.** Bytes `0xC0` and `0xAF` both fall outside the ASCII range used by `pchar`. Neither matches `unreserved` (limited to `ALPHA`, `DIGIT`, `-`, `.`, `_`, `~`, all below `0x7F`), `sub-delims`, `:`, `@`, or `pct-encoded` (requires a leading `%`). The request-target violates the URI grammar at the byte level, independent of any UTF-8 interpretation. - -2. **The bytes form an overlong UTF-8 encoding of `/`.** In standard UTF-8, `U+002F` (forward slash) is encoded as the single byte `0x2F`. The two-byte sequence `0xC0 0xAF` uses the `110xxxxx 10xxxxxx` pattern with the value bits `00000 101111` = `0x2F`. This is an overlong encoding: it uses 2 bytes where 1 byte suffices. - -3. **Overlong sequences MUST be rejected.** RFC 3629 Section 3 requires that implementations "MUST protect against decoding invalid sequences." Overlong encodings are explicitly invalid because they violate the shortest-form requirement. A conforming UTF-8 decoder must not accept `0xC0 0xAF` as equivalent to `0x2F`. - -4. **CVE-2000-0884 exploited exactly this pattern.** Microsoft IIS on Windows decoded overlong UTF-8 sequences in URLs, allowing `..%c0%af..` to be interpreted as `../../`. This enabled remote directory traversal, giving attackers access to files outside the web root. RFC 3629 Section 10 explicitly references this class of attack, noting "a widespread virus attacking Web servers in 2001" exploited overlong UTF-8 mishandling. - -5. **Two layers of defense apply.** First, the bytes fail the URI grammar (they are not valid `pchar`), so a strict URI parser will reject the request before any UTF-8 decoding. Second, even if a server attempts UTF-8 decoding, RFC 3629 mandates rejection of the overlong sequence. A server that accepts this request has failed at both layers. - -## Sources - -- [RFC 3986 Section 3.3](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) — URI path and pchar grammar -- [RFC 3629 Section 3](https://www.rfc-editor.org/rfc/rfc3629#section-3) — UTF-8 decoding requirements -- [RFC 3629 Section 10](https://www.rfc-editor.org/rfc/rfc3629#section-10) — UTF-8 security considerations -- [CVE-2000-0884](https://nvd.nist.gov/vuln/detail/CVE-2000-0884) — IIS Unicode directory traversal +--- +title: "URL Overlong UTF-8 — Malformed Input" +description: "A GET request with raw overlong UTF-8 bytes in the URL path. The bytes 0xC0 0xAF are an overlong encoding of / (U+002F). Part of Http11Probe's malformed input robustness suite." +weight: 22 +--- + +| | | +|---|---| +| **Test ID** | `MAL-URL-OVERLONG-UTF8` | +| **Category** | Malformed Input | +| **Expected** | `400` or close | + +## What it sends + +A GET request with raw overlong UTF-8 bytes in the URL path. The bytes `0xC0 0xAF` are an overlong encoding of `/` (U+002F). + +```http +GET /\xC0\xAF HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +The two bytes after `/` are `0xC0 0xAF` -- an illegal two-byte UTF-8 sequence that decodes to the ASCII forward slash character. + +## What the RFC says + +Raw bytes `0xC0` and `0xAF` are not valid URI characters. URI paths are limited to `pchar`: + +> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` — RFC 3986 Section 3.3 + +All `unreserved` and `sub-delims` characters are ASCII (`0x21-0x7E`). Bytes `0xC0` and `0xAF` fall outside this range and are not percent-encoded, so they violate the URI grammar. + +Additionally, RFC 3629 requires rejection of overlong encodings: + +> "Implementations of the decoding algorithm above MUST protect against decoding invalid sequences." — RFC 3629 Section 3 + +The bytes `0xC0 0xAF` are an overlong UTF-8 encoding of `U+002F` (forward slash `/`), which must be encoded as the single byte `0x2F`. + +## Why it matters + +Overlong UTF-8 sequences encode characters using more bytes than necessary. If a server decodes `0xC0 0xAF` as `/` during path resolution, it can bypass path traversal filters (e.g., `..%c0%af..` becomes `../../`). This was the basis of the infamous IIS Unicode directory traversal exploit (CVE-2000-0884). + +## Deep Analysis + +### Relevant ABNF + +``` +request-line = method SP request-target SP HTTP-version +origin-form = absolute-path [ "?" query ] +segment = *pchar +pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +``` + +### RFC Evidence + +> `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` +> -- RFC 3986 Section 3.3 + +> "Implementations of the decoding algorithm above MUST protect against decoding invalid sequences." +> -- RFC 3629 Section 3 + +> "The security threat is very real. [...] a widespread virus attacking Web servers in 2001 relied on the mishandling of overlong UTF-8 sequences to compromise vulnerable systems." +> -- RFC 3629 Section 10 + +### Chain of Reasoning + +1. **The raw bytes are not valid URI characters.** Bytes `0xC0` and `0xAF` both fall outside the ASCII range used by `pchar`. Neither matches `unreserved` (limited to `ALPHA`, `DIGIT`, `-`, `.`, `_`, `~`, all below `0x7F`), `sub-delims`, `:`, `@`, or `pct-encoded` (requires a leading `%`). The request-target violates the URI grammar at the byte level, independent of any UTF-8 interpretation. + +2. **The bytes form an overlong UTF-8 encoding of `/`.** In standard UTF-8, `U+002F` (forward slash) is encoded as the single byte `0x2F`. The two-byte sequence `0xC0 0xAF` uses the `110xxxxx 10xxxxxx` pattern with the value bits `00000 101111` = `0x2F`. This is an overlong encoding: it uses 2 bytes where 1 byte suffices. + +3. **Overlong sequences MUST be rejected.** RFC 3629 Section 3 requires that implementations "MUST protect against decoding invalid sequences." Overlong encodings are explicitly invalid because they violate the shortest-form requirement. A conforming UTF-8 decoder must not accept `0xC0 0xAF` as equivalent to `0x2F`. + +4. **CVE-2000-0884 exploited exactly this pattern.** Microsoft IIS on Windows decoded overlong UTF-8 sequences in URLs, allowing `..%c0%af..` to be interpreted as `../../`. This enabled remote directory traversal, giving attackers access to files outside the web root. RFC 3629 Section 10 explicitly references this class of attack, noting "a widespread virus attacking Web servers in 2001" exploited overlong UTF-8 mishandling. + +5. **Two layers of defense apply.** First, the bytes fail the URI grammar (they are not valid `pchar`), so a strict URI parser will reject the request before any UTF-8 decoding. Second, even if a server attempts UTF-8 decoding, RFC 3629 mandates rejection of the overlong sequence. A server that accepts this request has failed at both layers. + +## Sources + +- [RFC 3986 Section 3.3](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) — URI path and pchar grammar +- [RFC 3629 Section 3](https://www.rfc-editor.org/rfc/rfc3629#section-3) — UTF-8 decoding requirements +- [RFC 3629 Section 10](https://www.rfc-editor.org/rfc/rfc3629#section-10) — UTF-8 security considerations +- [CVE-2000-0884](https://nvd.nist.gov/vuln/detail/CVE-2000-0884) — IIS Unicode directory traversal diff --git a/docs/content/docs/malformed-input/url-percent-crlf.md b/docs/content/docs/malformed-input/url-percent-crlf.md index fb5316a..85bdda9 100644 --- a/docs/content/docs/malformed-input/url-percent-crlf.md +++ b/docs/content/docs/malformed-input/url-percent-crlf.md @@ -1,89 +1,89 @@ ---- -title: "URL-PERCENT-CRLF" -description: "URL-PERCENT-CRLF test documentation" -weight: 24 ---- - -| | | -|---|---| -| **Test ID** | `MAL-URL-PERCENT-CRLF` | -| **Category** | Malformed Input | -| **Expected** | `400` = Pass, `2xx`/`404` = Warn | - -## What it sends - -A GET request with percent-encoded CRLF characters (`%0d%0a`) in the URL, followed by an injected header. - -```http -GET /path%0d%0aX-Injected:%20true HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -The percent-encodings `%0d` and `%0a` are syntactically valid per the URI grammar: - -> `pct-encoded = "%" HEXDIG HEXDIG` — RFC 3986 Section 2.1 - -However, the decoded values (CR and LF) are HTTP message delimiters. If the server percent-decodes the request-target before parsing is complete, the decoded CR LF bytes can be interpreted as header line terminators: - -> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content." — RFC 9112 Section 2.2 - -The RFC explicitly treats CR and LF as dangerous in field values: - -> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters." — RFC 9110 Section 5.5 - -## Pass/Warn explanation - -- **Pass (400):** The server rejects the request containing `%0d%0a` in the URL, preventing CRLF injection. -- **Warn (2xx/404):** The server processed the request without injecting headers. It may have handled the encoded CRLF safely, but accepting this input is a risk if other components in the pipeline decode differently. - -## Why it matters - -Percent-encoded CRLF (`%0d%0a`) in the URL is a header injection vector if the server percent-decodes during initial request parsing. This could allow injecting arbitrary HTTP headers, splitting the response, or poisoning caches. - -## Deep Analysis - -### Relevant ABNF - -``` -request-target = origin-form / absolute-form / authority-form / asterisk-form -origin-form = absolute-path [ "?" query ] -segment = *pchar -pchar = unreserved / pct-encoded / sub-delims / ":" / "@" -pct-encoded = "%" HEXDIG HEXDIG - -field-vchar = VCHAR / obs-text -VCHAR = %x21-7E -``` - -### RFC Evidence - -> `pct-encoded = "%" HEXDIG HEXDIG` -> -- RFC 3986 Section 2.1 - -> "A percent-encoded octet is encoded as a character triplet, consisting of the percent character '%' followed by the two hexadecimal digits representing that octet's numeric value." -> -- RFC 3986 Section 2.1 - -> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters; a recipient of CR, LF, or NUL within a field value MUST either reject the message or replace each of those characters with SP before further processing or forwarding of that message." -> -- RFC 9110 Section 5.5 - -### Chain of Reasoning - -1. **The percent-encodings are syntactically valid.** `%0d` and `%0a` conform to `pct-encoded = "%" HEXDIG HEXDIG`. At the URI grammar level, the request-target `/path%0d%0aX-Injected:%20true` is a valid `origin-form` -- percent-encoded octets are allowed in path segments. - -2. **The danger arises from premature decoding.** If a server percent-decodes the request-target before completing HTTP message parsing, `%0d%0a` becomes `CR LF` (`0x0D 0x0A`). These are the HTTP line terminator characters. The decoded result would appear to the parser as a line break followed by `X-Injected: true` -- an injected header field. - -3. **CR and LF are explicitly called out as dangerous.** RFC 9110 Section 5.5 uses strong language: "invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters." The MUST-level requirement to reject or replace applies to the decoded values if they reach the field-value layer. - -4. **The correct parsing order prevents injection.** A properly implemented HTTP/1.1 parser first splits the message on raw CRLF boundaries to identify the request-line and header fields, then percent-decodes the request-target during URI interpretation. In this order, `%0d%0a` remains encoded during the structural parsing phase and never creates a spurious line break. - -5. **Warn for 2xx/404 reflects implementation-dependent safety.** A server returning 2xx or 404 may have handled the percent-encoded CRLF safely (correct parse order), but the acceptance creates risk if other components in the pipeline (reverse proxies, WAFs, backend applications) decode at a different stage. The request is a valid probe for CRLF injection vulnerabilities across the request chain. - -## Sources - -- [RFC 3986 Section 2.1](https://www.rfc-editor.org/rfc/rfc3986#section-2.1) — percent-encoding grammar -- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — bare CR prohibition -- [RFC 9110 Section 5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) — CR/LF characters are dangerous -- [CWE-113](https://cwe.mitre.org/data/definitions/113.html) — Improper Neutralization of CRLF Sequences +--- +title: "URL Percent CRLF — Malformed Input" +description: "A GET request with percent-encoded CRLF characters (%0d%0a) in the URL, followed by an injected header. Part of Http11Probe's malformed input robustness suite." +weight: 24 +--- + +| | | +|---|---| +| **Test ID** | `MAL-URL-PERCENT-CRLF` | +| **Category** | Malformed Input | +| **Expected** | `400` = Pass, `2xx`/`404` = Warn | + +## What it sends + +A GET request with percent-encoded CRLF characters (`%0d%0a`) in the URL, followed by an injected header. + +```http +GET /path%0d%0aX-Injected:%20true HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +The percent-encodings `%0d` and `%0a` are syntactically valid per the URI grammar: + +> `pct-encoded = "%" HEXDIG HEXDIG` — RFC 3986 Section 2.1 + +However, the decoded values (CR and LF) are HTTP message delimiters. If the server percent-decodes the request-target before parsing is complete, the decoded CR LF bytes can be interpreted as header line terminators: + +> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content." — RFC 9112 Section 2.2 + +The RFC explicitly treats CR and LF as dangerous in field values: + +> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters." — RFC 9110 Section 5.5 + +## Pass/Warn explanation + +- **Pass (400):** The server rejects the request containing `%0d%0a` in the URL, preventing CRLF injection. +- **Warn (2xx/404):** The server processed the request without injecting headers. It may have handled the encoded CRLF safely, but accepting this input is a risk if other components in the pipeline decode differently. + +## Why it matters + +Percent-encoded CRLF (`%0d%0a`) in the URL is a header injection vector if the server percent-decodes during initial request parsing. This could allow injecting arbitrary HTTP headers, splitting the response, or poisoning caches. + +## Deep Analysis + +### Relevant ABNF + +``` +request-target = origin-form / absolute-form / authority-form / asterisk-form +origin-form = absolute-path [ "?" query ] +segment = *pchar +pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +pct-encoded = "%" HEXDIG HEXDIG + +field-vchar = VCHAR / obs-text +VCHAR = %x21-7E +``` + +### RFC Evidence + +> `pct-encoded = "%" HEXDIG HEXDIG` +> -- RFC 3986 Section 2.1 + +> "A percent-encoded octet is encoded as a character triplet, consisting of the percent character '%' followed by the two hexadecimal digits representing that octet's numeric value." +> -- RFC 3986 Section 2.1 + +> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters; a recipient of CR, LF, or NUL within a field value MUST either reject the message or replace each of those characters with SP before further processing or forwarding of that message." +> -- RFC 9110 Section 5.5 + +### Chain of Reasoning + +1. **The percent-encodings are syntactically valid.** `%0d` and `%0a` conform to `pct-encoded = "%" HEXDIG HEXDIG`. At the URI grammar level, the request-target `/path%0d%0aX-Injected:%20true` is a valid `origin-form` -- percent-encoded octets are allowed in path segments. + +2. **The danger arises from premature decoding.** If a server percent-decodes the request-target before completing HTTP message parsing, `%0d%0a` becomes `CR LF` (`0x0D 0x0A`). These are the HTTP line terminator characters. The decoded result would appear to the parser as a line break followed by `X-Injected: true` -- an injected header field. + +3. **CR and LF are explicitly called out as dangerous.** RFC 9110 Section 5.5 uses strong language: "invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters." The MUST-level requirement to reject or replace applies to the decoded values if they reach the field-value layer. + +4. **The correct parsing order prevents injection.** A properly implemented HTTP/1.1 parser first splits the message on raw CRLF boundaries to identify the request-line and header fields, then percent-decodes the request-target during URI interpretation. In this order, `%0d%0a` remains encoded during the structural parsing phase and never creates a spurious line break. + +5. **Warn for 2xx/404 reflects implementation-dependent safety.** A server returning 2xx or 404 may have handled the percent-encoded CRLF safely (correct parse order), but the acceptance creates risk if other components in the pipeline (reverse proxies, WAFs, backend applications) decode at a different stage. The request is a valid probe for CRLF injection vulnerabilities across the request chain. + +## Sources + +- [RFC 3986 Section 2.1](https://www.rfc-editor.org/rfc/rfc3986#section-2.1) — percent-encoding grammar +- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — bare CR prohibition +- [RFC 9110 Section 5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) — CR/LF characters are dangerous +- [CWE-113](https://cwe.mitre.org/data/definitions/113.html) — Improper Neutralization of CRLF Sequences diff --git a/docs/content/docs/malformed-input/url-percent-null.md b/docs/content/docs/malformed-input/url-percent-null.md index 0b8a0fb..7d9f321 100644 --- a/docs/content/docs/malformed-input/url-percent-null.md +++ b/docs/content/docs/malformed-input/url-percent-null.md @@ -1,83 +1,83 @@ ---- -title: "URL-PERCENT-NULL" -description: "URL-PERCENT-NULL test documentation" -weight: 23 ---- - -| | | -|---|---| -| **Test ID** | `MAL-URL-PERCENT-NULL` | -| **Category** | Malformed Input | -| **Expected** | `400` = Pass, `2xx`/`404` = Warn | - -## What it sends - -A GET request with a percent-encoded NUL byte (`%00`) in the URL path. - -```http -GET /path%00.html HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -The percent-encoding `%00` is syntactically valid per the URI grammar: - -> `pct-encoded = "%" HEXDIG HEXDIG` — RFC 3986 Section 2.1 - -However, the decoded value (NUL byte, `0x00`) is dangerous. While RFC 3986 does not explicitly prohibit `%00`, the HTTP semantics layer addresses NUL in field values: - -> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters." — RFC 9110 Section 5.5 - -The same principle applies to NUL in the request-target: implementations vary in how they handle it, creating security risks. - -## Pass/Warn explanation - -- **Pass (400):** The server rejects the request containing `%00` in the URL, preventing null byte injection attacks. -- **Warn (2xx/404):** The server processed the request. It may have decoded `%00` safely, but this is a security risk if the decoded NUL reaches backend systems (path truncation, access control bypass). - -## Why it matters - -Percent-encoded NUL byte (`%00`) can cause C-based servers to truncate the path string at the null byte. For example, `file%00.php` might be interpreted as `file` while bypassing extension-based access controls. - -## Deep Analysis - -### Relevant ABNF - -``` -request-target = origin-form / absolute-form / authority-form / asterisk-form -origin-form = absolute-path [ "?" query ] -segment = *pchar -pchar = unreserved / pct-encoded / sub-delims / ":" / "@" -pct-encoded = "%" HEXDIG HEXDIG -``` - -### RFC Evidence - -> `pct-encoded = "%" HEXDIG HEXDIG` -> -- RFC 3986 Section 2.1 - -> "A percent-encoded octet is encoded as a character triplet, consisting of the percent character '%' followed by the two hexadecimal digits representing that octet's numeric value." -> -- RFC 3986 Section 2.1 - -> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters; a recipient of CR, LF, or NUL within a field value MUST either reject the message or replace each of those characters with SP before further processing or forwarding of that message." -> -- RFC 9110 Section 5.5 - -### Chain of Reasoning - -1. **The percent-encoding is syntactically valid.** `%00` conforms to `pct-encoded = "%" HEXDIG HEXDIG`. At the URI grammar level, `/path%00.html` is a valid `origin-form` -- percent-encoded octets are allowed in path segments regardless of the value they encode. - -2. **The decoded value is a NUL byte (`0x00`).** When the server percent-decodes the request-target for path resolution, `%00` becomes the NUL character. RFC 9110 Section 5.5 explicitly identifies NUL as "invalid and dangerous" in the context of HTTP field values. While Section 5.5 technically addresses field values rather than URIs, the same danger applies: NUL bytes are interpreted inconsistently across implementations. - -3. **C-string truncation is the primary attack.** In C and C++, strings are NUL-terminated. After percent-decoding, `/path\x00.html` may be truncated to `/path` by any function using `strlen()`, `strcmp()`, or similar. This enables: (a) bypassing extension-based access controls (e.g., `.php` restriction bypassed when the extension is truncated), (b) accessing unintended files (the truncated path resolves to a different resource), and (c) file upload filter bypass (e.g., `shell.php%00.jpg` passes a `.jpg` extension check but saves as `shell.php`). - -4. **No RFC explicitly prohibits `%00` in URIs.** RFC 3986 does not list `%00` as a forbidden percent-encoding. However, RFC 9110 Section 5.5's treatment of NUL as "dangerous" establishes a clear security principle. A server that rejects `%00` in URLs is applying a reasonable security policy consistent with the RFC's treatment of NUL. - -5. **Warn for 2xx/404 reflects the nuance.** Since `%00` is grammatically valid, a server that processes the request is not strictly violating the URI grammar. However, safely handling the decoded NUL requires the server (and every downstream component) to use NUL-safe string operations -- a fragile assumption. Rejection with 400 is the more robust approach. - -## Sources - -- [RFC 3986 Section 2.1](https://www.rfc-editor.org/rfc/rfc3986#section-2.1) — percent-encoding grammar -- [RFC 9110 Section 5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) — NUL characters are dangerous -- [CWE-158](https://cwe.mitre.org/data/definitions/158.html) — Improper Neutralization of Null Byte +--- +title: "URL Percent Null — Malformed Input" +description: "A GET request with a percent-encoded NUL byte (%00) in the URL path. Part of Http11Probe's malformed input robustness suite." +weight: 23 +--- + +| | | +|---|---| +| **Test ID** | `MAL-URL-PERCENT-NULL` | +| **Category** | Malformed Input | +| **Expected** | `400` = Pass, `2xx`/`404` = Warn | + +## What it sends + +A GET request with a percent-encoded NUL byte (`%00`) in the URL path. + +```http +GET /path%00.html HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +The percent-encoding `%00` is syntactically valid per the URI grammar: + +> `pct-encoded = "%" HEXDIG HEXDIG` — RFC 3986 Section 2.1 + +However, the decoded value (NUL byte, `0x00`) is dangerous. While RFC 3986 does not explicitly prohibit `%00`, the HTTP semantics layer addresses NUL in field values: + +> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters." — RFC 9110 Section 5.5 + +The same principle applies to NUL in the request-target: implementations vary in how they handle it, creating security risks. + +## Pass/Warn explanation + +- **Pass (400):** The server rejects the request containing `%00` in the URL, preventing null byte injection attacks. +- **Warn (2xx/404):** The server processed the request. It may have decoded `%00` safely, but this is a security risk if the decoded NUL reaches backend systems (path truncation, access control bypass). + +## Why it matters + +Percent-encoded NUL byte (`%00`) can cause C-based servers to truncate the path string at the null byte. For example, `file%00.php` might be interpreted as `file` while bypassing extension-based access controls. + +## Deep Analysis + +### Relevant ABNF + +``` +request-target = origin-form / absolute-form / authority-form / asterisk-form +origin-form = absolute-path [ "?" query ] +segment = *pchar +pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +pct-encoded = "%" HEXDIG HEXDIG +``` + +### RFC Evidence + +> `pct-encoded = "%" HEXDIG HEXDIG` +> -- RFC 3986 Section 2.1 + +> "A percent-encoded octet is encoded as a character triplet, consisting of the percent character '%' followed by the two hexadecimal digits representing that octet's numeric value." +> -- RFC 3986 Section 2.1 + +> "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the varying ways that implementations might parse and interpret those characters; a recipient of CR, LF, or NUL within a field value MUST either reject the message or replace each of those characters with SP before further processing or forwarding of that message." +> -- RFC 9110 Section 5.5 + +### Chain of Reasoning + +1. **The percent-encoding is syntactically valid.** `%00` conforms to `pct-encoded = "%" HEXDIG HEXDIG`. At the URI grammar level, `/path%00.html` is a valid `origin-form` -- percent-encoded octets are allowed in path segments regardless of the value they encode. + +2. **The decoded value is a NUL byte (`0x00`).** When the server percent-decodes the request-target for path resolution, `%00` becomes the NUL character. RFC 9110 Section 5.5 explicitly identifies NUL as "invalid and dangerous" in the context of HTTP field values. While Section 5.5 technically addresses field values rather than URIs, the same danger applies: NUL bytes are interpreted inconsistently across implementations. + +3. **C-string truncation is the primary attack.** In C and C++, strings are NUL-terminated. After percent-decoding, `/path\x00.html` may be truncated to `/path` by any function using `strlen()`, `strcmp()`, or similar. This enables: (a) bypassing extension-based access controls (e.g., `.php` restriction bypassed when the extension is truncated), (b) accessing unintended files (the truncated path resolves to a different resource), and (c) file upload filter bypass (e.g., `shell.php%00.jpg` passes a `.jpg` extension check but saves as `shell.php`). + +4. **No RFC explicitly prohibits `%00` in URIs.** RFC 3986 does not list `%00` as a forbidden percent-encoding. However, RFC 9110 Section 5.5's treatment of NUL as "dangerous" establishes a clear security principle. A server that rejects `%00` in URLs is applying a reasonable security policy consistent with the RFC's treatment of NUL. + +5. **Warn for 2xx/404 reflects the nuance.** Since `%00` is grammatically valid, a server that processes the request is not strictly violating the URI grammar. However, safely handling the decoded NUL requires the server (and every downstream component) to use NUL-safe string operations -- a fragile assumption. Rejection with 400 is the more robust approach. + +## Sources + +- [RFC 3986 Section 2.1](https://www.rfc-editor.org/rfc/rfc3986#section-2.1) — percent-encoding grammar +- [RFC 9110 Section 5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) — NUL characters are dangerous +- [CWE-158](https://cwe.mitre.org/data/definitions/158.html) — Improper Neutralization of Null Byte diff --git a/docs/content/docs/malformed-input/whitespace-only-line.md b/docs/content/docs/malformed-input/whitespace-only-line.md index 9730528..d87b46e 100644 --- a/docs/content/docs/malformed-input/whitespace-only-line.md +++ b/docs/content/docs/malformed-input/whitespace-only-line.md @@ -1,82 +1,82 @@ ---- -title: "WHITESPACE-ONLY-LINE" -description: "WHITESPACE-ONLY-LINE test documentation" -weight: 14 ---- - -| | | -|---|---| -| **Test ID** | `MAL-WHITESPACE-ONLY-LINE` | -| **Category** | Malformed Input | -| **Expected** | `400`, close, or timeout | - -## What it sends - -A line consisting only of spaces and tabs -- no method, URI, or version. - -```http - \r\n -\r\n -``` - -The request-line consists of three spaces followed by CRLF — no method, target, or version. - - -## What the RFC says - -> "In the interest of robustness, a server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." — RFC 9112 Section 2.2 - -An empty line is defined as a bare CRLF. A line containing spaces is not empty -- it is a non-empty sequence of octets that does not match the request-line grammar: - -> `request-line = method SP request-target SP HTTP-version` — RFC 9112 Section 3 - -Three spaces followed by CRLF cannot be parsed as `method SP request-target SP HTTP-version`. - -> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar...the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 - -## Why it matters - -A line of only whitespace is neither empty (CRLF) nor a valid request-line. If a server treats whitespace-only lines as empty lines and ignores them, it may be tricked into accepting subsequent malicious data as a valid request. - -## Deep Analysis - -### Relevant ABNF - -``` -HTTP-message = start-line CRLF *( field-line CRLF ) CRLF [ message-body ] -start-line = request-line / status-line -request-line = method SP request-target SP HTTP-version -method = token -token = 1*tchar -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" - / DIGIT / ALPHA -``` - -### RFC Evidence - -> "In the interest of robustness, a server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." -> -- RFC 9112 Section 2.2 - -> `request-line = method SP request-target SP HTTP-version` -> -- RFC 9112 Section 3 - -> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." -> -- RFC 9112 Section 2.2 - -### Chain of Reasoning - -1. **A whitespace-only line is not an empty line.** The robustness exception in RFC 9112 Section 2.2 allows ignoring "at least one empty line (CRLF)" before the request-line. An empty line is precisely `CRLF` -- zero content octets followed by the line terminator. A line containing `SP SP SP CRLF` has three content octets; it is not empty. - -2. **The line cannot be parsed as a `request-line`.** The grammar requires `method SP request-target SP HTTP-version`. The `method` production is `token = 1*tchar`, requiring at least one `tchar` character. `SP` (`0x20`) is not a `tchar` (the lowest `tchar` value is `0x21`). The parser cannot extract a `method` from a sequence of spaces, so the line fails at the very first production. - -3. **No robustness exception applies.** The only robustness exception in RFC 9112 Section 2.2 is for empty CRLF lines. There is no exception for whitespace-only lines, lines containing only control characters, or any other non-empty, non-matching input. The whitespace-only line falls squarely into the "sequence of octets that does not match the HTTP-message grammar" clause. - -4. **The 400 rejection rule triggers.** RFC 9112 Section 2.2 instructs the server to "respond with a 400 (Bad Request) response and close the connection" for input that does not match the grammar. This is a SHOULD-level requirement. - -5. **Treating whitespace as empty is a security risk.** If a server ignores whitespace-only lines as if they were empty CRLF lines, an attacker could use this behavior to desynchronize request boundaries in a pipeline. For example, inserting `SP SP SP CRLF` between pipelined requests might cause one server to skip the line while another treats it as the start of a malformed request, leading to request smuggling. - -## Sources - -- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — message parsing robustness -- [RFC 9112 Section 3](https://www.rfc-editor.org/rfc/rfc9112#section-3) — request-line grammar +--- +title: "Whitespace Only Line — Malformed Input" +description: "A line consisting only of spaces and tabs -- no method, URI, or version. Part of Http11Probe's malformed input robustness suite." +weight: 14 +--- + +| | | +|---|---| +| **Test ID** | `MAL-WHITESPACE-ONLY-LINE` | +| **Category** | Malformed Input | +| **Expected** | `400`, close, or timeout | + +## What it sends + +A line consisting only of spaces and tabs -- no method, URI, or version. + +```http + \r\n +\r\n +``` + +The request-line consists of three spaces followed by CRLF — no method, target, or version. + + +## What the RFC says + +> "In the interest of robustness, a server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." — RFC 9112 Section 2.2 + +An empty line is defined as a bare CRLF. A line containing spaces is not empty -- it is a non-empty sequence of octets that does not match the request-line grammar: + +> `request-line = method SP request-target SP HTTP-version` — RFC 9112 Section 3 + +Three spaces followed by CRLF cannot be parsed as `method SP request-target SP HTTP-version`. + +> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar...the server SHOULD respond with a 400 (Bad Request) response and close the connection." — RFC 9112 Section 2.2 + +## Why it matters + +A line of only whitespace is neither empty (CRLF) nor a valid request-line. If a server treats whitespace-only lines as empty lines and ignores them, it may be tricked into accepting subsequent malicious data as a valid request. + +## Deep Analysis + +### Relevant ABNF + +``` +HTTP-message = start-line CRLF *( field-line CRLF ) CRLF [ message-body ] +start-line = request-line / status-line +request-line = method SP request-target SP HTTP-version +method = token +token = 1*tchar +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + / DIGIT / ALPHA +``` + +### RFC Evidence + +> "In the interest of robustness, a server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line." +> -- RFC 9112 Section 2.2 + +> `request-line = method SP request-target SP HTTP-version` +> -- RFC 9112 Section 3 + +> "When a server listening only for HTTP request messages...receives a sequence of octets that does not match the HTTP-message grammar aside from the robustness exceptions listed above, the server SHOULD respond with a 400 (Bad Request) response and close the connection." +> -- RFC 9112 Section 2.2 + +### Chain of Reasoning + +1. **A whitespace-only line is not an empty line.** The robustness exception in RFC 9112 Section 2.2 allows ignoring "at least one empty line (CRLF)" before the request-line. An empty line is precisely `CRLF` -- zero content octets followed by the line terminator. A line containing `SP SP SP CRLF` has three content octets; it is not empty. + +2. **The line cannot be parsed as a `request-line`.** The grammar requires `method SP request-target SP HTTP-version`. The `method` production is `token = 1*tchar`, requiring at least one `tchar` character. `SP` (`0x20`) is not a `tchar` (the lowest `tchar` value is `0x21`). The parser cannot extract a `method` from a sequence of spaces, so the line fails at the very first production. + +3. **No robustness exception applies.** The only robustness exception in RFC 9112 Section 2.2 is for empty CRLF lines. There is no exception for whitespace-only lines, lines containing only control characters, or any other non-empty, non-matching input. The whitespace-only line falls squarely into the "sequence of octets that does not match the HTTP-message grammar" clause. + +4. **The 400 rejection rule triggers.** RFC 9112 Section 2.2 instructs the server to "respond with a 400 (Bad Request) response and close the connection" for input that does not match the grammar. This is a SHOULD-level requirement. + +5. **Treating whitespace as empty is a security risk.** If a server ignores whitespace-only lines as if they were empty CRLF lines, an attacker could use this behavior to desynchronize request boundaries in a pipeline. For example, inserting `SP SP SP CRLF` between pipelined requests might cause one server to skip the line while another treats it as the start of a malformed request, leading to request smuggling. + +## Sources + +- [RFC 9112 Section 2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) — message parsing robustness +- [RFC 9112 Section 3](https://www.rfc-editor.org/rfc/rfc9112#section-3) — request-line grammar diff --git a/docs/content/docs/normalization/_index.md b/docs/content/docs/normalization/_index.md index eac4e25..49878cb 100644 --- a/docs/content/docs/normalization/_index.md +++ b/docs/content/docs/normalization/_index.md @@ -1,6 +1,6 @@ --- title: Header Normalization -description: "Header Normalization — Http11Probe documentation" +description: "Echo-based tests checking whether servers normalize malformed header names — underscore-to-hyphen, tab in name, or case folding on Transfer-Encoding." weight: 8 sidebar: open: false diff --git a/docs/content/docs/normalization/case-te.md b/docs/content/docs/normalization/case-te.md index 74cad91..5f01545 100644 --- a/docs/content/docs/normalization/case-te.md +++ b/docs/content/docs/normalization/case-te.md @@ -1,45 +1,45 @@ ---- -title: "CASE-TE" -description: "CASE-TE test documentation" -weight: 4 ---- - -| | | -|---|---| -| **Test ID** | `NORM-CASE-TE` | -| **Category** | Normalization | -| **Scored** | No | -| **Expected** | Reject/drop (pass), normalize casing (fail), preserve (warn) | - -## What it sends - -A POST request to `/echo` with a valid `Content-Length: 11` for body framing, plus an all-uppercase `TRANSFER-ENCODING: chunked` header. - -```http -POST /echo HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 11\r\n -TRANSFER-ENCODING: chunked\r\n -\r\n -hello world -``` - -## What the RFC says - -RFC 9110 Section 5.1: - -> "Each field name [...] is case-insensitive." - -This means `TRANSFER-ENCODING` and `Transfer-Encoding` are semantically identical. Servers are required to treat them the same way. - -## Pass / Fail / Warn - -**Pass:** Server rejects (CL/TE conflict) or drops the header. -**Fail:** Server normalizes casing to `Transfer-Encoding` or `transfer-encoding` in the echo output. -**Warn:** Server preserves the original `TRANSFER-ENCODING` casing. - -## Why it matters - -This test is **unscored** because case normalization of header names is RFC-compliant and common. It provides visibility into how the server processes header name casing, which is informational for understanding proxy-chain behavior. - -If the server processes `TRANSFER-ENCODING: chunked` as a real Transfer-Encoding header, the CL/TE conflict would cause the request to be rejected (which is Pass). The interesting case is when the echo reveals casing transformation without the server acting on the value. +--- +title: "Case TE — Header Normalization" +description: "A POST request to /echo with a valid Content-Length: 11 for body framing, plus an all-uppercase TRANSFER-ENCODING: chunked header." +weight: 4 +--- + +| | | +|---|---| +| **Test ID** | `NORM-CASE-TE` | +| **Category** | Normalization | +| **Scored** | No | +| **Expected** | Reject/drop (pass), normalize casing (fail), preserve (warn) | + +## What it sends + +A POST request to `/echo` with a valid `Content-Length: 11` for body framing, plus an all-uppercase `TRANSFER-ENCODING: chunked` header. + +```http +POST /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 11\r\n +TRANSFER-ENCODING: chunked\r\n +\r\n +hello world +``` + +## What the RFC says + +RFC 9110 Section 5.1: + +> "Each field name [...] is case-insensitive." + +This means `TRANSFER-ENCODING` and `Transfer-Encoding` are semantically identical. Servers are required to treat them the same way. + +## Pass / Fail / Warn + +**Pass:** Server rejects (CL/TE conflict) or drops the header. +**Fail:** Server normalizes casing to `Transfer-Encoding` or `transfer-encoding` in the echo output. +**Warn:** Server preserves the original `TRANSFER-ENCODING` casing. + +## Why it matters + +This test is **unscored** because case normalization of header names is RFC-compliant and common. It provides visibility into how the server processes header name casing, which is informational for understanding proxy-chain behavior. + +If the server processes `TRANSFER-ENCODING: chunked` as a real Transfer-Encoding header, the CL/TE conflict would cause the request to be rejected (which is Pass). The interesting case is when the echo reveals casing transformation without the server acting on the value. diff --git a/docs/content/docs/normalization/sp-before-colon-cl.md b/docs/content/docs/normalization/sp-before-colon-cl.md index f7c8635..dd25f64 100644 --- a/docs/content/docs/normalization/sp-before-colon-cl.md +++ b/docs/content/docs/normalization/sp-before-colon-cl.md @@ -1,42 +1,42 @@ ---- -title: "SP-BEFORE-COLON-CL" -description: "SP-BEFORE-COLON-CL test documentation" -weight: 2 ---- - -| | | -|---|---| -| **Test ID** | `NORM-SP-BEFORE-COLON-CL` | -| **Category** | Normalization | -| **RFC** | [RFC 9112 §5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | -| **Requirement** | MUST reject | -| **Expected** | Reject/drop (pass), normalize (fail), preserve (warn) | - -## What it sends - -A POST request to `/echo` with a valid `Content-Length: 11` for body framing, plus a malformed `Content-Length : 5` header with a space before the colon. - -```http -POST /echo HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 11\r\n -Content-Length : 5\r\n -\r\n -hello world -``` - -## What the RFC says - -RFC 9112 Section 5 states: - -> "No whitespace is allowed between the field name and colon. [...] A server MUST reject, with a response status code of 400 (Bad Request), any received request message that contains whitespace between a header field name and colon." - -## Pass / Fail / Warn - -**Pass:** Server rejects the request (`400`) or drops the malformed header. -**Fail:** Server strips the whitespace and normalizes to `Content-Length: 5` — the echo shows a Content-Length header with value `5`, overriding the valid value of `11`. -**Warn:** Server preserves the header with the trailing space in the name. - -## Why it matters - -If a server normalizes `Content-Length : 5` by stripping the whitespace, the request now has two Content-Length values (11 and 5). This creates a framing disagreement that can enable request smuggling. The RFC explicitly mandates rejection with 400 for this reason. +--- +title: "Space Before Colon CL — Header Normalization" +description: "A POST request to /echo with a valid Content-Length: 11 for body framing, plus a malformed Content-Length : 5 header with a space before the colon. Tested against RFC 9112 §5." +weight: 2 +--- + +| | | +|---|---| +| **Test ID** | `NORM-SP-BEFORE-COLON-CL` | +| **Category** | Normalization | +| **RFC** | [RFC 9112 §5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | +| **Requirement** | MUST reject | +| **Expected** | Reject/drop (pass), normalize (fail), preserve (warn) | + +## What it sends + +A POST request to `/echo` with a valid `Content-Length: 11` for body framing, plus a malformed `Content-Length : 5` header with a space before the colon. + +```http +POST /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 11\r\n +Content-Length : 5\r\n +\r\n +hello world +``` + +## What the RFC says + +RFC 9112 Section 5 states: + +> "No whitespace is allowed between the field name and colon. [...] A server MUST reject, with a response status code of 400 (Bad Request), any received request message that contains whitespace between a header field name and colon." + +## Pass / Fail / Warn + +**Pass:** Server rejects the request (`400`) or drops the malformed header. +**Fail:** Server strips the whitespace and normalizes to `Content-Length: 5` — the echo shows a Content-Length header with value `5`, overriding the valid value of `11`. +**Warn:** Server preserves the header with the trailing space in the name. + +## Why it matters + +If a server normalizes `Content-Length : 5` by stripping the whitespace, the request now has two Content-Length values (11 and 5). This creates a framing disagreement that can enable request smuggling. The RFC explicitly mandates rejection with 400 for this reason. diff --git a/docs/content/docs/normalization/tab-in-name.md b/docs/content/docs/normalization/tab-in-name.md index 325714e..853924f 100644 --- a/docs/content/docs/normalization/tab-in-name.md +++ b/docs/content/docs/normalization/tab-in-name.md @@ -1,44 +1,44 @@ ---- -title: "TAB-IN-NAME" -description: "TAB-IN-NAME test documentation" -weight: 3 ---- - -| | | -|---|---| -| **Test ID** | `NORM-TAB-IN-NAME` | -| **Category** | Normalization | -| **Requirement** | MUST reject (invalid token character) | -| **Expected** | Reject/drop (pass), normalize (fail), preserve (warn) | - -## What it sends - -A POST request to `/echo` with a valid `Content-Length: 11` for body framing, plus a header containing a tab character in the name: `Content\tLength: 99`. - -```http -POST /echo HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 11\r\n -Content[TAB]Length: 99\r\n -\r\n -hello world -``` - -## What the RFC says - -RFC 9110 Section 5.1 defines field names using the `token` production: - -> `token = 1*tchar` -> `tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA` - -The horizontal tab character (0x09) is not a valid `tchar`, so `Content\tLength` is not a valid header name. - -## Pass / Fail / Warn - -**Pass:** Server rejects the request (`400`) or drops the malformed header. -**Fail:** Server normalizes `Content\tLength` to `Content-Length` — the echo shows `Content-Length: 99`. -**Warn:** Server preserves the original name with the tab character. - -## Why it matters - -A server that converts a tab to a hyphen (or strips it) silently transforms an invalid header name into a real Content-Length header. Proxies that pass the tab-containing name through without recognizing it create a smuggling vector when the back-end normalizes. +--- +title: "Tab In Name — Header Normalization" +description: "A POST request to /echo with a valid Content-Length: 11 for body framing, plus a header containing a tab character in the name: Content\\tLength: 99." +weight: 3 +--- + +| | | +|---|---| +| **Test ID** | `NORM-TAB-IN-NAME` | +| **Category** | Normalization | +| **Requirement** | MUST reject (invalid token character) | +| **Expected** | Reject/drop (pass), normalize (fail), preserve (warn) | + +## What it sends + +A POST request to `/echo` with a valid `Content-Length: 11` for body framing, plus a header containing a tab character in the name: `Content\tLength: 99`. + +```http +POST /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 11\r\n +Content[TAB]Length: 99\r\n +\r\n +hello world +``` + +## What the RFC says + +RFC 9110 Section 5.1 defines field names using the `token` production: + +> `token = 1*tchar` +> `tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA` + +The horizontal tab character (0x09) is not a valid `tchar`, so `Content\tLength` is not a valid header name. + +## Pass / Fail / Warn + +**Pass:** Server rejects the request (`400`) or drops the malformed header. +**Fail:** Server normalizes `Content\tLength` to `Content-Length` — the echo shows `Content-Length: 99`. +**Warn:** Server preserves the original name with the tab character. + +## Why it matters + +A server that converts a tab to a hyphen (or strips it) silently transforms an invalid header name into a real Content-Length header. Proxies that pass the tab-containing name through without recognizing it create a smuggling vector when the back-end normalizes. diff --git a/docs/content/docs/normalization/underscore-cl.md b/docs/content/docs/normalization/underscore-cl.md index b36846d..f88dc63 100644 --- a/docs/content/docs/normalization/underscore-cl.md +++ b/docs/content/docs/normalization/underscore-cl.md @@ -1,43 +1,43 @@ ---- -title: "UNDERSCORE-CL" -description: "UNDERSCORE-CL test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `NORM-UNDERSCORE-CL` | -| **Category** | Normalization | -| **Requirement** | Drop or reject | -| **Expected** | Reject/drop (pass), normalize (fail), preserve (warn) | - -## What it sends - -A POST request to `/echo` with a valid `Content-Length: 11` for body framing, plus a malformed `Content_Length: 99` header using an underscore instead of a hyphen. - -```http -POST /echo HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 11\r\n -Content_Length: 99\r\n -\r\n -hello world -``` - -## What the RFC says - -RFC 9110 defines header field names using the `token` production (RFC 9110 Section 5.1), which includes hyphens (`-`) but also underscores (`_`). So `Content_Length` is technically a valid header *name* per the grammar, but it is not the standard `Content-Length` header. - -The security concern is whether the server silently maps `Content_Length` to `Content-Length`. If it does, the malformed name becomes a real framing header that upstream proxies may not have recognized. - -## Pass / Fail / Warn - -**Pass:** Server rejects the request (`400`) or drops the `Content_Length` header (echo does not contain it). -**Fail:** Server normalizes `Content_Length` to `Content-Length` — the echo shows `Content-Length: 99`. -**Warn:** Server preserves the original name — the echo shows `Content_Length: 99`. - -## Why it matters - -In a proxy chain, an upstream server may pass `Content_Length: 99` through as an unknown header. If the back-end normalizes it to `Content-Length: 99`, the request now has conflicting Content-Length values (11 vs 99), creating a classic request smuggling vector. - -This is the same class of attack tested by `SMUG-TRANSFER_ENCODING`, but applied to Content-Length instead of Transfer-Encoding. +--- +title: "Underscore CL — Header Normalization" +description: "A POST request to /echo with a valid Content-Length: 11 for body framing, plus a malformed Content_Length: 99 header using an underscore instead of a hyphen." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `NORM-UNDERSCORE-CL` | +| **Category** | Normalization | +| **Requirement** | Drop or reject | +| **Expected** | Reject/drop (pass), normalize (fail), preserve (warn) | + +## What it sends + +A POST request to `/echo` with a valid `Content-Length: 11` for body framing, plus a malformed `Content_Length: 99` header using an underscore instead of a hyphen. + +```http +POST /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 11\r\n +Content_Length: 99\r\n +\r\n +hello world +``` + +## What the RFC says + +RFC 9110 defines header field names using the `token` production (RFC 9110 Section 5.1), which includes hyphens (`-`) but also underscores (`_`). So `Content_Length` is technically a valid header *name* per the grammar, but it is not the standard `Content-Length` header. + +The security concern is whether the server silently maps `Content_Length` to `Content-Length`. If it does, the malformed name becomes a real framing header that upstream proxies may not have recognized. + +## Pass / Fail / Warn + +**Pass:** Server rejects the request (`400`) or drops the `Content_Length` header (echo does not contain it). +**Fail:** Server normalizes `Content_Length` to `Content-Length` — the echo shows `Content-Length: 99`. +**Warn:** Server preserves the original name — the echo shows `Content_Length: 99`. + +## Why it matters + +In a proxy chain, an upstream server may pass `Content_Length: 99` through as an unknown header. If the back-end normalizes it to `Content-Length: 99`, the request now has conflicting Content-Length values (11 vs 99), creating a classic request smuggling vector. + +This is the same class of attack tested by `SMUG-TRANSFER_ENCODING`, but applied to Content-Length instead of Transfer-Encoding. diff --git a/docs/content/docs/normalization/underscore-te.md b/docs/content/docs/normalization/underscore-te.md index fb18f4a..9d0e9eb 100644 --- a/docs/content/docs/normalization/underscore-te.md +++ b/docs/content/docs/normalization/underscore-te.md @@ -1,41 +1,41 @@ ---- -title: "UNDERSCORE-TE" -description: "UNDERSCORE-TE test documentation" -weight: 5 ---- - -| | | -|---|---| -| **Test ID** | `NORM-UNDERSCORE-TE` | -| **Category** | Normalization | -| **Requirement** | Drop or reject | -| **Expected** | Reject/drop (pass), normalize (fail), preserve (warn) | - -## What it sends - -A POST request to `/echo` with a valid `Content-Length: 11` for body framing, plus a malformed `Transfer_Encoding: chunked` header using an underscore instead of a hyphen. - -```http -POST /echo HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 11\r\n -Transfer_Encoding: chunked\r\n -\r\n -hello world -``` - -## What the RFC says - -Like `Content_Length`, the name `Transfer_Encoding` is a valid token per the `tchar` production but is not the standard `Transfer-Encoding` header. The security concern is whether the server maps the underscore variant to the real Transfer-Encoding header. - -## Pass / Fail / Warn - -**Pass:** Server rejects the request (`400`) or drops the `Transfer_Encoding` header. -**Fail:** Server normalizes `Transfer_Encoding` to `Transfer-Encoding` — this creates a CL/TE conflict. -**Warn:** Server preserves the original name — the echo shows `Transfer_Encoding: chunked`. - -## Why it matters - -This is the Transfer-Encoding counterpart to `NORM-UNDERSCORE-CL` and closely related to `SMUG-TRANSFER_ENCODING`. If a proxy passes `Transfer_Encoding: chunked` through without recognizing it, but the back-end normalizes it to `Transfer-Encoding: chunked`, the back-end will use chunked framing while the proxy used Content-Length. This is a textbook CL.TE smuggling vector. - -The existing `SMUG-TRANSFER_ENCODING` test checks if the server *processes* the underscore form. This normalization test additionally checks whether the *name itself* appears normalized in the echo output, regardless of whether the server acted on the value. +--- +title: "Underscore TE — Header Normalization" +description: "A POST request to /echo with a valid Content-Length: 11 for body framing, plus a malformed Transfer_Encoding: chunked header using an underscore instead of a hyphen." +weight: 5 +--- + +| | | +|---|---| +| **Test ID** | `NORM-UNDERSCORE-TE` | +| **Category** | Normalization | +| **Requirement** | Drop or reject | +| **Expected** | Reject/drop (pass), normalize (fail), preserve (warn) | + +## What it sends + +A POST request to `/echo` with a valid `Content-Length: 11` for body framing, plus a malformed `Transfer_Encoding: chunked` header using an underscore instead of a hyphen. + +```http +POST /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 11\r\n +Transfer_Encoding: chunked\r\n +\r\n +hello world +``` + +## What the RFC says + +Like `Content_Length`, the name `Transfer_Encoding` is a valid token per the `tchar` production but is not the standard `Transfer-Encoding` header. The security concern is whether the server maps the underscore variant to the real Transfer-Encoding header. + +## Pass / Fail / Warn + +**Pass:** Server rejects the request (`400`) or drops the `Transfer_Encoding` header. +**Fail:** Server normalizes `Transfer_Encoding` to `Transfer-Encoding` — this creates a CL/TE conflict. +**Warn:** Server preserves the original name — the echo shows `Transfer_Encoding: chunked`. + +## Why it matters + +This is the Transfer-Encoding counterpart to `NORM-UNDERSCORE-CL` and closely related to `SMUG-TRANSFER_ENCODING`. If a proxy passes `Transfer_Encoding: chunked` through without recognizing it, but the back-end normalizes it to `Transfer-Encoding: chunked`, the back-end will use chunked framing while the proxy used Content-Length. This is a textbook CL.TE smuggling vector. + +The existing `SMUG-TRANSFER_ENCODING` test checks if the server *processes* the underscore form. This normalization test additionally checks whether the *name itself* appears normalized in the echo output, regardless of whether the server acted on the value. diff --git a/docs/content/docs/request-line/405-allow.md b/docs/content/docs/request-line/405-allow.md index 618ad06..1ec8434 100644 --- a/docs/content/docs/request-line/405-allow.md +++ b/docs/content/docs/request-line/405-allow.md @@ -1,40 +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) +--- +title: "405 Allow — HTTP/1.1 Compliance" +description: "A DELETE request to the root path, which most servers do not support. This is intended to trigger a 405 response. Tested against RFC 9110 §15.5.6." +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 7950838..1ba93f4 100644 --- a/docs/content/docs/request-line/_index.md +++ b/docs/content/docs/request-line/_index.md @@ -1,6 +1,6 @@ --- title: Request Line -description: "Request Line — Http11Probe documentation" +description: "Request-line parsing tests covering method casing, multiple spaces, missing targets, fragments, and HTTP version validation per RFC 9112 Section 3." weight: 5 sidebar: open: false diff --git a/docs/content/docs/request-line/absolute-form.md b/docs/content/docs/request-line/absolute-form.md index 94cf060..2b0ce4a 100644 --- a/docs/content/docs/request-line/absolute-form.md +++ b/docs/content/docs/request-line/absolute-form.md @@ -1,87 +1,87 @@ ---- -title: "ABSOLUTE-FORM" -description: "ABSOLUTE-FORM test documentation" -weight: 9 ---- - -| | | -|---|---| -| **Test ID** | `COMP-ABSOLUTE-FORM` | -| **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** | `2xx` = Pass, `400` = Warn (unscored) | - -## What it sends - -`GET http://host/ HTTP/1.1` — the absolute-form request-target. - -```http -GET http://localhost:8080/ HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - - -## What the RFC says - -> "When making a request to a proxy, other than a CONNECT or server-wide OPTIONS request, a client MUST send the target URI in 'absolute-form' as the request-target." — RFC 9112 Section 3.2.2 - -> "A server MUST accept the absolute-form in requests even though most HTTP/1.1 clients will only send the absolute-form to a proxy." — RFC 9112 Section 3.2.2 - -## Why this test is unscored - -Although the RFC says servers MUST accept absolute-form, in practice most non-proxy origin servers reject it. Both `400` and `2xx` are observed in the wild, and rejecting absolute-form on an origin server is a common and generally harmless deviation. Since neither behavior is clearly wrong from a practical standpoint, this test records the response without scoring it. - -## Why it matters - -**Pass:** Server accepts with `2xx` (RFC-compliant). -**Warn:** Server rejects with `400` (common in practice, but non-compliant with MUST accept). - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -request-line = method SP request-target SP HTTP-version -request-target = origin-form / absolute-form / authority-form / asterisk-form -absolute-form = absolute-URI -``` - -The `absolute-form` production requires a complete `absolute-URI` as defined in RFC 3986. This is the full URI including scheme, authority, path, and optional query -- for example `http://host/path?query`. - -### RFC Evidence - -**RFC 9112 Section 3.2.2** mandates server acceptance: - -> "A server MUST accept the absolute-form in requests even though most HTTP/1.1 clients will only send the absolute-form to a proxy." — RFC 9112 Section 3.2.2 - -**RFC 9112 Section 3.2.2** describes the client-side usage: - -> "When making a request to a proxy, other than a CONNECT or server-wide OPTIONS request, a client MUST send the target URI in absolute-form as the request-target." -- RFC 9112 Section 3.2.2 - -**RFC 9112 Section 3.2.2** specifies Host header override behavior: - -> "When an origin server receives a request with an absolute-form of request-target, the origin server MUST ignore the received Host header field (if any) and instead use the host information of the request-target." -- RFC 9112 Section 3.2.2 - -### Chain of Reasoning - -1. The `absolute-form` production requires the full `absolute-URI` as the request-target, e.g., `GET http://host/ HTTP/1.1`. -2. Historically, only proxy-targeted requests used absolute-form. Clients sending directly to origin servers used origin-form (`/path`). -3. Despite this convention, the RFC contains a clear MUST: servers MUST accept absolute-form even from direct clients. This ensures interoperability across the request chain. -4. When absolute-form is received, the server MUST use the host from the request-target and ignore the Host header, which prevents host confusion attacks when both are present. -5. A server that rejects absolute-form with `400` is technically non-compliant with the MUST, but since most origin servers are not proxies, rejecting it is a common and pragmatically harmless behavior. - -### Scoring Justification - -**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 - -- **Mismatched Host and request-target authority:** If the absolute-form says `http://a.com/` but the Host header says `b.com`, the server MUST use `a.com` per the RFC. Servers that do not implement this override may route to the wrong virtual host. -- **Scheme mismatch:** A request arriving on HTTPS with `http://` in the absolute-form creates ambiguity about the intended scheme. The RFC does not address this directly. -- **Missing path:** `GET http://host HTTP/1.1` (no trailing slash) is a valid absolute-URI. The server should treat the empty path as `/`. - -## Sources - -- [RFC 9112 Section 3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2) +--- +title: "Absolute Form — HTTP/1.1 Compliance" +description: "GET http://host/ HTTP/1.1 — the absolute-form request-target. Tested against RFC 9112 Section 3.2.2." +weight: 9 +--- + +| | | +|---|---| +| **Test ID** | `COMP-ABSOLUTE-FORM` | +| **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** | `2xx` = Pass, `400` = Warn (unscored) | + +## What it sends + +`GET http://host/ HTTP/1.1` — the absolute-form request-target. + +```http +GET http://localhost:8080/ HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + + +## What the RFC says + +> "When making a request to a proxy, other than a CONNECT or server-wide OPTIONS request, a client MUST send the target URI in 'absolute-form' as the request-target." — RFC 9112 Section 3.2.2 + +> "A server MUST accept the absolute-form in requests even though most HTTP/1.1 clients will only send the absolute-form to a proxy." — RFC 9112 Section 3.2.2 + +## Why this test is unscored + +Although the RFC says servers MUST accept absolute-form, in practice most non-proxy origin servers reject it. Both `400` and `2xx` are observed in the wild, and rejecting absolute-form on an origin server is a common and generally harmless deviation. Since neither behavior is clearly wrong from a practical standpoint, this test records the response without scoring it. + +## Why it matters + +**Pass:** Server accepts with `2xx` (RFC-compliant). +**Warn:** Server rejects with `400` (common in practice, but non-compliant with MUST accept). + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +request-line = method SP request-target SP HTTP-version +request-target = origin-form / absolute-form / authority-form / asterisk-form +absolute-form = absolute-URI +``` + +The `absolute-form` production requires a complete `absolute-URI` as defined in RFC 3986. This is the full URI including scheme, authority, path, and optional query -- for example `http://host/path?query`. + +### RFC Evidence + +**RFC 9112 Section 3.2.2** mandates server acceptance: + +> "A server MUST accept the absolute-form in requests even though most HTTP/1.1 clients will only send the absolute-form to a proxy." — RFC 9112 Section 3.2.2 + +**RFC 9112 Section 3.2.2** describes the client-side usage: + +> "When making a request to a proxy, other than a CONNECT or server-wide OPTIONS request, a client MUST send the target URI in absolute-form as the request-target." -- RFC 9112 Section 3.2.2 + +**RFC 9112 Section 3.2.2** specifies Host header override behavior: + +> "When an origin server receives a request with an absolute-form of request-target, the origin server MUST ignore the received Host header field (if any) and instead use the host information of the request-target." -- RFC 9112 Section 3.2.2 + +### Chain of Reasoning + +1. The `absolute-form` production requires the full `absolute-URI` as the request-target, e.g., `GET http://host/ HTTP/1.1`. +2. Historically, only proxy-targeted requests used absolute-form. Clients sending directly to origin servers used origin-form (`/path`). +3. Despite this convention, the RFC contains a clear MUST: servers MUST accept absolute-form even from direct clients. This ensures interoperability across the request chain. +4. When absolute-form is received, the server MUST use the host from the request-target and ignore the Host header, which prevents host confusion attacks when both are present. +5. A server that rejects absolute-form with `400` is technically non-compliant with the MUST, but since most origin servers are not proxies, rejecting it is a common and pragmatically harmless behavior. + +### Scoring Justification + +**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 + +- **Mismatched Host and request-target authority:** If the absolute-form says `http://a.com/` but the Host header says `b.com`, the server MUST use `a.com` per the RFC. Servers that do not implement this override may route to the wrong virtual host. +- **Scheme mismatch:** A request arriving on HTTPS with `http://` in the absolute-form creates ambiguity about the intended scheme. The RFC does not address this directly. +- **Missing path:** `GET http://host HTTP/1.1` (no trailing slash) is a valid absolute-URI. The server should treat the empty path as `/`. + +## Sources + +- [RFC 9112 Section 3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2) diff --git a/docs/content/docs/request-line/asterisk-with-get.md b/docs/content/docs/request-line/asterisk-with-get.md index 1c5d7f0..20fa0b6 100644 --- a/docs/content/docs/request-line/asterisk-with-get.md +++ b/docs/content/docs/request-line/asterisk-with-get.md @@ -1,84 +1,84 @@ ---- -title: "ASTERISK-WITH-GET" -description: "ASTERISK-WITH-GET test documentation" -weight: 6 ---- - -| | | -|---|---| -| **Test ID** | `COMP-ASTERISK-WITH-GET` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 3.2.4](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.4) | -| **Requirement** | MUST only be used with OPTIONS | -| **Expected** | `400` or close | - -## What it sends - -`GET * HTTP/1.1` — the asterisk-form request-target with a non-OPTIONS method. - -```http -GET * HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - - -## What the RFC says - -> "The 'asterisk-form' of request-target is only used for a server-wide OPTIONS request." -- RFC 9112 §3.2.4 - -> "When a client wishes to request OPTIONS for the server as a whole, as opposed to a specific named resource of that server, the client MUST send only '*' (%x2A) as the request-target." -- RFC 9112 §3.2.4 - -Since the asterisk-form is defined exclusively for OPTIONS, using it with GET produces an invalid request-target for that method. - -## Why it matters - -Asterisk-form with any method other than OPTIONS is invalid. Accepting it could lead to unexpected server behavior. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -request-line = method SP request-target SP HTTP-version -request-target = origin-form / absolute-form / authority-form / asterisk-form -asterisk-form = "*" -``` - -The `asterisk-form` production is the literal character `*` (%x2A). The grammar allows it as one of four valid request-target forms, but its usage is constrained by prose in the RFC to a single method. - -### RFC Evidence - -**RFC 9112 Section 3.2.4** restricts asterisk-form to OPTIONS only: - -> "The 'asterisk-form' of request-target is only used for a server-wide OPTIONS request." -- RFC 9112 Section 3.2.4 - -**RFC 9112 Section 3.2.4** further reinforces the constraint with a client MUST: - -> "When a client wishes to request OPTIONS for the server as a whole, as opposed to a specific named resource of that server, the client MUST send only '*' (%x2A) as the request-target." -- RFC 9112 Section 3.2.4 - -**RFC 9112 Section 3** defines the invalid request-line handling: - -> "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 Section 3 - -### Chain of Reasoning - -1. The ABNF allows `asterisk-form` as a valid `request-target`, but the RFC prose restricts it: "only used for a server-wide OPTIONS request." -2. A request like `GET * HTTP/1.1` uses the asterisk-form with a method other than OPTIONS, violating this restriction. -3. Since the request-target form is invalid for the given method, the request-line is effectively malformed from the server's perspective. -4. The server SHOULD respond with `400` per the invalid request-line guidance in Section 3, or may close the connection. -5. The "only used for" phrasing is a definitional constraint rather than a MUST requirement, but a server that accepts `GET *` is processing a request with no meaningful target resource. - -### Scoring Justification - -**Scored (MUST).** The asterisk-form is definitionally restricted to OPTIONS. Sending `GET *` produces a request-target that has no valid interpretation under any method other than OPTIONS. A server that accepts this is processing a syntactically invalid request, which could lead to unpredictable behavior. The test expects `400` or connection close. - -### Edge Cases - -- **HEAD * HTTP/1.1:** HEAD is semantically identical to GET without a body. `HEAD *` is equally invalid since asterisk-form is restricted to OPTIONS. -- **POST * HTTP/1.1:** Even more problematic since POST with an asterisk target has no defined resource to act upon. -- **OPTIONS * HTTP/1.1:** This is the valid case -- the server should respond with `200` and applicable Allow/capability headers. - -## Sources - -- [RFC 9112 Section 3.2.4](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.4) +--- +title: "Asterisk With GET — HTTP/1.1 Compliance" +description: "GET * HTTP/1.1 — the asterisk-form request-target with a non-OPTIONS method. Tested against RFC 9112 Section 3.2.4." +weight: 6 +--- + +| | | +|---|---| +| **Test ID** | `COMP-ASTERISK-WITH-GET` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 3.2.4](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.4) | +| **Requirement** | MUST only be used with OPTIONS | +| **Expected** | `400` or close | + +## What it sends + +`GET * HTTP/1.1` — the asterisk-form request-target with a non-OPTIONS method. + +```http +GET * HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + + +## What the RFC says + +> "The 'asterisk-form' of request-target is only used for a server-wide OPTIONS request." -- RFC 9112 §3.2.4 + +> "When a client wishes to request OPTIONS for the server as a whole, as opposed to a specific named resource of that server, the client MUST send only '*' (%x2A) as the request-target." -- RFC 9112 §3.2.4 + +Since the asterisk-form is defined exclusively for OPTIONS, using it with GET produces an invalid request-target for that method. + +## Why it matters + +Asterisk-form with any method other than OPTIONS is invalid. Accepting it could lead to unexpected server behavior. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +request-line = method SP request-target SP HTTP-version +request-target = origin-form / absolute-form / authority-form / asterisk-form +asterisk-form = "*" +``` + +The `asterisk-form` production is the literal character `*` (%x2A). The grammar allows it as one of four valid request-target forms, but its usage is constrained by prose in the RFC to a single method. + +### RFC Evidence + +**RFC 9112 Section 3.2.4** restricts asterisk-form to OPTIONS only: + +> "The 'asterisk-form' of request-target is only used for a server-wide OPTIONS request." -- RFC 9112 Section 3.2.4 + +**RFC 9112 Section 3.2.4** further reinforces the constraint with a client MUST: + +> "When a client wishes to request OPTIONS for the server as a whole, as opposed to a specific named resource of that server, the client MUST send only '*' (%x2A) as the request-target." -- RFC 9112 Section 3.2.4 + +**RFC 9112 Section 3** defines the invalid request-line handling: + +> "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 Section 3 + +### Chain of Reasoning + +1. The ABNF allows `asterisk-form` as a valid `request-target`, but the RFC prose restricts it: "only used for a server-wide OPTIONS request." +2. A request like `GET * HTTP/1.1` uses the asterisk-form with a method other than OPTIONS, violating this restriction. +3. Since the request-target form is invalid for the given method, the request-line is effectively malformed from the server's perspective. +4. The server SHOULD respond with `400` per the invalid request-line guidance in Section 3, or may close the connection. +5. The "only used for" phrasing is a definitional constraint rather than a MUST requirement, but a server that accepts `GET *` is processing a request with no meaningful target resource. + +### Scoring Justification + +**Scored (MUST).** The asterisk-form is definitionally restricted to OPTIONS. Sending `GET *` produces a request-target that has no valid interpretation under any method other than OPTIONS. A server that accepts this is processing a syntactically invalid request, which could lead to unpredictable behavior. The test expects `400` or connection close. + +### Edge Cases + +- **HEAD * HTTP/1.1:** HEAD is semantically identical to GET without a body. `HEAD *` is equally invalid since asterisk-form is restricted to OPTIONS. +- **POST * HTTP/1.1:** Even more problematic since POST with an asterisk target has no defined resource to act upon. +- **OPTIONS * HTTP/1.1:** This is the valid case -- the server should respond with `200` and applicable Allow/capability headers. + +## Sources + +- [RFC 9112 Section 3.2.4](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.4) diff --git a/docs/content/docs/request-line/fragment-in-target.md b/docs/content/docs/request-line/fragment-in-target.md index 03df2ce..45a7425 100644 --- a/docs/content/docs/request-line/fragment-in-target.md +++ b/docs/content/docs/request-line/fragment-in-target.md @@ -1,104 +1,104 @@ ---- -title: "FRAGMENT-IN-TARGET" -description: "FRAGMENT-IN-TARGET test documentation" -weight: 3 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-3.2-FRAGMENT-IN-TARGET` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | -| **Requirement** | SHOULD | -| **Expected** | `400` = Pass; `2xx` = Warn | - -## What it sends - -A request with a fragment identifier in the URI: `GET /path#frag HTTP/1.1`. - -```http -GET /path#frag HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -The origin-form of request-target is defined as: - -> "origin-form = absolute-path [ '?' query ]" -- RFC 9112 §3.2.1 - -There is no fragment component in this grammar. The `#` character and anything after it are not part of any valid request-target form. RFC 9110 confirms that fragments are stripped before transmission: - -> "The target URI excludes the reference's fragment component, if any, since fragment identifiers are reserved for client-side processing." -- RFC 9110 §7.1 - -> "The fragment identifier component is not part of the scheme definition for a URI scheme (see Section 4.3 of [URI]), thus does not appear in the ABNF definitions for the 'http' and 'https' URI schemes." -- RFC 9110 §4.2.5 - -Since the request-line doesn't match any valid form, it is an invalid request-line: - -> "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 Section 3 - -This is a SHOULD, not a MUST -- servers that strip the fragment and process the path are not violating a mandatory requirement. - -## Why it matters - -Fragments are a client-side concept used to reference a position within a document. They should never appear on the wire. A server that silently strips fragments may process a different resource than what the client intended, though the practical security risk is low. - -**Pass:** Server rejects with `400` (strict parsing). -**Warn:** Server returns `2xx` (likely strips the fragment and processes `/path`). - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -request-line = method SP request-target SP HTTP-version -request-target = origin-form / absolute-form / authority-form / asterisk-form -origin-form = absolute-path [ "?" query ] -absolute-path = 1*( "/" segment ) -query = *( pchar / "/" / "?" ) -``` - -The `origin-form` production allows only an absolute path and an optional query component. There is no fragment production (`"#" fragment`) in the grammar. The `#` character is not a valid character in `absolute-path`, `query`, or any other component of `request-target`. - -### RFC Evidence - -**RFC 9112 Section 3.2.1** defines origin-form without fragments: - -> "origin-form = absolute-path [ '?' query ]" -- RFC 9112 Section 3.2.1 - -**RFC 9110 Section 4.2.5** confirms fragments are excluded from the URI scheme definition: - -> "The fragment identifier component is not part of the scheme definition for a URI scheme (see Section 4.3 of [URI]), thus does not appear in the ABNF definitions for the 'http' and 'https' URI schemes." -- RFC 9110 Section 4.2.5 - -**RFC 9110 Section 7.1** explicitly states fragments are stripped before transmission: - -> "The target URI excludes the reference's fragment component, if any, since fragment identifiers are reserved for client-side processing." -- RFC 9110 Section 7.1 - -**RFC 9112 Section 3** covers invalid request-line handling: - -> "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 Section 3 - -### Chain of Reasoning - -1. The origin-form ABNF is `absolute-path [ "?" query ]`. There is no fragment component in this grammar. -2. The `#` character does not appear in the `pchar`, `query`, or `absolute-path` productions from RFC 3986. It is a delimiter reserved exclusively for fragment identification. -3. RFC 9110 Section 7.1 confirms that the target URI "excludes the reference's fragment component" because "fragment identifiers are reserved for client-side processing." Fragments should never be transmitted on the wire. -4. A request-target of `/path#frag` does not match `origin-form` (or any other valid form), making it an invalid request-line per Section 3. -5. The handling guidance is SHOULD (not MUST): servers SHOULD respond with 400 or 301. A server that strips the fragment and processes `/path` is not violating a mandatory requirement. - -### Scoring Justification - -**Scored (SHOULD) -- Pass/Warn.** The RFC uses SHOULD, not MUST, for invalid request-line handling. A server that rejects with `400` demonstrates strict, correct parsing and earns a Pass. A server that returns `2xx` (likely stripping the fragment) is not violating a mandatory requirement but is being lenient with invalid input, earning a Warn. Neither outcome is a Fail. - -### Edge Cases - -- **Fragment with query:** `GET /path?q=1#frag HTTP/1.1` -- the `#frag` portion is still invalid in the request-target. The server may strip both the fragment and process `/path?q=1`, or reject entirely. -- **Empty fragment:** `GET /path# HTTP/1.1` -- the `#` alone (with no fragment text) is still invalid since `#` is not part of the origin-form grammar. -- **Percent-encoded hash:** `GET /path%23frag HTTP/1.1` -- `%23` is the percent-encoding of `#`. This is valid in the path and should be processed as the literal path `/path#frag` after decoding. The server should NOT treat this as a fragment. - -## Sources - -- [RFC 9112 Section 3.2 -- origin-form](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) -- [RFC 9112 Section 3 -- Request Line](https://www.rfc-editor.org/rfc/rfc9112#section-3) -- [RFC 9110 Section 4.1 -- URI References](https://www.rfc-editor.org/rfc/rfc9110#section-4.1) +--- +title: "Fragment In Target — HTTP/1.1 Compliance" +description: "A request with a fragment identifier in the URI: GET /path#frag HTTP/1.1. Tested against RFC 9112 Section 3.2." +weight: 3 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-3.2-FRAGMENT-IN-TARGET` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) | +| **Requirement** | SHOULD | +| **Expected** | `400` = Pass; `2xx` = Warn | + +## What it sends + +A request with a fragment identifier in the URI: `GET /path#frag HTTP/1.1`. + +```http +GET /path#frag HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +The origin-form of request-target is defined as: + +> "origin-form = absolute-path [ '?' query ]" -- RFC 9112 §3.2.1 + +There is no fragment component in this grammar. The `#` character and anything after it are not part of any valid request-target form. RFC 9110 confirms that fragments are stripped before transmission: + +> "The target URI excludes the reference's fragment component, if any, since fragment identifiers are reserved for client-side processing." -- RFC 9110 §7.1 + +> "The fragment identifier component is not part of the scheme definition for a URI scheme (see Section 4.3 of [URI]), thus does not appear in the ABNF definitions for the 'http' and 'https' URI schemes." -- RFC 9110 §4.2.5 + +Since the request-line doesn't match any valid form, it is an invalid request-line: + +> "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 Section 3 + +This is a SHOULD, not a MUST -- servers that strip the fragment and process the path are not violating a mandatory requirement. + +## Why it matters + +Fragments are a client-side concept used to reference a position within a document. They should never appear on the wire. A server that silently strips fragments may process a different resource than what the client intended, though the practical security risk is low. + +**Pass:** Server rejects with `400` (strict parsing). +**Warn:** Server returns `2xx` (likely strips the fragment and processes `/path`). + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +request-line = method SP request-target SP HTTP-version +request-target = origin-form / absolute-form / authority-form / asterisk-form +origin-form = absolute-path [ "?" query ] +absolute-path = 1*( "/" segment ) +query = *( pchar / "/" / "?" ) +``` + +The `origin-form` production allows only an absolute path and an optional query component. There is no fragment production (`"#" fragment`) in the grammar. The `#` character is not a valid character in `absolute-path`, `query`, or any other component of `request-target`. + +### RFC Evidence + +**RFC 9112 Section 3.2.1** defines origin-form without fragments: + +> "origin-form = absolute-path [ '?' query ]" -- RFC 9112 Section 3.2.1 + +**RFC 9110 Section 4.2.5** confirms fragments are excluded from the URI scheme definition: + +> "The fragment identifier component is not part of the scheme definition for a URI scheme (see Section 4.3 of [URI]), thus does not appear in the ABNF definitions for the 'http' and 'https' URI schemes." -- RFC 9110 Section 4.2.5 + +**RFC 9110 Section 7.1** explicitly states fragments are stripped before transmission: + +> "The target URI excludes the reference's fragment component, if any, since fragment identifiers are reserved for client-side processing." -- RFC 9110 Section 7.1 + +**RFC 9112 Section 3** covers invalid request-line handling: + +> "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 Section 3 + +### Chain of Reasoning + +1. The origin-form ABNF is `absolute-path [ "?" query ]`. There is no fragment component in this grammar. +2. The `#` character does not appear in the `pchar`, `query`, or `absolute-path` productions from RFC 3986. It is a delimiter reserved exclusively for fragment identification. +3. RFC 9110 Section 7.1 confirms that the target URI "excludes the reference's fragment component" because "fragment identifiers are reserved for client-side processing." Fragments should never be transmitted on the wire. +4. A request-target of `/path#frag` does not match `origin-form` (or any other valid form), making it an invalid request-line per Section 3. +5. The handling guidance is SHOULD (not MUST): servers SHOULD respond with 400 or 301. A server that strips the fragment and processes `/path` is not violating a mandatory requirement. + +### Scoring Justification + +**Scored (SHOULD) -- Pass/Warn.** The RFC uses SHOULD, not MUST, for invalid request-line handling. A server that rejects with `400` demonstrates strict, correct parsing and earns a Pass. A server that returns `2xx` (likely stripping the fragment) is not violating a mandatory requirement but is being lenient with invalid input, earning a Warn. Neither outcome is a Fail. + +### Edge Cases + +- **Fragment with query:** `GET /path?q=1#frag HTTP/1.1` -- the `#frag` portion is still invalid in the request-target. The server may strip both the fragment and process `/path?q=1`, or reject entirely. +- **Empty fragment:** `GET /path# HTTP/1.1` -- the `#` alone (with no fragment text) is still invalid since `#` is not part of the origin-form grammar. +- **Percent-encoded hash:** `GET /path%23frag HTTP/1.1` -- `%23` is the percent-encoding of `#`. This is valid in the path and should be processed as the literal path `/path#frag` after decoding. The server should NOT treat this as a fragment. + +## Sources + +- [RFC 9112 Section 3.2 -- origin-form](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) +- [RFC 9112 Section 3 -- Request Line](https://www.rfc-editor.org/rfc/rfc9112#section-3) +- [RFC 9110 Section 4.1 -- URI References](https://www.rfc-editor.org/rfc/rfc9110#section-4.1) diff --git a/docs/content/docs/request-line/head-no-body.md b/docs/content/docs/request-line/head-no-body.md index 58227cf..85cfebc 100644 --- a/docs/content/docs/request-line/head-no-body.md +++ b/docs/content/docs/request-line/head-no-body.md @@ -1,37 +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) +--- +title: "HEAD No Body — HTTP/1.1 Compliance" +description: "A standard HEAD request. The server must respond with headers only — no message body. Tested against RFC 9110 §9.3.2." +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/http09-request.md b/docs/content/docs/request-line/http09-request.md index 042cef2..e894982 100644 --- a/docs/content/docs/request-line/http09-request.md +++ b/docs/content/docs/request-line/http09-request.md @@ -1,89 +1,89 @@ ---- -title: "HTTP09-REQUEST" -description: "HTTP09-REQUEST test documentation" -weight: 5 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-2.3-HTTP09-REQUEST` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | -| **Requirement** | SHOULD | -| **Expected** | `400`, close, or timeout | - -## What it sends - -An HTTP/0.9 style request: `GET /\r\n` — a method and target with no HTTP version. - -```http -GET /\r\n -``` - -No HTTP version, no headers, no blank line — just a raw path terminated by CRLF. - - -## What the RFC says - -The request-line grammar requires a version string: - -> "request-line = method SP request-target SP HTTP-version" -- RFC 9112 §3 - -Without a version, the request-line does not match this grammar. It is an invalid request-line: - -> "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 Section 3 - -The server may also not recognize this as a request at all, hence timeout is acceptable. - -## Why it matters - -HTTP/0.9 was a protocol from 1991 with no headers, no status codes, and no Content-Length. It has no place in modern infrastructure. A server that attempts to process HTTP/0.9 requests is exposing legacy attack surface. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -request-line = method SP request-target SP HTTP-version CRLF -HTTP-version = HTTP-name "/" DIGIT "." DIGIT -HTTP-name = %s"HTTP" -``` - -The `request-line` grammar requires three components separated by spaces: `method`, `request-target`, and `HTTP-version`, terminated by CRLF. An HTTP/0.9 request (`GET /\r\n`) contains only two tokens (method and target) with no version string, and is immediately terminated by CRLF. - -### RFC Evidence - -**RFC 9112 Section 3** defines the required request-line structure: - -> "request-line = method SP request-target SP HTTP-version" -- RFC 9112 Section 3 - -**RFC 9112 Section 2.3** defines the HTTP-version format and its role: - -> "HTTP uses a '.' numbering scheme to indicate versions of the protocol. This specification defines version '1.1'." -- RFC 9112 Section 2.3 - -**RFC 9112 Section 3** provides guidance for invalid request-lines: - -> "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 Section 3 - -### Chain of Reasoning - -1. The request-line ABNF requires exactly three space-delimited tokens: method, request-target, and HTTP-version. `GET /\r\n` has only two tokens. -2. Without an HTTP-version token, the message does not match the `request-line` production at all. It is structurally incomplete. -3. HTTP/0.9 was a 1991-era protocol that predated headers, status codes, and content framing. RFC 9112 does not define support for HTTP/0.9 -- the version field is mandatory. -4. A server encountering `GET /\r\n` without further data faces an ambiguity: it could be an incomplete HTTP/1.x request (more data coming) or a complete HTTP/0.9 request. Waiting for more data (timeout) is a legitimate interpretation. -5. The SHOULD from Section 3 applies: servers SHOULD respond with `400`. Connection close and timeout are also acceptable since the server may not recognize this as a valid HTTP message at all. - -### Scoring Justification - -**Scored (SHOULD) -- Pass/Warn.** The RFC uses SHOULD for invalid request-line handling. Since `GET /\r\n` is clearly an invalid request-line (missing the mandatory HTTP-version), responding with `400` earns a Pass. A connection close or timeout is also acceptable because the server may be waiting for the rest of a partially received HTTP/1.x request-line. A server that processes this as an HTTP/0.9 request and returns content without headers would be a Warn, since it is serving a deprecated protocol with no framing. - -### Edge Cases - -- **No CRLF terminator:** A raw `GET /` with no line terminator may cause the server to wait indefinitely for the rest of the request-line, resulting in a timeout. This is acceptable behavior. -- **HTTP/0.9 POST:** `POST /\r\n` is even more dangerous than `GET /` because HTTP/0.9 had no concept of request bodies. A server that processes this may read subsequent data as a new request. -- **Pipeline after HTTP/0.9:** If an attacker sends `GET /\r\nGET /secret HTTP/1.1\r\n...`, a server that processes the first line as HTTP/0.9 and then reads the second as a new HTTP/1.1 request could be tricked into request smuggling. - -## Sources - -- [RFC 9112 Section 2.3 -- HTTP Version](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) -- [RFC 9112 Section 3 -- Request Line](https://www.rfc-editor.org/rfc/rfc9112#section-3) +--- +title: "HTTP/0.9 Request — HTTP/1.1 Compliance" +description: "An HTTP/0.9 style request: GET /\\r\\n — a method and target with no HTTP version. Tested against RFC 9112 Section 2.3." +weight: 5 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-2.3-HTTP09-REQUEST` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | +| **Requirement** | SHOULD | +| **Expected** | `400`, close, or timeout | + +## What it sends + +An HTTP/0.9 style request: `GET /\r\n` — a method and target with no HTTP version. + +```http +GET /\r\n +``` + +No HTTP version, no headers, no blank line — just a raw path terminated by CRLF. + + +## What the RFC says + +The request-line grammar requires a version string: + +> "request-line = method SP request-target SP HTTP-version" -- RFC 9112 §3 + +Without a version, the request-line does not match this grammar. It is an invalid request-line: + +> "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 Section 3 + +The server may also not recognize this as a request at all, hence timeout is acceptable. + +## Why it matters + +HTTP/0.9 was a protocol from 1991 with no headers, no status codes, and no Content-Length. It has no place in modern infrastructure. A server that attempts to process HTTP/0.9 requests is exposing legacy attack surface. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +request-line = method SP request-target SP HTTP-version CRLF +HTTP-version = HTTP-name "/" DIGIT "." DIGIT +HTTP-name = %s"HTTP" +``` + +The `request-line` grammar requires three components separated by spaces: `method`, `request-target`, and `HTTP-version`, terminated by CRLF. An HTTP/0.9 request (`GET /\r\n`) contains only two tokens (method and target) with no version string, and is immediately terminated by CRLF. + +### RFC Evidence + +**RFC 9112 Section 3** defines the required request-line structure: + +> "request-line = method SP request-target SP HTTP-version" -- RFC 9112 Section 3 + +**RFC 9112 Section 2.3** defines the HTTP-version format and its role: + +> "HTTP uses a '.' numbering scheme to indicate versions of the protocol. This specification defines version '1.1'." -- RFC 9112 Section 2.3 + +**RFC 9112 Section 3** provides guidance for invalid request-lines: + +> "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 Section 3 + +### Chain of Reasoning + +1. The request-line ABNF requires exactly three space-delimited tokens: method, request-target, and HTTP-version. `GET /\r\n` has only two tokens. +2. Without an HTTP-version token, the message does not match the `request-line` production at all. It is structurally incomplete. +3. HTTP/0.9 was a 1991-era protocol that predated headers, status codes, and content framing. RFC 9112 does not define support for HTTP/0.9 -- the version field is mandatory. +4. A server encountering `GET /\r\n` without further data faces an ambiguity: it could be an incomplete HTTP/1.x request (more data coming) or a complete HTTP/0.9 request. Waiting for more data (timeout) is a legitimate interpretation. +5. The SHOULD from Section 3 applies: servers SHOULD respond with `400`. Connection close and timeout are also acceptable since the server may not recognize this as a valid HTTP message at all. + +### Scoring Justification + +**Scored (SHOULD) -- Pass/Warn.** The RFC uses SHOULD for invalid request-line handling. Since `GET /\r\n` is clearly an invalid request-line (missing the mandatory HTTP-version), responding with `400` earns a Pass. A connection close or timeout is also acceptable because the server may be waiting for the rest of a partially received HTTP/1.x request-line. A server that processes this as an HTTP/0.9 request and returns content without headers would be a Warn, since it is serving a deprecated protocol with no framing. + +### Edge Cases + +- **No CRLF terminator:** A raw `GET /` with no line terminator may cause the server to wait indefinitely for the rest of the request-line, resulting in a timeout. This is acceptable behavior. +- **HTTP/0.9 POST:** `POST /\r\n` is even more dangerous than `GET /` because HTTP/0.9 had no concept of request bodies. A server that processes this may read subsequent data as a new request. +- **Pipeline after HTTP/0.9:** If an attacker sends `GET /\r\nGET /secret HTTP/1.1\r\n...`, a server that processes the first line as HTTP/0.9 and then reads the second as a new HTTP/1.1 request could be tricked into request smuggling. + +## Sources + +- [RFC 9112 Section 2.3 -- HTTP Version](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) +- [RFC 9112 Section 3 -- Request Line](https://www.rfc-editor.org/rfc/rfc9112#section-3) diff --git a/docs/content/docs/request-line/http12-version.md b/docs/content/docs/request-line/http12-version.md index 5ad00d1..55d7470 100644 --- a/docs/content/docs/request-line/http12-version.md +++ b/docs/content/docs/request-line/http12-version.md @@ -1,91 +1,91 @@ ---- -title: "HTTP12-VERSION" -description: "HTTP12-VERSION test documentation" -weight: 19 ---- - -| | | -|---|---| -| **Test ID** | `COMP-HTTP12-VERSION` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | -| **Requirement** | MAY (unscored) | -| **Expected** | `200` or `505` = Warn | - -## What it sends - -A request using HTTP version 1.2, which does not exist but has a higher minor version than 1.1. - -```http -GET / HTTP/1.2\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "A recipient that receives a message with a major version number that it implements and a minor version number higher than what it implements SHOULD process the message as if it were in the highest minor version within that major version to which the recipient is conformant." — RFC 9110 Section 2.5 - -> "The 505 (HTTP Version Not Supported) status code indicates that the server does not support, or refuses to support, the major version of HTTP that was used in the request message." — RFC 9110 Section 15.6.6 - -A server implementing HTTP/1.1 that receives `HTTP/1.2` should treat it as HTTP/1.1 and process normally. The server may also respond with `505 HTTP Version Not Supported` if it chooses not to handle unrecognized minor versions. - -**Warn:** Server responds `200` (correctly processes as HTTP/1.1) or `505` (refuses the minor version). Both are acceptable behaviors. - -## Why this test is unscored - -The RFC uses SHOULD (not MUST) for processing higher minor versions, and `505` is an explicitly permitted alternative. Since both `200` and `505` are valid responses, and even `400` (while strict) does not represent a security risk, this test records behavior without scoring it. - -## Why it matters - -Forward compatibility is a core design principle of HTTP versioning. Minor version increments within the same major version should not break communication. A server that rejects `HTTP/1.2` with a `400` instead of processing it as `HTTP/1.1` or returning `505` has an overly strict version parser that may break when clients or proxies use future HTTP/1.x versions. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -HTTP-version = HTTP-name "/" DIGIT "." DIGIT -HTTP-name = %s"HTTP" -``` - -The `HTTP-version` production accepts any single digit for both major and minor version numbers. `HTTP/1.2` is syntactically valid -- it matches `HTTP-name "/" DIGIT "." DIGIT` where major=1 and minor=2. The question is not syntax but semantics: how should a server handle a recognized major version with an unrecognized minor version? - -### RFC Evidence - -**RFC 9110 Section 2.5** provides the forward-compatibility guidance: - -> "A recipient that receives a message with a major version number that it implements and a minor version number higher than what it implements SHOULD process the message as if it were in the highest minor version within that major version to which the recipient is conformant." -- RFC 9110 Section 2.5 - -**RFC 9110 Section 2.5** also permits version refusal: - -> "A server can send a 505 (HTTP Version Not Supported) response if it wishes, for any reason, to refuse service of the client's major protocol version." -- RFC 9110 Section 2.5 - -**RFC 9110 Section 15.6.6** defines the 505 status code: - -> "The 505 (HTTP Version Not Supported) status code indicates that the server does not support, or refuses to support, the major version of HTTP that was used in the request message." -- RFC 9110 Section 15.6.6 - -### Chain of Reasoning - -1. `HTTP/1.2` is syntactically valid per the `HTTP-version` ABNF. The major version (1) matches what the server implements; only the minor version (2) is unrecognized. -2. The RFC uses SHOULD for the forward-compatibility behavior: the server SHOULD treat `HTTP/1.2` as `HTTP/1.1` and process the message normally. This is the ideal behavior for forward compatibility. -3. The server MAY alternatively respond with `505`, which is explicitly permitted by Section 2.5. While `505` is technically for refusing "the client's major protocol version," servers are given latitude ("for any reason"). -4. A server that returns `400` is being overly strict -- the version string is syntactically valid and the major version matches. This indicates a brittle version parser. -5. Since the RFC uses SHOULD (not MUST) and provides `505` as a legitimate alternative, there is no single required behavior. - -### Scoring Justification - -**Unscored (MAY).** The RFC provides two explicitly acceptable paths: process as HTTP/1.1 (SHOULD) or respond with 505 (MAY). Neither path is mandated with MUST. Even a `400` response, while indicating an overly strict parser, does not represent a security vulnerability. The test records behavior as Warn for informational purposes without penalizing any outcome. - -### Edge Cases - -- **HTTP/1.0:** A server implementing HTTP/1.1 that receives `HTTP/1.0` should process it under HTTP/1.0 semantics. This is a downgrade, not an upgrade, and is well-defined behavior. -- **HTTP/2.0 in HTTP/1.1 syntax:** `GET / HTTP/2.0\r\n` -- major version 2 is a different protocol. The server should respond with `505` since it does not implement HTTP/2 over this wire format (HTTP/2 uses a different framing mechanism). -- **HTTP/1.9:** Same situation as HTTP/1.2. The server should treat it as HTTP/1.1. The single-digit minor version means the highest possible is HTTP/1.9. -- **Intermediary version forwarding:** RFC 9112 Section 2.3 states intermediaries "MUST send their own HTTP-version in forwarded messages." A proxy should not forward `HTTP/1.2` -- it should downgrade to `HTTP/1.1`. - -## Sources - -- [RFC 9112 §2.3 -- HTTP Version](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) -- [RFC 9110 Section 2.5 -- Protocol Version](https://www.rfc-editor.org/rfc/rfc9110#section-2.5) -- [RFC 9110 Section 15.6.6 -- 505 HTTP Version Not Supported](https://www.rfc-editor.org/rfc/rfc9110#section-15.6.6) +--- +title: "HTTP/1.2 Version — HTTP/1.1 Compliance" +description: "A request using HTTP version 1.2, which does not exist but has a higher minor version than 1.1. Tested against RFC 9112 §2.3." +weight: 19 +--- + +| | | +|---|---| +| **Test ID** | `COMP-HTTP12-VERSION` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | +| **Requirement** | MAY (unscored) | +| **Expected** | `200` or `505` = Warn | + +## What it sends + +A request using HTTP version 1.2, which does not exist but has a higher minor version than 1.1. + +```http +GET / HTTP/1.2\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "A recipient that receives a message with a major version number that it implements and a minor version number higher than what it implements SHOULD process the message as if it were in the highest minor version within that major version to which the recipient is conformant." — RFC 9110 Section 2.5 + +> "The 505 (HTTP Version Not Supported) status code indicates that the server does not support, or refuses to support, the major version of HTTP that was used in the request message." — RFC 9110 Section 15.6.6 + +A server implementing HTTP/1.1 that receives `HTTP/1.2` should treat it as HTTP/1.1 and process normally. The server may also respond with `505 HTTP Version Not Supported` if it chooses not to handle unrecognized minor versions. + +**Warn:** Server responds `200` (correctly processes as HTTP/1.1) or `505` (refuses the minor version). Both are acceptable behaviors. + +## Why this test is unscored + +The RFC uses SHOULD (not MUST) for processing higher minor versions, and `505` is an explicitly permitted alternative. Since both `200` and `505` are valid responses, and even `400` (while strict) does not represent a security risk, this test records behavior without scoring it. + +## Why it matters + +Forward compatibility is a core design principle of HTTP versioning. Minor version increments within the same major version should not break communication. A server that rejects `HTTP/1.2` with a `400` instead of processing it as `HTTP/1.1` or returning `505` has an overly strict version parser that may break when clients or proxies use future HTTP/1.x versions. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +HTTP-version = HTTP-name "/" DIGIT "." DIGIT +HTTP-name = %s"HTTP" +``` + +The `HTTP-version` production accepts any single digit for both major and minor version numbers. `HTTP/1.2` is syntactically valid -- it matches `HTTP-name "/" DIGIT "." DIGIT` where major=1 and minor=2. The question is not syntax but semantics: how should a server handle a recognized major version with an unrecognized minor version? + +### RFC Evidence + +**RFC 9110 Section 2.5** provides the forward-compatibility guidance: + +> "A recipient that receives a message with a major version number that it implements and a minor version number higher than what it implements SHOULD process the message as if it were in the highest minor version within that major version to which the recipient is conformant." -- RFC 9110 Section 2.5 + +**RFC 9110 Section 2.5** also permits version refusal: + +> "A server can send a 505 (HTTP Version Not Supported) response if it wishes, for any reason, to refuse service of the client's major protocol version." -- RFC 9110 Section 2.5 + +**RFC 9110 Section 15.6.6** defines the 505 status code: + +> "The 505 (HTTP Version Not Supported) status code indicates that the server does not support, or refuses to support, the major version of HTTP that was used in the request message." -- RFC 9110 Section 15.6.6 + +### Chain of Reasoning + +1. `HTTP/1.2` is syntactically valid per the `HTTP-version` ABNF. The major version (1) matches what the server implements; only the minor version (2) is unrecognized. +2. The RFC uses SHOULD for the forward-compatibility behavior: the server SHOULD treat `HTTP/1.2` as `HTTP/1.1` and process the message normally. This is the ideal behavior for forward compatibility. +3. The server MAY alternatively respond with `505`, which is explicitly permitted by Section 2.5. While `505` is technically for refusing "the client's major protocol version," servers are given latitude ("for any reason"). +4. A server that returns `400` is being overly strict -- the version string is syntactically valid and the major version matches. This indicates a brittle version parser. +5. Since the RFC uses SHOULD (not MUST) and provides `505` as a legitimate alternative, there is no single required behavior. + +### Scoring Justification + +**Unscored (MAY).** The RFC provides two explicitly acceptable paths: process as HTTP/1.1 (SHOULD) or respond with 505 (MAY). Neither path is mandated with MUST. Even a `400` response, while indicating an overly strict parser, does not represent a security vulnerability. The test records behavior as Warn for informational purposes without penalizing any outcome. + +### Edge Cases + +- **HTTP/1.0:** A server implementing HTTP/1.1 that receives `HTTP/1.0` should process it under HTTP/1.0 semantics. This is a downgrade, not an upgrade, and is well-defined behavior. +- **HTTP/2.0 in HTTP/1.1 syntax:** `GET / HTTP/2.0\r\n` -- major version 2 is a different protocol. The server should respond with `505` since it does not implement HTTP/2 over this wire format (HTTP/2 uses a different framing mechanism). +- **HTTP/1.9:** Same situation as HTTP/1.2. The server should treat it as HTTP/1.1. The single-digit minor version means the highest possible is HTTP/1.9. +- **Intermediary version forwarding:** RFC 9112 Section 2.3 states intermediaries "MUST send their own HTTP-version in forwarded messages." A proxy should not forward `HTTP/1.2` -- it should downgrade to `HTTP/1.1`. + +## Sources + +- [RFC 9112 §2.3 -- HTTP Version](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) +- [RFC 9110 Section 2.5 -- Protocol Version](https://www.rfc-editor.org/rfc/rfc9110#section-2.5) +- [RFC 9110 Section 15.6.6 -- 505 HTTP Version Not Supported](https://www.rfc-editor.org/rfc/rfc9110#section-15.6.6) diff --git a/docs/content/docs/request-line/invalid-version.md b/docs/content/docs/request-line/invalid-version.md index 459be38..f865b63 100644 --- a/docs/content/docs/request-line/invalid-version.md +++ b/docs/content/docs/request-line/invalid-version.md @@ -1,97 +1,97 @@ ---- -title: "INVALID-VERSION" -description: "INVALID-VERSION test documentation" -weight: 4 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-2.3-INVALID-VERSION` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | -| **Requirement** | No MUST | -| **Expected** | `400`, `505`, or close | - -## What it sends - -A request with an unrecognizable HTTP version string, e.g., `GET / HTTP/9.9`. - -```http -GET / HTTP/9.9\r\n -Host: localhost:8080\r\n -\r\n -``` - - -## What the RFC says - -The HTTP-version grammar is strict and case-sensitive: - -> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 §2.3 - -> "HTTP-version is case-sensitive." -- RFC 9112 §2.3 - -`HTTP/9.9` matches the grammar syntactically (single digit, dot, single digit), but it uses a major version that the server does not implement. The server can refuse it: - -> "A server can send a 505 (HTTP Version Not Supported) response if it wishes, for any reason, to refuse service of the client's major protocol version." — RFC 9110 Section 2.5 - -> "The 505 (HTTP Version Not Supported) status code indicates that the server does not support, or refuses to support, the major version of HTTP that was used in the request message." -- RFC 9110 §15.6.6 - -There is no MUST-level requirement for a specific response to invalid versions. - -## Why it matters - -An unrecognized major version like `HTTP/9.9` means the server cannot determine the client's protocol capabilities. Accepting such a request could lead to applying incorrect framing rules, connection semantics, or feature assumptions. Rejecting with `400` or `505` is the safest response. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -request-line = method SP request-target SP HTTP-version CRLF -HTTP-version = HTTP-name "/" DIGIT "." DIGIT -HTTP-name = %s"HTTP" -``` - -The `HTTP-version` production is syntactically permissive: any `HTTP/X.Y` where X and Y are single digits is grammatically valid. `HTTP/9.9` satisfies the ABNF. The issue is purely semantic -- major version 9 does not exist and the server cannot determine what protocol rules to apply. - -### RFC Evidence - -**RFC 9112 Section 2.3** defines the version grammar: - -> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 Section 2.3 - -**RFC 9112 Section 2.3** states version handling is case-sensitive: - -> "HTTP-version is case-sensitive." -- RFC 9112 Section 2.3 - -**RFC 9110 Section 15.6.6** defines the 505 status code: - -> "The 505 (HTTP Version Not Supported) status code indicates that the server does not support, or refuses to support, the major version of HTTP that was used in the request message." -- RFC 9110 Section 15.6.6 - -**RFC 9110 Section 2.5** permits version refusal: - -> "A server can send a 505 (HTTP Version Not Supported) response if it wishes, for any reason, to refuse service of the client's major protocol version." -- RFC 9110 Section 2.5 - -### Chain of Reasoning - -1. `HTTP/9.9` is syntactically valid per the `HTTP-version` ABNF -- it is `HTTP-name "/" DIGIT "." DIGIT` with major=9 and minor=9. -2. Unlike `HTTP/1.2` (same major, higher minor), `HTTP/9.9` uses a major version the server does not implement. The forward-compatibility SHOULD from Section 2.5 only applies when the major version matches. -3. The server has no knowledge of HTTP/9.x semantics -- it cannot safely assume any framing, header, or connection rules. Processing the message as HTTP/1.1 would be a guess. -4. `505` is the purpose-built response for this situation. `400` is also reasonable since the server considers the version unsupported. Connection close is acceptable as a last resort. -5. There is no MUST-level requirement for any specific response. The RFC provides `505` as a SHOULD and connection close as an implicit option. - -### Scoring Justification - -**Scored (no MUST) -- Pass/Warn.** There is no mandatory behavior defined for unrecognized major versions. The test accepts `400`, `505`, or connection close as passing outcomes. A server that returns `200` would be concerning -- it would mean the server is blindly processing a request from a completely unknown protocol version, which could lead to incorrect framing or security assumptions. - -### Edge Cases - -- **HTTP/0.9 as version string:** `GET / HTTP/0.9\r\n` -- syntactically valid per the ABNF (unlike the HTTP/0.9 protocol which has no version string). The server should reject with `505` since HTTP/0.x is not implemented. -- **HTTP/3.0 over TCP:** `GET / HTTP/3.0\r\n` -- HTTP/3 is a real protocol but runs over QUIC, not TCP. Receiving it over a TCP connection is invalid. `505` is appropriate. -- **Malformed versions:** `HTTP/1.` (missing minor digit) or `HTTP/11.1` (multi-digit major) do not match the ABNF at all and are invalid request-lines, not just unsupported versions. - -## Sources - -- [RFC 9112 Section 2.3 -- HTTP Version](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) -- [RFC 9110 Section 15.6.6 -- 505 HTTP Version Not Supported](https://www.rfc-editor.org/rfc/rfc9110#section-15.6.6) +--- +title: "Invalid Version — HTTP/1.1 Compliance" +description: "A request with an unrecognizable HTTP version string, e.g., GET / HTTP/9.9. Tested against RFC 9112 Section 2.3." +weight: 4 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-2.3-INVALID-VERSION` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | +| **Requirement** | No MUST | +| **Expected** | `400`, `505`, or close | + +## What it sends + +A request with an unrecognizable HTTP version string, e.g., `GET / HTTP/9.9`. + +```http +GET / HTTP/9.9\r\n +Host: localhost:8080\r\n +\r\n +``` + + +## What the RFC says + +The HTTP-version grammar is strict and case-sensitive: + +> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 §2.3 + +> "HTTP-version is case-sensitive." -- RFC 9112 §2.3 + +`HTTP/9.9` matches the grammar syntactically (single digit, dot, single digit), but it uses a major version that the server does not implement. The server can refuse it: + +> "A server can send a 505 (HTTP Version Not Supported) response if it wishes, for any reason, to refuse service of the client's major protocol version." — RFC 9110 Section 2.5 + +> "The 505 (HTTP Version Not Supported) status code indicates that the server does not support, or refuses to support, the major version of HTTP that was used in the request message." -- RFC 9110 §15.6.6 + +There is no MUST-level requirement for a specific response to invalid versions. + +## Why it matters + +An unrecognized major version like `HTTP/9.9` means the server cannot determine the client's protocol capabilities. Accepting such a request could lead to applying incorrect framing rules, connection semantics, or feature assumptions. Rejecting with `400` or `505` is the safest response. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +request-line = method SP request-target SP HTTP-version CRLF +HTTP-version = HTTP-name "/" DIGIT "." DIGIT +HTTP-name = %s"HTTP" +``` + +The `HTTP-version` production is syntactically permissive: any `HTTP/X.Y` where X and Y are single digits is grammatically valid. `HTTP/9.9` satisfies the ABNF. The issue is purely semantic -- major version 9 does not exist and the server cannot determine what protocol rules to apply. + +### RFC Evidence + +**RFC 9112 Section 2.3** defines the version grammar: + +> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 Section 2.3 + +**RFC 9112 Section 2.3** states version handling is case-sensitive: + +> "HTTP-version is case-sensitive." -- RFC 9112 Section 2.3 + +**RFC 9110 Section 15.6.6** defines the 505 status code: + +> "The 505 (HTTP Version Not Supported) status code indicates that the server does not support, or refuses to support, the major version of HTTP that was used in the request message." -- RFC 9110 Section 15.6.6 + +**RFC 9110 Section 2.5** permits version refusal: + +> "A server can send a 505 (HTTP Version Not Supported) response if it wishes, for any reason, to refuse service of the client's major protocol version." -- RFC 9110 Section 2.5 + +### Chain of Reasoning + +1. `HTTP/9.9` is syntactically valid per the `HTTP-version` ABNF -- it is `HTTP-name "/" DIGIT "." DIGIT` with major=9 and minor=9. +2. Unlike `HTTP/1.2` (same major, higher minor), `HTTP/9.9` uses a major version the server does not implement. The forward-compatibility SHOULD from Section 2.5 only applies when the major version matches. +3. The server has no knowledge of HTTP/9.x semantics -- it cannot safely assume any framing, header, or connection rules. Processing the message as HTTP/1.1 would be a guess. +4. `505` is the purpose-built response for this situation. `400` is also reasonable since the server considers the version unsupported. Connection close is acceptable as a last resort. +5. There is no MUST-level requirement for any specific response. The RFC provides `505` as a SHOULD and connection close as an implicit option. + +### Scoring Justification + +**Scored (no MUST) -- Pass/Warn.** There is no mandatory behavior defined for unrecognized major versions. The test accepts `400`, `505`, or connection close as passing outcomes. A server that returns `200` would be concerning -- it would mean the server is blindly processing a request from a completely unknown protocol version, which could lead to incorrect framing or security assumptions. + +### Edge Cases + +- **HTTP/0.9 as version string:** `GET / HTTP/0.9\r\n` -- syntactically valid per the ABNF (unlike the HTTP/0.9 protocol which has no version string). The server should reject with `505` since HTTP/0.x is not implemented. +- **HTTP/3.0 over TCP:** `GET / HTTP/3.0\r\n` -- HTTP/3 is a real protocol but runs over QUIC, not TCP. Receiving it over a TCP connection is invalid. `505` is appropriate. +- **Malformed versions:** `HTTP/1.` (missing minor digit) or `HTTP/11.1` (multi-digit major) do not match the ABNF at all and are invalid request-lines, not just unsupported versions. + +## Sources + +- [RFC 9112 Section 2.3 -- HTTP Version](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) +- [RFC 9110 Section 15.6.6 -- 505 HTTP Version Not Supported](https://www.rfc-editor.org/rfc/rfc9110#section-15.6.6) diff --git a/docs/content/docs/request-line/long-url-ok.md b/docs/content/docs/request-line/long-url-ok.md index cb1b34c..a4c0a25 100644 --- a/docs/content/docs/request-line/long-url-ok.md +++ b/docs/content/docs/request-line/long-url-ok.md @@ -1,48 +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) +--- +title: "Long URL OK — HTTP/1.1 Compliance" +description: "A GET request with a ~7900-character path (well under 8000 octets total for the request-line). Tested against RFC 9112 §3." +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/method-case.md b/docs/content/docs/request-line/method-case.md index 6633109..77b2170 100644 --- a/docs/content/docs/request-line/method-case.md +++ b/docs/content/docs/request-line/method-case.md @@ -1,95 +1,95 @@ ---- -title: "METHOD-CASE" -description: "METHOD-CASE test documentation" -weight: 10 ---- - -| | | -|---|---| -| **Test ID** | `COMP-METHOD-CASE` | -| **Category** | Compliance | -| **RFC** | [RFC 9110 Section 9.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.1) | -| **Requirement** | Case-sensitive | -| **Expected** | `400`/`405`/`501` or `2xx` | - -## What it sends - -`get / HTTP/1.1` — lowercase method name. - -```http -get / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -The method `get` is lowercase instead of the standard `GET`. - - -## What the RFC says - -> "The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names. By convention, standardized methods are defined in all-uppercase US-ASCII letters." — RFC 9110 Section 9.1 - -> "The request method is case-sensitive." — RFC 9112 Section 3.1 - -## Why this test is unscored - -The RFC states that method tokens are case-sensitive, but there is no MUST-level requirement that servers reject lowercase methods. Many servers accept `get` as equivalent to `GET` in practice. Both behaviors are common, and neither represents a security risk -- this test observes the behavior without scoring it. - -## Why it matters - -The method token is case-sensitive by definition. A server that rejects `get` is strictly correct. A server that accepts `get` treats methods case-insensitively, which works in practice but deviates from the spec. - -**Pass:** Server rejects with `400`, `405`, or `501` (strict case-sensitive parsing). -**Warn:** Server accepts with `2xx` (case-insensitive, common in practice). - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -request-line = method SP request-target SP HTTP-version -method = token -token = 1*tchar -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" - / DIGIT / ALPHA -``` - -The `method` production is a `token`, which includes both uppercase and lowercase `ALPHA` characters. This means `get` is syntactically valid as a method token -- it matches the ABNF. The case-sensitivity constraint comes from the semantic layer, not the grammar. - -### RFC Evidence - -**RFC 9110 Section 9.1** defines method case sensitivity and its rationale: - -> "The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names. By convention, standardized methods are defined in all-uppercase US-ASCII letters." -- RFC 9110 Section 9.1 - -**RFC 9112 Section 3.1** reinforces that the method is case-sensitive: - -> "The request method is case-sensitive." -- RFC 9112 Section 3.1 - -**RFC 9110 Section 9.1** describes method registration: - -> "The method token is case-sensitive and ought to be registered within the 'Hypertext Transfer Protocol (HTTP) Method Registry'." -- RFC 9110 Section 9.1 - -### Chain of Reasoning - -1. The `method` ABNF production (`token`) permits lowercase letters. `get` is syntactically valid. -2. However, both RFC 9110 Section 9.1 and RFC 9112 Section 3.1 state that the method token is case-sensitive. `get` and `GET` are therefore different method tokens. -3. `GET` is a registered, standardized method. `get` is not registered in the IANA HTTP Method Registry. A server receiving `get` is receiving an unregistered method. -4. The RFC does not contain a MUST requirement to reject unrecognized methods. RFC 9110 Section 15.6.2 defines `501 Not Implemented` for methods the server does not recognize, and `405 Method Not Allowed` for methods not supported on the target resource. -5. Many servers treat methods case-insensitively as a pragmatic choice. This works because all standardized methods have unique uppercase names and there is no registered lowercase method that collides. - -### Scoring Justification - -**Unscored (no MUST).** The RFC defines method tokens as case-sensitive but does not mandate rejection of unrecognized (lowercase) methods with any specific behavior. A server that rejects `get` with `400`, `405`, or `501` is strictly correct. A server that accepts `get` as `GET` is being lenient but not violating a MUST requirement. Both behaviors are recorded without scoring. - -### Edge Cases - -- **Mixed case methods:** `Get`, `gEt`, `GEt` -- each is a distinct token under case-sensitive rules. None are registered methods. -- **WebDAV methods:** Methods like `PROPFIND` are registered in uppercase. A server handling `propfind` as `PROPFIND` may work but is technically receiving an unregistered method. -- **Custom methods:** If a server defines a custom method `get` (lowercase) for internal use, it would collide with case-insensitive handling of `GET`. This is the exact scenario the RFC warns about with "gateway to object-based systems." - -## Sources - -- [RFC 9110 Section 9.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.1) -- [RFC 9112 Section 3.1 -- Method](https://www.rfc-editor.org/rfc/rfc9112#section-3.1) +--- +title: "Method Case — HTTP/1.1 Compliance" +description: "Get / HTTP/1.1 — lowercase method name. Tested against RFC 9110 Section 9.1." +weight: 10 +--- + +| | | +|---|---| +| **Test ID** | `COMP-METHOD-CASE` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 Section 9.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.1) | +| **Requirement** | Case-sensitive | +| **Expected** | `400`/`405`/`501` or `2xx` | + +## What it sends + +`get / HTTP/1.1` — lowercase method name. + +```http +get / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +The method `get` is lowercase instead of the standard `GET`. + + +## What the RFC says + +> "The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names. By convention, standardized methods are defined in all-uppercase US-ASCII letters." — RFC 9110 Section 9.1 + +> "The request method is case-sensitive." — RFC 9112 Section 3.1 + +## Why this test is unscored + +The RFC states that method tokens are case-sensitive, but there is no MUST-level requirement that servers reject lowercase methods. Many servers accept `get` as equivalent to `GET` in practice. Both behaviors are common, and neither represents a security risk -- this test observes the behavior without scoring it. + +## Why it matters + +The method token is case-sensitive by definition. A server that rejects `get` is strictly correct. A server that accepts `get` treats methods case-insensitively, which works in practice but deviates from the spec. + +**Pass:** Server rejects with `400`, `405`, or `501` (strict case-sensitive parsing). +**Warn:** Server accepts with `2xx` (case-insensitive, common in practice). + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +request-line = method SP request-target SP HTTP-version +method = token +token = 1*tchar +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + / DIGIT / ALPHA +``` + +The `method` production is a `token`, which includes both uppercase and lowercase `ALPHA` characters. This means `get` is syntactically valid as a method token -- it matches the ABNF. The case-sensitivity constraint comes from the semantic layer, not the grammar. + +### RFC Evidence + +**RFC 9110 Section 9.1** defines method case sensitivity and its rationale: + +> "The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names. By convention, standardized methods are defined in all-uppercase US-ASCII letters." -- RFC 9110 Section 9.1 + +**RFC 9112 Section 3.1** reinforces that the method is case-sensitive: + +> "The request method is case-sensitive." -- RFC 9112 Section 3.1 + +**RFC 9110 Section 9.1** describes method registration: + +> "The method token is case-sensitive and ought to be registered within the 'Hypertext Transfer Protocol (HTTP) Method Registry'." -- RFC 9110 Section 9.1 + +### Chain of Reasoning + +1. The `method` ABNF production (`token`) permits lowercase letters. `get` is syntactically valid. +2. However, both RFC 9110 Section 9.1 and RFC 9112 Section 3.1 state that the method token is case-sensitive. `get` and `GET` are therefore different method tokens. +3. `GET` is a registered, standardized method. `get` is not registered in the IANA HTTP Method Registry. A server receiving `get` is receiving an unregistered method. +4. The RFC does not contain a MUST requirement to reject unrecognized methods. RFC 9110 Section 15.6.2 defines `501 Not Implemented` for methods the server does not recognize, and `405 Method Not Allowed` for methods not supported on the target resource. +5. Many servers treat methods case-insensitively as a pragmatic choice. This works because all standardized methods have unique uppercase names and there is no registered lowercase method that collides. + +### Scoring Justification + +**Unscored (no MUST).** The RFC defines method tokens as case-sensitive but does not mandate rejection of unrecognized (lowercase) methods with any specific behavior. A server that rejects `get` with `400`, `405`, or `501` is strictly correct. A server that accepts `get` as `GET` is being lenient but not violating a MUST requirement. Both behaviors are recorded without scoring. + +### Edge Cases + +- **Mixed case methods:** `Get`, `gEt`, `GEt` -- each is a distinct token under case-sensitive rules. None are registered methods. +- **WebDAV methods:** Methods like `PROPFIND` are registered in uppercase. A server handling `propfind` as `PROPFIND` may work but is technically receiving an unregistered method. +- **Custom methods:** If a server defines a custom method `get` (lowercase) for internal use, it would collide with case-insensitive handling of `GET`. This is the exact scenario the RFC warns about with "gateway to object-based systems." + +## Sources + +- [RFC 9110 Section 9.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.1) +- [RFC 9112 Section 3.1 -- Method](https://www.rfc-editor.org/rfc/rfc9112#section-3.1) diff --git a/docs/content/docs/request-line/method-connect.md b/docs/content/docs/request-line/method-connect.md index 6ef1a93..cadbf0e 100644 --- a/docs/content/docs/request-line/method-connect.md +++ b/docs/content/docs/request-line/method-connect.md @@ -1,87 +1,87 @@ ---- -title: "METHOD-CONNECT" -description: "METHOD-CONNECT test documentation" -weight: 12 ---- - -| | | -|---|---| -| **Test ID** | `COMP-METHOD-CONNECT` | -| **Category** | Compliance | -| **RFC** | [RFC 9110 Section 9.3.6](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.6) | -| **Requirement** | origin server SHOULD reject | -| **Expected** | `400`, `405`, `501`, or close | - -## What it sends - -`CONNECT example.com:443 HTTP/1.1` — a CONNECT request sent directly to an origin server (not a proxy). - -```http -CONNECT example.com:443 HTTP/1.1\r\n -Host: example.com:443\r\n -\r\n -``` - - -## What the RFC says - -> "The CONNECT method requests that the recipient establish a tunnel to the destination origin server identified by the request target and, if successful, thereafter restrict its behavior to blind forwarding of data, in both directions, until the tunnel is closed." -- RFC 9110 §9.3.6 - -> "CONNECT is intended for use in requests to a proxy." -- RFC 9110 §9.3.6 - -> "An origin server MAY accept a CONNECT request, but most origin servers do not implement CONNECT." -- RFC 9110 §9.3.6 - -Origin servers are not proxies. They have no reason to accept CONNECT and establish a TCP tunnel. - -## Why it matters - -If an origin server accepts CONNECT, it effectively becomes an open proxy. This can be exploited for port scanning internal networks, bypassing firewalls, or pivoting attacks through the server. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -request-line = method SP request-target SP HTTP-version -method = token -authority-form = uri-host ":" port -``` - -CONNECT is one of eight standardized HTTP methods. Unlike all other methods, CONNECT uses `authority-form` for its request-target rather than `origin-form`. The method itself is a `token` and is case-sensitive. - -### RFC Evidence - -**RFC 9110 Section 9.3.6** defines the purpose of CONNECT: - -> "The CONNECT method requests that the recipient establish a tunnel to the destination origin server identified by the request target and, if successful, thereafter restrict its behavior to blind forwarding of data, in both directions, until the tunnel is closed." -- RFC 9110 Section 9.3.6 - -**RFC 9110 Section 9.3.6** states the intended usage context: - -> "CONNECT is intended for use in requests to a proxy." -- RFC 9110 Section 9.3.6 - -**RFC 9110 Section 9.3.6** acknowledges origin server handling: - -> "An origin server MAY accept a CONNECT request, but most origin servers do not implement CONNECT." -- RFC 9110 Section 9.3.6 - -### Chain of Reasoning - -1. CONNECT is designed for proxy-to-proxy or client-to-proxy communication. Its purpose is to establish a TCP tunnel through an intermediary to a destination server. -2. The RFC explicitly states CONNECT "is intended for use in requests to a proxy." Sending CONNECT to an origin server is outside the designed use case. -3. An origin server MAY accept CONNECT, but this is a permissive allowance, not an expectation. The RFC acknowledges "most origin servers do not implement CONNECT." -4. An origin server that accepts CONNECT effectively becomes a proxy, enabling tunnel creation to arbitrary hosts. This is a significant security concern: the server could be used for port scanning, firewall bypass, or SSRF. -5. The safest behavior is to reject CONNECT with `405 Method Not Allowed` (method exists but is not applicable), `501 Not Implemented` (method not recognized), or `400 Bad Request`. - -### Scoring Justification - -**Scored (SHOULD).** The RFC uses "intended for use in requests to a proxy" and "MAY accept," making it clear that origin servers are not expected to support CONNECT. While there is no MUST to reject, accepting CONNECT on an origin server has severe security implications -- it turns the server into an open proxy. The test expects rejection (`400`, `405`, `501`, or close) and treats acceptance as a failure due to the security risk. - -### Edge Cases - -- **CONNECT to self:** `CONNECT localhost:8080 HTTP/1.1` sent to the server on port 8080. If accepted, the server would establish a tunnel to itself, potentially enabling request smuggling through the tunnel. -- **CONNECT to internal networks:** `CONNECT 10.0.0.1:6379 HTTP/1.1` -- if the origin server accepts this, it becomes a gateway to internal Redis instances or other services behind the firewall. -- **CONNECT with body:** RFC 9110 Section 9.3.6 states that a CONNECT request has no defined body semantics. A server that reads body data from a CONNECT request may misparse the tunnel data. -- **2xx response:** A `200` response to CONNECT means the server has agreed to establish a tunnel. After sending `200`, the server is expected to blindly forward bytes. This is catastrophic on an origin server. - -## Sources - -- [RFC 9110 Section 9.3.6](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.6) +--- +title: "Method CONNECT — HTTP/1.1 Compliance" +description: "CONNECT example.com:443 HTTP/1.1 — a CONNECT request sent directly to an origin server (not a proxy). Tested against RFC 9110 Section 9.3.6." +weight: 12 +--- + +| | | +|---|---| +| **Test ID** | `COMP-METHOD-CONNECT` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 Section 9.3.6](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.6) | +| **Requirement** | origin server SHOULD reject | +| **Expected** | `400`, `405`, `501`, or close | + +## What it sends + +`CONNECT example.com:443 HTTP/1.1` — a CONNECT request sent directly to an origin server (not a proxy). + +```http +CONNECT example.com:443 HTTP/1.1\r\n +Host: example.com:443\r\n +\r\n +``` + + +## What the RFC says + +> "The CONNECT method requests that the recipient establish a tunnel to the destination origin server identified by the request target and, if successful, thereafter restrict its behavior to blind forwarding of data, in both directions, until the tunnel is closed." -- RFC 9110 §9.3.6 + +> "CONNECT is intended for use in requests to a proxy." -- RFC 9110 §9.3.6 + +> "An origin server MAY accept a CONNECT request, but most origin servers do not implement CONNECT." -- RFC 9110 §9.3.6 + +Origin servers are not proxies. They have no reason to accept CONNECT and establish a TCP tunnel. + +## Why it matters + +If an origin server accepts CONNECT, it effectively becomes an open proxy. This can be exploited for port scanning internal networks, bypassing firewalls, or pivoting attacks through the server. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +request-line = method SP request-target SP HTTP-version +method = token +authority-form = uri-host ":" port +``` + +CONNECT is one of eight standardized HTTP methods. Unlike all other methods, CONNECT uses `authority-form` for its request-target rather than `origin-form`. The method itself is a `token` and is case-sensitive. + +### RFC Evidence + +**RFC 9110 Section 9.3.6** defines the purpose of CONNECT: + +> "The CONNECT method requests that the recipient establish a tunnel to the destination origin server identified by the request target and, if successful, thereafter restrict its behavior to blind forwarding of data, in both directions, until the tunnel is closed." -- RFC 9110 Section 9.3.6 + +**RFC 9110 Section 9.3.6** states the intended usage context: + +> "CONNECT is intended for use in requests to a proxy." -- RFC 9110 Section 9.3.6 + +**RFC 9110 Section 9.3.6** acknowledges origin server handling: + +> "An origin server MAY accept a CONNECT request, but most origin servers do not implement CONNECT." -- RFC 9110 Section 9.3.6 + +### Chain of Reasoning + +1. CONNECT is designed for proxy-to-proxy or client-to-proxy communication. Its purpose is to establish a TCP tunnel through an intermediary to a destination server. +2. The RFC explicitly states CONNECT "is intended for use in requests to a proxy." Sending CONNECT to an origin server is outside the designed use case. +3. An origin server MAY accept CONNECT, but this is a permissive allowance, not an expectation. The RFC acknowledges "most origin servers do not implement CONNECT." +4. An origin server that accepts CONNECT effectively becomes a proxy, enabling tunnel creation to arbitrary hosts. This is a significant security concern: the server could be used for port scanning, firewall bypass, or SSRF. +5. The safest behavior is to reject CONNECT with `405 Method Not Allowed` (method exists but is not applicable), `501 Not Implemented` (method not recognized), or `400 Bad Request`. + +### Scoring Justification + +**Scored (SHOULD).** The RFC uses "intended for use in requests to a proxy" and "MAY accept," making it clear that origin servers are not expected to support CONNECT. While there is no MUST to reject, accepting CONNECT on an origin server has severe security implications -- it turns the server into an open proxy. The test expects rejection (`400`, `405`, `501`, or close) and treats acceptance as a failure due to the security risk. + +### Edge Cases + +- **CONNECT to self:** `CONNECT localhost:8080 HTTP/1.1` sent to the server on port 8080. If accepted, the server would establish a tunnel to itself, potentially enabling request smuggling through the tunnel. +- **CONNECT to internal networks:** `CONNECT 10.0.0.1:6379 HTTP/1.1` -- if the origin server accepts this, it becomes a gateway to internal Redis instances or other services behind the firewall. +- **CONNECT with body:** RFC 9110 Section 9.3.6 states that a CONNECT request has no defined body semantics. A server that reads body data from a CONNECT request may misparse the tunnel data. +- **2xx response:** A `200` response to CONNECT means the server has agreed to establish a tunnel. After sending `200`, the server is expected to blindly forward bytes. This is catastrophic on an origin server. + +## Sources + +- [RFC 9110 Section 9.3.6](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.6) diff --git a/docs/content/docs/request-line/method-trace.md b/docs/content/docs/request-line/method-trace.md index 7fbc766..7ba08a1 100644 --- a/docs/content/docs/request-line/method-trace.md +++ b/docs/content/docs/request-line/method-trace.md @@ -1,85 +1,85 @@ ---- -title: "METHOD-TRACE" -description: "METHOD-TRACE test documentation" -weight: 14 ---- - -| | | -|---|---| -| **Test ID** | `COMP-METHOD-TRACE` | -| **Category** | Compliance | -| **RFC** | [RFC 9110 Section 9.3.8](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.8) | -| **Requirement** | SHOULD disable | -| **Expected** | `405` or `501` preferred; `200` is a warning | - -## What it sends - -`TRACE / HTTP/1.1` — a standard TRACE request. - -```http -TRACE / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - - -## What the RFC says - -> "The TRACE method requests a remote, application-level loop-back of the request message. The final recipient of the request SHOULD reflect the message received, excluding some fields described below, back to the client as the content of a 200 (OK) response." -- RFC 9110 §9.3.8 - -> "A client MUST NOT generate fields in a TRACE request containing sensitive data that might be disclosed by the response." -- RFC 9110 §9.3.8 - -> "The final recipient of the request SHOULD exclude any request fields that are likely to contain sensitive data when that recipient generates the response content." -- RFC 9110 §9.3.8 - -TRACE echoes the received request back to the client. While valid per the HTTP spec, it is widely considered a security risk in production. - -## Why this test is unscored - -The RFC does not mandate that servers reject TRACE; it merely defines the method's behavior. Disabling TRACE is a security best practice rather than an RFC conformance requirement. Both `405`/`501` (disabled) and `200` (enabled) are valid behaviors. - -## Why it matters - -TRACE can be abused for **Cross-Site Tracing (XST)** attacks — if an attacker can trigger a TRACE request (via XSS or other means), the echoed response may expose cookies, authorization headers, or other sensitive data that `HttpOnly` flags are meant to protect. - -Most security hardening guides recommend disabling TRACE entirely. A `405 Method Not Allowed` or `501 Not Implemented` response is ideal. - -## Deep Analysis - -### Relevant ABNF - -``` -request-line = method SP request-target SP HTTP-version -method = token -``` - -The TRACE method is a valid `token` per the HTTP grammar, and `TRACE / HTTP/1.1` is a syntactically well-formed request-line under RFC 9112 Section 3. - -### RFC Evidence - -RFC 9110 Section 9.3.8 defines the TRACE method and its purpose: - -> "The TRACE method requests a remote, application-level loop-back of the request message. The final recipient of the request SHOULD reflect the message received, excluding some fields described below, back to the client as the content of a 200 (OK) response." -- RFC 9110 Section 9.3.8 - -The specification acknowledges the security sensitivity of the echoed response: - -> "A client MUST NOT generate 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 user credentials or cookies in a TRACE request. The final recipient of the request SHOULD exclude any request fields that are likely to contain sensitive data when that recipient generates the response content." -- RFC 9110 Section 9.3.8 - -The value of TRACE for diagnostics is also noted: - -> "TRACE allows the client to see what is being received at the other end of the request chain and use that data for testing or diagnostic information." -- RFC 9110 Section 9.3.8 - -### Chain of Reasoning - -1. TRACE is a defined HTTP method, so a server is not required to reject it on syntactic grounds. -2. However, TRACE echoes back request headers, which creates a reflected data channel. If an attacker can trigger a TRACE request (e.g., via XSS), the response may expose `HttpOnly` cookies, `Authorization` headers, and other credentials that would otherwise be inaccessible to JavaScript -- the classic **Cross-Site Tracing (XST)** attack vector. -3. The RFC uses SHOULD language for reflecting the message, not MUST -- servers are not obligated to implement TRACE at all. Industry-wide security hardening guides (OWASP, CIS benchmarks) uniformly recommend disabling TRACE in production. -4. A `405 Method Not Allowed` or `501 Not Implemented` response demonstrates the server has been hardened against XST. A `200` response means TRACE is active and the reflected-data attack surface exists. - -### Scoring Justification - -This test is **unscored** (warning-level). The RFC does not mandate that servers reject TRACE; it merely defines the method's behavior. Disabling TRACE is a security best practice rather than an RFC conformance requirement. Therefore, `405`/`501` = **Pass** (hardened), and `200` = **Warn** (functional but exposes attack surface). - -## Sources - -- [RFC 9110 Section 9.3.8](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.8) -- [OWASP: Test HTTP Methods](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/06-Test_HTTP_Methods) +--- +title: "Method TRACE — HTTP/1.1 Compliance" +description: "TRACE / HTTP/1.1 — a standard TRACE request. Tested against RFC 9110 Section 9.3.8." +weight: 14 +--- + +| | | +|---|---| +| **Test ID** | `COMP-METHOD-TRACE` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 Section 9.3.8](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.8) | +| **Requirement** | SHOULD disable | +| **Expected** | `405` or `501` preferred; `200` is a warning | + +## What it sends + +`TRACE / HTTP/1.1` — a standard TRACE request. + +```http +TRACE / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + + +## What the RFC says + +> "The TRACE method requests a remote, application-level loop-back of the request message. The final recipient of the request SHOULD reflect the message received, excluding some fields described below, back to the client as the content of a 200 (OK) response." -- RFC 9110 §9.3.8 + +> "A client MUST NOT generate fields in a TRACE request containing sensitive data that might be disclosed by the response." -- RFC 9110 §9.3.8 + +> "The final recipient of the request SHOULD exclude any request fields that are likely to contain sensitive data when that recipient generates the response content." -- RFC 9110 §9.3.8 + +TRACE echoes the received request back to the client. While valid per the HTTP spec, it is widely considered a security risk in production. + +## Why this test is unscored + +The RFC does not mandate that servers reject TRACE; it merely defines the method's behavior. Disabling TRACE is a security best practice rather than an RFC conformance requirement. Both `405`/`501` (disabled) and `200` (enabled) are valid behaviors. + +## Why it matters + +TRACE can be abused for **Cross-Site Tracing (XST)** attacks — if an attacker can trigger a TRACE request (via XSS or other means), the echoed response may expose cookies, authorization headers, or other sensitive data that `HttpOnly` flags are meant to protect. + +Most security hardening guides recommend disabling TRACE entirely. A `405 Method Not Allowed` or `501 Not Implemented` response is ideal. + +## Deep Analysis + +### Relevant ABNF + +``` +request-line = method SP request-target SP HTTP-version +method = token +``` + +The TRACE method is a valid `token` per the HTTP grammar, and `TRACE / HTTP/1.1` is a syntactically well-formed request-line under RFC 9112 Section 3. + +### RFC Evidence + +RFC 9110 Section 9.3.8 defines the TRACE method and its purpose: + +> "The TRACE method requests a remote, application-level loop-back of the request message. The final recipient of the request SHOULD reflect the message received, excluding some fields described below, back to the client as the content of a 200 (OK) response." -- RFC 9110 Section 9.3.8 + +The specification acknowledges the security sensitivity of the echoed response: + +> "A client MUST NOT generate 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 user credentials or cookies in a TRACE request. The final recipient of the request SHOULD exclude any request fields that are likely to contain sensitive data when that recipient generates the response content." -- RFC 9110 Section 9.3.8 + +The value of TRACE for diagnostics is also noted: + +> "TRACE allows the client to see what is being received at the other end of the request chain and use that data for testing or diagnostic information." -- RFC 9110 Section 9.3.8 + +### Chain of Reasoning + +1. TRACE is a defined HTTP method, so a server is not required to reject it on syntactic grounds. +2. However, TRACE echoes back request headers, which creates a reflected data channel. If an attacker can trigger a TRACE request (e.g., via XSS), the response may expose `HttpOnly` cookies, `Authorization` headers, and other credentials that would otherwise be inaccessible to JavaScript -- the classic **Cross-Site Tracing (XST)** attack vector. +3. The RFC uses SHOULD language for reflecting the message, not MUST -- servers are not obligated to implement TRACE at all. Industry-wide security hardening guides (OWASP, CIS benchmarks) uniformly recommend disabling TRACE in production. +4. A `405 Method Not Allowed` or `501 Not Implemented` response demonstrates the server has been hardened against XST. A `200` response means TRACE is active and the reflected-data attack surface exists. + +### Scoring Justification + +This test is **unscored** (warning-level). The RFC does not mandate that servers reject TRACE; it merely defines the method's behavior. Disabling TRACE is a security best practice rather than an RFC conformance requirement. Therefore, `405`/`501` = **Pass** (hardened), and `200` = **Warn** (functional but exposes attack surface). + +## Sources + +- [RFC 9110 Section 9.3.8](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.8) +- [OWASP: Test HTTP Methods](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/06-Test_HTTP_Methods) diff --git a/docs/content/docs/request-line/missing-target.md b/docs/content/docs/request-line/missing-target.md index b451f66..aec44fa 100644 --- a/docs/content/docs/request-line/missing-target.md +++ b/docs/content/docs/request-line/missing-target.md @@ -1,81 +1,81 @@ ---- -title: "MISSING-TARGET" -description: "MISSING-TARGET test documentation" -weight: 2 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-3-MISSING-TARGET` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 3](https://www.rfc-editor.org/rfc/rfc9112#section-3) | -| **Requirement** | MUST (grammar violation) | -| **Expected** | `400` or close | - -## What it sends - -A request-line with no request-target: `GET HTTP/1.1` (method directly followed by version, no URI). - -```http -GET HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -The request-target (path) is missing — `GET` is followed directly by the version string with no path in between. The server is likely to parse `HTTP/1.1` as the request-target, leaving no version field at all. - -## What the RFC says - -The request-target is a required component of the request-line grammar: - -> "request-line = method SP request-target SP HTTP-version" — RFC 9112 Section 3 - -Without a request-target, the line does not match the grammar and is invalid: - -> "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 Section 3 - -## Why it matters - -A missing request-target produces a fundamentally malformed request-line. The server cannot determine what resource the client intended to access. Strict rejection prevents ambiguous parsing where `HTTP/1.1` could be misinterpreted as the target path. - -## Deep Analysis - -### Relevant ABNF - -``` -request-line = method SP request-target SP HTTP-version -request-target = origin-form / absolute-form / authority-form / asterisk-form -origin-form = absolute-path [ "?" query ] -``` - -The request-line grammar mandates three components separated by two `SP` octets. When the request-target is absent, the line contains only two whitespace-delimited tokens instead of three, and no token in the line matches any `request-target` production. - -### RFC Evidence - -The grammar itself is the primary evidence -- `request-target` is not optional: - -> "A request-line begins with a method token, followed by a single space (SP), the request-target, and another single space (SP), and ends with the protocol version." -- RFC 9112 Section 3 - -When the request-target is missing, the server sees `GET HTTP/1.1` which a whitespace-delimited parser would split into just two tokens: `GET` as the method and `HTTP/1.1` as the request-target, with no version field at all. The RFC is explicit about how to handle such invalid lines: - -> "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 Section 3 - -The security rationale for strict rejection is also stated: - -> "A recipient SHOULD NOT attempt to autocorrect and then process the request without a redirect, since the invalid request-line might be deliberately crafted to bypass security filters along the request chain." — RFC 9112 Section 3 - -### Chain of Reasoning - -1. The ABNF `request-line = method SP request-target SP HTTP-version` requires exactly three whitespace-separated components. With the request-target missing, the line `GET HTTP/1.1` has only two. -2. A lenient parser splitting on whitespace boundaries would read `GET` as method and `HTTP/1.1` as the request-target, with no version token remaining. The version would be indeterminate. -3. Without a valid HTTP-version, the server cannot determine the protocol level. This makes the request fundamentally unparseable in a spec-compliant way. -4. Even if the server guesses the intent, doing so would mask a potentially malicious probe -- the RFC explicitly warns against autocorrection. -5. Because the grammar violation is unambiguous, both `400` and connection close are appropriate responses. - -### Scoring Justification - -This test is **scored**. The request-line grammar is a MUST-level requirement (the grammar itself is normative), and a missing component produces an invalid request-line. The RFC's SHOULD recommendation for `400` on invalid request-lines makes rejection the expected behavior. `400` or close = **Pass**, any other response = **Fail**. - -## Sources - -- [RFC 9112 Section 3 -- Request Line](https://www.rfc-editor.org/rfc/rfc9112#section-3) +--- +title: "Missing Target — HTTP/1.1 Compliance" +description: "A request-line with no request-target: GET HTTP/1.1 (method directly followed by version, no URI). Tested against RFC 9112 Section 3." +weight: 2 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-3-MISSING-TARGET` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 3](https://www.rfc-editor.org/rfc/rfc9112#section-3) | +| **Requirement** | MUST (grammar violation) | +| **Expected** | `400` or close | + +## What it sends + +A request-line with no request-target: `GET HTTP/1.1` (method directly followed by version, no URI). + +```http +GET HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +The request-target (path) is missing — `GET` is followed directly by the version string with no path in between. The server is likely to parse `HTTP/1.1` as the request-target, leaving no version field at all. + +## What the RFC says + +The request-target is a required component of the request-line grammar: + +> "request-line = method SP request-target SP HTTP-version" — RFC 9112 Section 3 + +Without a request-target, the line does not match the grammar and is invalid: + +> "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 Section 3 + +## Why it matters + +A missing request-target produces a fundamentally malformed request-line. The server cannot determine what resource the client intended to access. Strict rejection prevents ambiguous parsing where `HTTP/1.1` could be misinterpreted as the target path. + +## Deep Analysis + +### Relevant ABNF + +``` +request-line = method SP request-target SP HTTP-version +request-target = origin-form / absolute-form / authority-form / asterisk-form +origin-form = absolute-path [ "?" query ] +``` + +The request-line grammar mandates three components separated by two `SP` octets. When the request-target is absent, the line contains only two whitespace-delimited tokens instead of three, and no token in the line matches any `request-target` production. + +### RFC Evidence + +The grammar itself is the primary evidence -- `request-target` is not optional: + +> "A request-line begins with a method token, followed by a single space (SP), the request-target, and another single space (SP), and ends with the protocol version." -- RFC 9112 Section 3 + +When the request-target is missing, the server sees `GET HTTP/1.1` which a whitespace-delimited parser would split into just two tokens: `GET` as the method and `HTTP/1.1` as the request-target, with no version field at all. The RFC is explicit about how to handle such invalid lines: + +> "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 Section 3 + +The security rationale for strict rejection is also stated: + +> "A recipient SHOULD NOT attempt to autocorrect and then process the request without a redirect, since the invalid request-line might be deliberately crafted to bypass security filters along the request chain." — RFC 9112 Section 3 + +### Chain of Reasoning + +1. The ABNF `request-line = method SP request-target SP HTTP-version` requires exactly three whitespace-separated components. With the request-target missing, the line `GET HTTP/1.1` has only two. +2. A lenient parser splitting on whitespace boundaries would read `GET` as method and `HTTP/1.1` as the request-target, with no version token remaining. The version would be indeterminate. +3. Without a valid HTTP-version, the server cannot determine the protocol level. This makes the request fundamentally unparseable in a spec-compliant way. +4. Even if the server guesses the intent, doing so would mask a potentially malicious probe -- the RFC explicitly warns against autocorrection. +5. Because the grammar violation is unambiguous, both `400` and connection close are appropriate responses. + +### Scoring Justification + +This test is **scored**. The request-line grammar is a MUST-level requirement (the grammar itself is normative), and a missing component produces an invalid request-line. The RFC's SHOULD recommendation for `400` on invalid request-lines makes rejection the expected behavior. `400` or close = **Pass**, any other response = **Fail**. + +## Sources + +- [RFC 9112 Section 3 -- Request Line](https://www.rfc-editor.org/rfc/rfc9112#section-3) diff --git a/docs/content/docs/request-line/multi-sp-request-line.md b/docs/content/docs/request-line/multi-sp-request-line.md index 952430c..2b5d905 100644 --- a/docs/content/docs/request-line/multi-sp-request-line.md +++ b/docs/content/docs/request-line/multi-sp-request-line.md @@ -1,90 +1,90 @@ ---- -title: "MULTI-SP-REQUEST-LINE" -description: "MULTI-SP-REQUEST-LINE test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `RFC9112-3-MULTI-SP-REQUEST-LINE` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 §3](https://www.rfc-editor.org/rfc/rfc9112#section-3) | -| **Requirement** | SHOULD reject, MAY parse leniently | -| **Expected** | `400` or `2xx` | - -## What it sends - -A request-line with multiple spaces between components: `GET / HTTP/1.1` (double spaces). - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -Note the double space between `GET` and `/`. - - -## What the RFC says - -The request-line grammar requires exactly one space between components: - -> "request-line = method SP request-target SP HTTP-version" -- RFC 9112 §3 - -Multiple spaces do not match this grammar, making the request-line invalid: - -> "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 Section 3 - -However, RFC 9112 §3 also permits lenient parsing: - -> "Although the request-line grammar rule requires that each of the component elements be separated by a single SP octet, recipients MAY instead parse on whitespace-delimited word boundaries and, aside from the CRLF terminator, treat any form of whitespace as the SP separator while ignoring preceding or trailing whitespace; such whitespace includes one or more of the following octets: SP, HTAB, VT (%x0B), FF (%x0C), or bare CR." -- RFC 9112 §3 - -This means a server that collapses multiple spaces and processes the request is also RFC-compliant. - -**Pass:** Server rejects with `400` (strict, follows SHOULD). -**Warn:** Server accepts and responds `2xx` (RFC-valid per MAY parse leniently). - -## Why it matters - -Some parsers are lenient and collapse multiple spaces. If a front-end collapses spaces but a back-end does not, they may parse the method, target, or version differently — leading to routing confusion or bypass. - -## Deep Analysis - -### Relevant ABNF - -``` -request-line = method SP request-target SP HTTP-version -SP = %x20 ; a single space octet -``` - -The `SP` rule in HTTP ABNF (inherited from RFC 5234) matches exactly one `%x20` octet. The request-line grammar calls for `SP` (singular), not `*SP` or `1*SP`. Therefore, `GET / HTTP/1.1` with a double space between `GET` and `/` does not match the `request-line` production. - -### RFC Evidence - -The specification is explicit that the grammar requires a single space: - -> "A request-line begins with a method token, followed by a single space (SP), the request-target, and another single space (SP), and ends with the protocol version." -- RFC 9112 Section 3 - -It then immediately acknowledges that recipients may be lenient: - -> "Although the request-line grammar rule requires that each of the component elements be separated by a single SP octet, recipients MAY instead parse on whitespace-delimited word boundaries and, aside from the CRLF terminator, treat any form of whitespace as the SP separator while ignoring preceding or trailing whitespace; such whitespace includes one or more of the following octets: SP, HTAB, VT (%x0B), FF (%x0C), or bare CR." -- RFC 9112 Section 3 - -But the specification warns about the consequences of leniency: - -> "However, lenient parsing can result in request smuggling security vulnerabilities if there are multiple recipients of the message and each has its own unique interpretation of robustness." -- RFC 9112 Section 3 - -### Chain of Reasoning - -1. The double space `GET / HTTP/1.1` does not match the ABNF production `method SP request-target SP HTTP-version` because `SP` is exactly one `%x20`. -2. This makes the request-line technically invalid. The SHOULD-level recommendation is to respond with `400`. -3. However, the MAY clause explicitly permits lenient whitespace parsing, so a server that collapses the double space and processes the request normally is also conformant. -4. The security concern is real: if a front-end proxy collapses multiple spaces but a back-end does not, they may disagree on the boundary between method and request-target. An attacker could exploit this parser differential for routing confusion or request smuggling. -5. Both strict rejection (400) and lenient acceptance (2xx) are RFC-compliant behaviors. - -### Scoring Justification - -This test is **scored with two valid outcomes**. The SHOULD/MAY duality means that `400` (strict rejection) = **Pass** and `2xx` (lenient parsing per the explicit MAY) = **Warn**. Neither outcome is a failure, because the RFC explicitly permits both behaviors. A server that returns an unexpected status (e.g., `500`) would be a concern. - -## Sources - -- [RFC 9112 §3 -- Request Line](https://www.rfc-editor.org/rfc/rfc9112#section-3) +--- +title: "Multi Space Request Line — HTTP/1.1 Compliance" +description: "A request-line with multiple spaces between components: GET / HTTP/1.1 (double spaces). Tested against RFC 9112 §3." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `RFC9112-3-MULTI-SP-REQUEST-LINE` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 §3](https://www.rfc-editor.org/rfc/rfc9112#section-3) | +| **Requirement** | SHOULD reject, MAY parse leniently | +| **Expected** | `400` or `2xx` | + +## What it sends + +A request-line with multiple spaces between components: `GET / HTTP/1.1` (double spaces). + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +Note the double space between `GET` and `/`. + + +## What the RFC says + +The request-line grammar requires exactly one space between components: + +> "request-line = method SP request-target SP HTTP-version" -- RFC 9112 §3 + +Multiple spaces do not match this grammar, making the request-line invalid: + +> "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 Section 3 + +However, RFC 9112 §3 also permits lenient parsing: + +> "Although the request-line grammar rule requires that each of the component elements be separated by a single SP octet, recipients MAY instead parse on whitespace-delimited word boundaries and, aside from the CRLF terminator, treat any form of whitespace as the SP separator while ignoring preceding or trailing whitespace; such whitespace includes one or more of the following octets: SP, HTAB, VT (%x0B), FF (%x0C), or bare CR." -- RFC 9112 §3 + +This means a server that collapses multiple spaces and processes the request is also RFC-compliant. + +**Pass:** Server rejects with `400` (strict, follows SHOULD). +**Warn:** Server accepts and responds `2xx` (RFC-valid per MAY parse leniently). + +## Why it matters + +Some parsers are lenient and collapse multiple spaces. If a front-end collapses spaces but a back-end does not, they may parse the method, target, or version differently — leading to routing confusion or bypass. + +## Deep Analysis + +### Relevant ABNF + +``` +request-line = method SP request-target SP HTTP-version +SP = %x20 ; a single space octet +``` + +The `SP` rule in HTTP ABNF (inherited from RFC 5234) matches exactly one `%x20` octet. The request-line grammar calls for `SP` (singular), not `*SP` or `1*SP`. Therefore, `GET / HTTP/1.1` with a double space between `GET` and `/` does not match the `request-line` production. + +### RFC Evidence + +The specification is explicit that the grammar requires a single space: + +> "A request-line begins with a method token, followed by a single space (SP), the request-target, and another single space (SP), and ends with the protocol version." -- RFC 9112 Section 3 + +It then immediately acknowledges that recipients may be lenient: + +> "Although the request-line grammar rule requires that each of the component elements be separated by a single SP octet, recipients MAY instead parse on whitespace-delimited word boundaries and, aside from the CRLF terminator, treat any form of whitespace as the SP separator while ignoring preceding or trailing whitespace; such whitespace includes one or more of the following octets: SP, HTAB, VT (%x0B), FF (%x0C), or bare CR." -- RFC 9112 Section 3 + +But the specification warns about the consequences of leniency: + +> "However, lenient parsing can result in request smuggling security vulnerabilities if there are multiple recipients of the message and each has its own unique interpretation of robustness." -- RFC 9112 Section 3 + +### Chain of Reasoning + +1. The double space `GET / HTTP/1.1` does not match the ABNF production `method SP request-target SP HTTP-version` because `SP` is exactly one `%x20`. +2. This makes the request-line technically invalid. The SHOULD-level recommendation is to respond with `400`. +3. However, the MAY clause explicitly permits lenient whitespace parsing, so a server that collapses the double space and processes the request normally is also conformant. +4. The security concern is real: if a front-end proxy collapses multiple spaces but a back-end does not, they may disagree on the boundary between method and request-target. An attacker could exploit this parser differential for routing confusion or request smuggling. +5. Both strict rejection (400) and lenient acceptance (2xx) are RFC-compliant behaviors. + +### Scoring Justification + +This test is **scored with two valid outcomes**. The SHOULD/MAY duality means that `400` (strict rejection) = **Pass** and `2xx` (lenient parsing per the explicit MAY) = **Warn**. Neither outcome is a failure, because the RFC explicitly permits both behaviors. A server that returns an unexpected status (e.g., `500`) would be a concern. + +## Sources + +- [RFC 9112 §3 -- Request Line](https://www.rfc-editor.org/rfc/rfc9112#section-3) diff --git a/docs/content/docs/request-line/options-allow.md b/docs/content/docs/request-line/options-allow.md index 0f30be7..eab023c 100644 --- a/docs/content/docs/request-line/options-allow.md +++ b/docs/content/docs/request-line/options-allow.md @@ -1,35 +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) +--- +title: "OPTIONS Allow — HTTP/1.1 Compliance" +description: "An OPTIONS request to the root path, asking the server to describe its capabilities for that resource. Tested against RFC 9110 §9.3.7." +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/options-star.md b/docs/content/docs/request-line/options-star.md index 6f48b77..90b5d62 100644 --- a/docs/content/docs/request-line/options-star.md +++ b/docs/content/docs/request-line/options-star.md @@ -1,83 +1,83 @@ ---- -title: "OPTIONS-STAR" -description: "OPTIONS-STAR test documentation" -weight: 7 ---- - -| | | -|---|---| -| **Test ID** | `COMP-OPTIONS-STAR` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 3.2.4](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.4) | -| **Requirement** | MUST accept | -| **Expected** | `2xx` | - -## What it sends - -`OPTIONS * HTTP/1.1` — the valid asterisk-form request. - -```http -OPTIONS * HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - - -## What the RFC says - -> "The 'asterisk-form' of request-target is only used for a server-wide OPTIONS request." -- RFC 9112 §3.2.4 - -> "When a client wishes to request OPTIONS for the server as a whole, as opposed to a specific named resource of that server, the client MUST send only '*' (%x2A) as the request-target." -- RFC 9112 §3.2.4 - -> "The OPTIONS method requests information about the communication options available for the target resource, at either the origin server or an intervening intermediary." -- RFC 9110 §9.3.7 - -## Why it matters - -This is the only valid use of `*` as a request-target. A compliant server should accept it and respond with 2xx (typically 200 with Allow header). - -## Deep Analysis - -### Relevant ABNF - -``` -request-line = method SP request-target SP HTTP-version -request-target = origin-form / absolute-form / authority-form / asterisk-form -asterisk-form = "*" -method = token -``` - -The `asterisk-form` is one of four valid `request-target` productions. `OPTIONS * HTTP/1.1` is a perfectly valid request-line: the method is `OPTIONS`, the request-target matches `asterisk-form`, and the version is `HTTP/1.1`. - -### RFC Evidence - -RFC 9112 Section 3.2.4 explicitly ties the asterisk-form to the OPTIONS method: - -> "The 'asterisk-form' of request-target is only used for a server-wide OPTIONS request." -- RFC 9112 Section 3.2.4 - -The client's obligation when making such a request is stated as a MUST: - -> "When a client wishes to request OPTIONS for the server as a whole, as opposed to a specific named resource of that server, the client MUST send only '*' (%x2A) as the request-target." -- RFC 9112 Section 3.2.4 - -RFC 9110 Section 9.3.7 describes the server-side semantics: - -> "The OPTIONS method requests information about the communication options available for the target resource, at either the origin server or an intervening intermediary." -- RFC 9110 Section 9.3.7 - -The specification also defines what a successful response should contain: - -> "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), including potential extensions not defined by this specification." -- RFC 9110 Section 9.3.7 - -### Chain of Reasoning - -1. `OPTIONS * HTTP/1.1` is syntactically valid per the ABNF. The asterisk-form is one of four defined request-target forms, and it is specifically reserved for this use case. -2. A server that implements HTTP/1.1 MUST be able to parse all four request-target forms. Rejecting `*` as a request-target would be a parsing failure. -3. The expected response is `200 OK` with an `Allow` header listing the server's supported methods. This is the canonical "ping" or capability-discovery mechanism for HTTP servers. -4. Any response other than `2xx` (such as `400` or `501`) indicates the server cannot handle the asterisk-form, which is a conformance gap. - -### Scoring Justification - -This test is **scored**. The asterisk-form is a defined part of the HTTP/1.1 grammar, and the combination `OPTIONS *` is the only valid use of it. The server MUST accept this well-formed request. `2xx` = **Pass**, any non-2xx response = **Fail**. - -## Sources - -- [RFC 9112 Section 3.2.4](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.4) -- [RFC 9110 Section 9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) +--- +title: "OPTIONS Star — HTTP/1.1 Compliance" +description: "OPTIONS * HTTP/1.1 — the valid asterisk-form request. Tested against RFC 9112 Section 3.2.4." +weight: 7 +--- + +| | | +|---|---| +| **Test ID** | `COMP-OPTIONS-STAR` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 3.2.4](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.4) | +| **Requirement** | MUST accept | +| **Expected** | `2xx` | + +## What it sends + +`OPTIONS * HTTP/1.1` — the valid asterisk-form request. + +```http +OPTIONS * HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + + +## What the RFC says + +> "The 'asterisk-form' of request-target is only used for a server-wide OPTIONS request." -- RFC 9112 §3.2.4 + +> "When a client wishes to request OPTIONS for the server as a whole, as opposed to a specific named resource of that server, the client MUST send only '*' (%x2A) as the request-target." -- RFC 9112 §3.2.4 + +> "The OPTIONS method requests information about the communication options available for the target resource, at either the origin server or an intervening intermediary." -- RFC 9110 §9.3.7 + +## Why it matters + +This is the only valid use of `*` as a request-target. A compliant server should accept it and respond with 2xx (typically 200 with Allow header). + +## Deep Analysis + +### Relevant ABNF + +``` +request-line = method SP request-target SP HTTP-version +request-target = origin-form / absolute-form / authority-form / asterisk-form +asterisk-form = "*" +method = token +``` + +The `asterisk-form` is one of four valid `request-target` productions. `OPTIONS * HTTP/1.1` is a perfectly valid request-line: the method is `OPTIONS`, the request-target matches `asterisk-form`, and the version is `HTTP/1.1`. + +### RFC Evidence + +RFC 9112 Section 3.2.4 explicitly ties the asterisk-form to the OPTIONS method: + +> "The 'asterisk-form' of request-target is only used for a server-wide OPTIONS request." -- RFC 9112 Section 3.2.4 + +The client's obligation when making such a request is stated as a MUST: + +> "When a client wishes to request OPTIONS for the server as a whole, as opposed to a specific named resource of that server, the client MUST send only '*' (%x2A) as the request-target." -- RFC 9112 Section 3.2.4 + +RFC 9110 Section 9.3.7 describes the server-side semantics: + +> "The OPTIONS method requests information about the communication options available for the target resource, at either the origin server or an intervening intermediary." -- RFC 9110 Section 9.3.7 + +The specification also defines what a successful response should contain: + +> "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), including potential extensions not defined by this specification." -- RFC 9110 Section 9.3.7 + +### Chain of Reasoning + +1. `OPTIONS * HTTP/1.1` is syntactically valid per the ABNF. The asterisk-form is one of four defined request-target forms, and it is specifically reserved for this use case. +2. A server that implements HTTP/1.1 MUST be able to parse all four request-target forms. Rejecting `*` as a request-target would be a parsing failure. +3. The expected response is `200 OK` with an `Allow` header listing the server's supported methods. This is the canonical "ping" or capability-discovery mechanism for HTTP servers. +4. Any response other than `2xx` (such as `400` or `501`) indicates the server cannot handle the asterisk-form, which is a conformance gap. + +### Scoring Justification + +This test is **scored**. The asterisk-form is a defined part of the HTTP/1.1 grammar, and the combination `OPTIONS *` is the only valid use of it. The server MUST accept this well-formed request. `2xx` = **Pass**, any non-2xx response = **Fail**. + +## Sources + +- [RFC 9112 Section 3.2.4](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.4) +- [RFC 9110 Section 9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) diff --git a/docs/content/docs/request-line/request-line-tab.md b/docs/content/docs/request-line/request-line-tab.md index 352a00b..8409c5f 100644 --- a/docs/content/docs/request-line/request-line-tab.md +++ b/docs/content/docs/request-line/request-line-tab.md @@ -1,90 +1,90 @@ ---- -title: "REQUEST-LINE-TAB" -description: "REQUEST-LINE-TAB test documentation" -weight: 14 ---- - -| | | -|---|---| -| **Test ID** | `COMP-REQUEST-LINE-TAB` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 §3](https://www.rfc-editor.org/rfc/rfc9112#section-3) | -| **Requirement** | SHOULD reject, MAY accept | -| **Expected** | `400` or `2xx` | - -## What it sends - -A request-line that uses a horizontal tab (HT, 0x09) instead of a space (SP, 0x20) between the method and the request-target. - -```http -GET\t/ HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -The `\t` between `GET` and `/` is a tab character, not a space. - -## What the RFC says - -The request-line grammar requires exactly one space (0x20) as separator: - -> "request-line = method SP request-target SP HTTP-version" -- RFC 9112 §3 - -A tab character does not match `SP`, making the request-line technically invalid: - -> "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 Section 3 - -However, RFC 9112 §3 also permits lenient parsing: - -> "Although the request-line grammar rule requires that each of the component elements be separated by a single SP octet, recipients MAY instead parse on whitespace-delimited word boundaries and, aside from the CRLF terminator, treat any form of whitespace as the SP separator while ignoring preceding or trailing whitespace; such whitespace includes one or more of the following octets: SP, HTAB, VT (%x0B), FF (%x0C), or bare CR." -- RFC 9112 §3 - -This explicitly lists HTAB as accepted whitespace, so a server that treats tab as a separator is also RFC-compliant. - -**Pass:** Server rejects with `400` (strict, follows SHOULD). -**Warn:** Server accepts and responds `2xx` (RFC-valid per MAY parse leniently). - -## Why it matters - -If a front-end proxy collapses all whitespace (including tabs) while a back-end server only recognizes spaces, they may disagree on where the method, target, and version boundaries are. This kind of parser differential can be exploited for request smuggling or routing bypasses. - -## Deep Analysis - -### Relevant ABNF - -``` -request-line = method SP request-target SP HTTP-version -SP = %x20 ; exactly one space octet -HTAB = %x09 ; horizontal tab -``` - -The `SP` rule matches only `%x20`. A horizontal tab (`%x09` / HTAB) is a distinct octet and does not match the `SP` production. Therefore, `GET\t/ HTTP/1.1` with a tab between method and request-target does not conform to the `request-line` grammar. - -### RFC Evidence - -The grammar is stated with `SP` as the required separator: - -> "A request-line begins with a method token, followed by a single space (SP), the request-target, and another single space (SP), and ends with the protocol version." -- RFC 9112 Section 3 - -The lenient parsing allowance explicitly includes HTAB in the set of accepted whitespace: - -> "Although the request-line grammar rule requires that each of the component elements be separated by a single SP octet, recipients MAY instead parse on whitespace-delimited word boundaries and, aside from the CRLF terminator, treat any form of whitespace as the SP separator while ignoring preceding or trailing whitespace; such whitespace includes one or more of the following octets: SP, HTAB, VT (%x0B), FF (%x0C), or bare CR." -- RFC 9112 Section 3 - -The security warning applies equally to tab-based parsing differentials: - -> "However, lenient parsing can result in request smuggling security vulnerabilities if there are multiple recipients of the message and each has its own unique interpretation of robustness." -- RFC 9112 Section 3 - -### Chain of Reasoning - -1. The tab octet `%x09` does not match `SP` (`%x20`), so `GET\t/ HTTP/1.1` is syntactically invalid per the strict ABNF grammar. -2. The SHOULD-level recommendation for invalid request-lines is to respond with `400`. -3. However, the MAY clause explicitly lists `HTAB` as an acceptable whitespace character for lenient parsers. A server that treats the tab as equivalent to a space and successfully parses the request is RFC-compliant. -4. The security risk is a parser differential: if a front-end proxy recognizes only `SP` as the delimiter while a back-end server also accepts HTAB, they will parse the same bytes into different request-line components. This is a classic request smuggling vector. -5. Strict rejection is safer; lenient acceptance is permitted. Both are valid. - -### Scoring Justification - -This test is **scored with two valid outcomes**, mirroring the SHOULD/MAY pattern. `400` (strict rejection) = **Pass**, `2xx` (lenient acceptance per the explicit MAY listing of HTAB) = **Warn**. The MAY clause prevents treating lenient acceptance as a failure. - -## Sources - -- [RFC 9112 §3 -- Request Line](https://www.rfc-editor.org/rfc/rfc9112#section-3) +--- +title: "Request Line Tab — HTTP/1.1 Compliance" +description: "A request-line that uses a horizontal tab (HT, 0x09) instead of a space (SP, 0x20) between the method and the request-target. Tested against RFC 9112 §3." +weight: 14 +--- + +| | | +|---|---| +| **Test ID** | `COMP-REQUEST-LINE-TAB` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 §3](https://www.rfc-editor.org/rfc/rfc9112#section-3) | +| **Requirement** | SHOULD reject, MAY accept | +| **Expected** | `400` or `2xx` | + +## What it sends + +A request-line that uses a horizontal tab (HT, 0x09) instead of a space (SP, 0x20) between the method and the request-target. + +```http +GET\t/ HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +The `\t` between `GET` and `/` is a tab character, not a space. + +## What the RFC says + +The request-line grammar requires exactly one space (0x20) as separator: + +> "request-line = method SP request-target SP HTTP-version" -- RFC 9112 §3 + +A tab character does not match `SP`, making the request-line technically invalid: + +> "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 Section 3 + +However, RFC 9112 §3 also permits lenient parsing: + +> "Although the request-line grammar rule requires that each of the component elements be separated by a single SP octet, recipients MAY instead parse on whitespace-delimited word boundaries and, aside from the CRLF terminator, treat any form of whitespace as the SP separator while ignoring preceding or trailing whitespace; such whitespace includes one or more of the following octets: SP, HTAB, VT (%x0B), FF (%x0C), or bare CR." -- RFC 9112 §3 + +This explicitly lists HTAB as accepted whitespace, so a server that treats tab as a separator is also RFC-compliant. + +**Pass:** Server rejects with `400` (strict, follows SHOULD). +**Warn:** Server accepts and responds `2xx` (RFC-valid per MAY parse leniently). + +## Why it matters + +If a front-end proxy collapses all whitespace (including tabs) while a back-end server only recognizes spaces, they may disagree on where the method, target, and version boundaries are. This kind of parser differential can be exploited for request smuggling or routing bypasses. + +## Deep Analysis + +### Relevant ABNF + +``` +request-line = method SP request-target SP HTTP-version +SP = %x20 ; exactly one space octet +HTAB = %x09 ; horizontal tab +``` + +The `SP` rule matches only `%x20`. A horizontal tab (`%x09` / HTAB) is a distinct octet and does not match the `SP` production. Therefore, `GET\t/ HTTP/1.1` with a tab between method and request-target does not conform to the `request-line` grammar. + +### RFC Evidence + +The grammar is stated with `SP` as the required separator: + +> "A request-line begins with a method token, followed by a single space (SP), the request-target, and another single space (SP), and ends with the protocol version." -- RFC 9112 Section 3 + +The lenient parsing allowance explicitly includes HTAB in the set of accepted whitespace: + +> "Although the request-line grammar rule requires that each of the component elements be separated by a single SP octet, recipients MAY instead parse on whitespace-delimited word boundaries and, aside from the CRLF terminator, treat any form of whitespace as the SP separator while ignoring preceding or trailing whitespace; such whitespace includes one or more of the following octets: SP, HTAB, VT (%x0B), FF (%x0C), or bare CR." -- RFC 9112 Section 3 + +The security warning applies equally to tab-based parsing differentials: + +> "However, lenient parsing can result in request smuggling security vulnerabilities if there are multiple recipients of the message and each has its own unique interpretation of robustness." -- RFC 9112 Section 3 + +### Chain of Reasoning + +1. The tab octet `%x09` does not match `SP` (`%x20`), so `GET\t/ HTTP/1.1` is syntactically invalid per the strict ABNF grammar. +2. The SHOULD-level recommendation for invalid request-lines is to respond with `400`. +3. However, the MAY clause explicitly lists `HTAB` as an acceptable whitespace character for lenient parsers. A server that treats the tab as equivalent to a space and successfully parses the request is RFC-compliant. +4. The security risk is a parser differential: if a front-end proxy recognizes only `SP` as the delimiter while a back-end server also accepts HTAB, they will parse the same bytes into different request-line components. This is a classic request smuggling vector. +5. Strict rejection is safer; lenient acceptance is permitted. Both are valid. + +### Scoring Justification + +This test is **scored with two valid outcomes**, mirroring the SHOULD/MAY pattern. `400` (strict rejection) = **Pass**, `2xx` (lenient acceptance per the explicit MAY listing of HTAB) = **Warn**. The MAY clause prevents treating lenient acceptance as a failure. + +## Sources + +- [RFC 9112 §3 -- Request Line](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 index af5266e..4938563 100644 --- a/docs/content/docs/request-line/space-in-target.md +++ b/docs/content/docs/request-line/space-in-target.md @@ -1,47 +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) +--- +title: "Space In Target — HTTP/1.1 Compliance" +description: "A GET request with an unencoded space inside the request-target. Tested against RFC 9112 §3.2." +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 index 3e82717..b7415c1 100644 --- a/docs/content/docs/request-line/trace-sensitive.md +++ b/docs/content/docs/request-line/trace-sensitive.md @@ -1,46 +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) +--- +title: "TRACE Sensitive — HTTP/1.1 Compliance" +description: "A TRACE request that includes an Authorization header with a bearer token. Tested against RFC 9110 §9.3.8." +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/trace-with-body.md b/docs/content/docs/request-line/trace-with-body.md index f346723..6707b8c 100644 --- a/docs/content/docs/request-line/trace-with-body.md +++ b/docs/content/docs/request-line/trace-with-body.md @@ -1,88 +1,88 @@ ---- -title: "TRACE-WITH-BODY" -description: "TRACE-WITH-BODY test documentation" -weight: 20 ---- - -| | | -|---|---| -| **Test ID** | `COMP-TRACE-WITH-BODY` | -| **Category** | Compliance | -| **RFC** | [RFC 9110 §9.3.8](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.8) | -| **Requirement** | SHOULD reject (unscored) | -| **Expected** | `400`/`405` = Pass, `200` = Warn | - -## What it sends - -A TRACE request that includes a `Content-Length` header and a message body, which clients are prohibited from sending. - -```http -TRACE / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -## What the RFC says - -> "A client MUST NOT send content in a TRACE request." -- RFC 9110 §9.3.8 - -> "A client MUST NOT generate fields in a TRACE request containing sensitive data that might be disclosed by the response." -- RFC 9110 §9.3.8 - -> "The final recipient of the request SHOULD exclude any request fields that are likely to contain sensitive data when that recipient generates the response content." -- RFC 9110 §9.3.8 - -While the `MUST NOT send content` prohibition is stated as a client requirement, a server receiving a TRACE request with a body is dealing with a client that has violated the spec. The server should reject the request or ignore the body entirely. - -**Pass:** Server rejects with `400` (bad request) or `405` (method not allowed) or `501` (not implemented). -**Warn:** Server accepts with `200` (processes the TRACE despite the body). - -## Why this test is unscored - -The MUST NOT is directed at clients, not servers. There is no explicit RFC requirement for how a server should handle a TRACE request that contains a body. Both rejecting the request and processing it (ignoring the body) are defensible behaviors, so this test records the response without scoring it. - -## Why it matters - -TRACE is designed to echo back the request headers for diagnostic purposes. If a server processes a TRACE request with a body, the body content could be reflected back or logged, potentially amplifying Cross-Site Tracing (XST) attacks. A body in a TRACE request is always a sign of a misbehaving or malicious client, and the safest response is rejection. - -## Deep Analysis - -### Relevant ABNF - -``` -request-line = method SP request-target SP HTTP-version -method = token -``` - -`TRACE / HTTP/1.1` is a syntactically valid request-line. The violation here is not in the request-line grammar but in the semantic constraint on request content for the TRACE method. - -### RFC Evidence - -The prohibition on request content is stated as a client-side MUST NOT: - -> "A client MUST NOT send content in a TRACE request." -- RFC 9110 Section 9.3.8 - -The purpose of TRACE is to echo the request for diagnostics: - -> "The TRACE method requests a remote, application-level loop-back of the request message. The final recipient of the request SHOULD reflect the message received, excluding some fields described below, back to the client as the content of a 200 (OK) response." -- RFC 9110 Section 9.3.8 - -The specification also restricts sensitive data in TRACE requests and responses: - -> "A client MUST NOT generate 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 user credentials or cookies in a TRACE request. The final recipient of the request SHOULD exclude any request fields that are likely to contain sensitive data when that recipient generates the response content." -- RFC 9110 Section 9.3.8 - -### Chain of Reasoning - -1. The `MUST NOT send content` requirement is directed at clients, not servers. The RFC defines no server-side MUST or SHOULD for how to handle a TRACE request that arrives with a body. -2. A server that receives a TRACE request with `Content-Length: 5` and a body is dealing with a non-conformant client. The specification does not prescribe a specific error code for this scenario. -3. Rejecting with `400` (the request violates a protocol constraint), `405` (TRACE not allowed), or `501` (TRACE not implemented) are all defensible. Each prevents the body from being reflected. -4. Accepting with `200` is also defensible -- the server may simply ignore the body and echo only the headers, which is the defined TRACE behavior. However, if the server reflects the body, it amplifies the XST attack surface. -5. Because no server-side normative requirement exists for this scenario, the test cannot fairly penalize either response. - -### Scoring Justification - -This test is **unscored**. The `MUST NOT` applies to the client, not the server. There is no explicit server-side requirement in RFC 9110 for rejecting TRACE requests that contain a body. `400`/`405`/`501` = **Pass** (defensive), `200` = **Warn** (the server processed a request from a non-conformant client, which may expose attack surface). - -## Sources - -- [RFC 9110 §9.3.8 -- TRACE](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.8) -- [OWASP: Cross-Site Tracing](https://owasp.org/www-community/attacks/Cross_Site_Tracing) +--- +title: "TRACE With Body — HTTP/1.1 Compliance" +description: "A TRACE request that includes a Content-Length header and a message body, which clients are prohibited from sending. Tested against RFC 9110 §9.3.8." +weight: 20 +--- + +| | | +|---|---| +| **Test ID** | `COMP-TRACE-WITH-BODY` | +| **Category** | Compliance | +| **RFC** | [RFC 9110 §9.3.8](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.8) | +| **Requirement** | SHOULD reject (unscored) | +| **Expected** | `400`/`405` = Pass, `200` = Warn | + +## What it sends + +A TRACE request that includes a `Content-Length` header and a message body, which clients are prohibited from sending. + +```http +TRACE / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +## What the RFC says + +> "A client MUST NOT send content in a TRACE request." -- RFC 9110 §9.3.8 + +> "A client MUST NOT generate fields in a TRACE request containing sensitive data that might be disclosed by the response." -- RFC 9110 §9.3.8 + +> "The final recipient of the request SHOULD exclude any request fields that are likely to contain sensitive data when that recipient generates the response content." -- RFC 9110 §9.3.8 + +While the `MUST NOT send content` prohibition is stated as a client requirement, a server receiving a TRACE request with a body is dealing with a client that has violated the spec. The server should reject the request or ignore the body entirely. + +**Pass:** Server rejects with `400` (bad request) or `405` (method not allowed) or `501` (not implemented). +**Warn:** Server accepts with `200` (processes the TRACE despite the body). + +## Why this test is unscored + +The MUST NOT is directed at clients, not servers. There is no explicit RFC requirement for how a server should handle a TRACE request that contains a body. Both rejecting the request and processing it (ignoring the body) are defensible behaviors, so this test records the response without scoring it. + +## Why it matters + +TRACE is designed to echo back the request headers for diagnostic purposes. If a server processes a TRACE request with a body, the body content could be reflected back or logged, potentially amplifying Cross-Site Tracing (XST) attacks. A body in a TRACE request is always a sign of a misbehaving or malicious client, and the safest response is rejection. + +## Deep Analysis + +### Relevant ABNF + +``` +request-line = method SP request-target SP HTTP-version +method = token +``` + +`TRACE / HTTP/1.1` is a syntactically valid request-line. The violation here is not in the request-line grammar but in the semantic constraint on request content for the TRACE method. + +### RFC Evidence + +The prohibition on request content is stated as a client-side MUST NOT: + +> "A client MUST NOT send content in a TRACE request." -- RFC 9110 Section 9.3.8 + +The purpose of TRACE is to echo the request for diagnostics: + +> "The TRACE method requests a remote, application-level loop-back of the request message. The final recipient of the request SHOULD reflect the message received, excluding some fields described below, back to the client as the content of a 200 (OK) response." -- RFC 9110 Section 9.3.8 + +The specification also restricts sensitive data in TRACE requests and responses: + +> "A client MUST NOT generate 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 user credentials or cookies in a TRACE request. The final recipient of the request SHOULD exclude any request fields that are likely to contain sensitive data when that recipient generates the response content." -- RFC 9110 Section 9.3.8 + +### Chain of Reasoning + +1. The `MUST NOT send content` requirement is directed at clients, not servers. The RFC defines no server-side MUST or SHOULD for how to handle a TRACE request that arrives with a body. +2. A server that receives a TRACE request with `Content-Length: 5` and a body is dealing with a non-conformant client. The specification does not prescribe a specific error code for this scenario. +3. Rejecting with `400` (the request violates a protocol constraint), `405` (TRACE not allowed), or `501` (TRACE not implemented) are all defensible. Each prevents the body from being reflected. +4. Accepting with `200` is also defensible -- the server may simply ignore the body and echo only the headers, which is the defined TRACE behavior. However, if the server reflects the body, it amplifies the XST attack surface. +5. Because no server-side normative requirement exists for this scenario, the test cannot fairly penalize either response. + +### Scoring Justification + +This test is **unscored**. The `MUST NOT` applies to the client, not the server. There is no explicit server-side requirement in RFC 9110 for rejecting TRACE requests that contain a body. `400`/`405`/`501` = **Pass** (defensive), `200` = **Warn** (the server processed a request from a non-conformant client, which may expose attack surface). + +## Sources + +- [RFC 9110 §9.3.8 -- TRACE](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.8) +- [OWASP: Cross-Site Tracing](https://owasp.org/www-community/attacks/Cross_Site_Tracing) diff --git a/docs/content/docs/request-line/unknown-method.md b/docs/content/docs/request-line/unknown-method.md index 4ba497f..604ed17 100644 --- a/docs/content/docs/request-line/unknown-method.md +++ b/docs/content/docs/request-line/unknown-method.md @@ -1,41 +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) +--- +title: "Unknown Method — HTTP/1.1 Compliance" +description: "A request with a completely fabricated method name that no server should recognize. Tested against RFC 9110 §9.1." +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/request-line/unknown-te-501.md b/docs/content/docs/request-line/unknown-te-501.md index 855b1d5..e08404d 100644 --- a/docs/content/docs/request-line/unknown-te-501.md +++ b/docs/content/docs/request-line/unknown-te-501.md @@ -1,78 +1,78 @@ ---- -title: "UNKNOWN-TE-501" -description: "UNKNOWN-TE-501 test documentation" -weight: 8 ---- - -| | | -|---|---| -| **Test ID** | `COMP-UNKNOWN-TE-501` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 Section 6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **Requirement** | SHOULD respond with 501 | -| **Expected** | `400`/`501` or close | - -## What it sends - -`Transfer-Encoding: gzip` without any Content-Length — an unknown transfer coding as the only framing. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: gzip\r\n -\r\n -``` - - -## What the RFC says - -> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 §6.1 - -Additionally, the chunked coding is the only transfer coding that is universally required: - -> "A recipient MUST be able to parse the chunked transfer coding because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 §6.1 - -## Why it matters - -When a server doesn't understand the transfer coding and there's no Content-Length fallback, it cannot determine the message body boundaries. Rejecting or responding with 501 is correct. - -## Deep Analysis - -### Relevant ABNF - -``` -Transfer-Encoding = #transfer-coding -transfer-coding = token *( OWS ";" OWS transfer-parameter ) -``` - -The `Transfer-Encoding` header carries a list of transfer codings. The only transfer coding that all HTTP/1.1 implementations are required to support is `chunked`. The value `gzip` is a content coding, not a transfer coding -- when it appears alone in `Transfer-Encoding` without `chunked` as the final coding, the server likely cannot parse or frame the message body. - -### RFC Evidence - -The core requirement for handling unrecognized transfer codings is stated directly: - -> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 Section 6.1 - -The universal requirement to support chunked is also relevant: - -> "A recipient MUST be able to parse the chunked transfer coding because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 Section 6.1 - -The specification also requires that non-chunked transfer codings must be followed by chunked for proper framing: - -> "If any transfer coding other than chunked is applied to a request's content, the sender MUST apply chunked as the final transfer coding to ensure that the message is properly framed." -- RFC 9112 Section 6.1 - -### Chain of Reasoning - -1. The request sends `Transfer-Encoding: gzip` as the sole transfer coding, with no `Content-Length` header. This means the only framing information available is the `Transfer-Encoding` header. -2. If the server does not understand `gzip` as a transfer coding (and most servers only recognize `chunked`), it has no way to determine the message body boundaries. -3. The RFC recommends `501 (Not Implemented)` for this scenario. A `400` is also a reasonable rejection -- the request lacks proper framing. -4. Without either a recognized transfer coding or a `Content-Length`, the server cannot safely read the body. Any attempt to guess body boundaries could lead to desynchronization on a persistent connection. -5. Connection close is also an acceptable response because it eliminates the desynchronization risk entirely. - -### Scoring Justification - -This test is **scored**. The RFC provides a clear SHOULD-level recommendation for `501` when the transfer coding is not understood, and the absence of any alternative framing (no `Content-Length`) makes the request unprocessable. `400`/`501` or close = **Pass**, `2xx` (processing a request with unknown framing) = **Fail**. - -## Sources - -- [RFC 9112 Section 6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "Unknown TE 501 — HTTP/1.1 Compliance" +description: "Transfer-Encoding: gzip without any Content-Length — an unknown transfer coding as the only framing. Tested against RFC 9112 Section 6.1." +weight: 8 +--- + +| | | +|---|---| +| **Test ID** | `COMP-UNKNOWN-TE-501` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 Section 6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **Requirement** | SHOULD respond with 501 | +| **Expected** | `400`/`501` or close | + +## What it sends + +`Transfer-Encoding: gzip` without any Content-Length — an unknown transfer coding as the only framing. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: gzip\r\n +\r\n +``` + + +## What the RFC says + +> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 §6.1 + +Additionally, the chunked coding is the only transfer coding that is universally required: + +> "A recipient MUST be able to parse the chunked transfer coding because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 §6.1 + +## Why it matters + +When a server doesn't understand the transfer coding and there's no Content-Length fallback, it cannot determine the message body boundaries. Rejecting or responding with 501 is correct. + +## Deep Analysis + +### Relevant ABNF + +``` +Transfer-Encoding = #transfer-coding +transfer-coding = token *( OWS ";" OWS transfer-parameter ) +``` + +The `Transfer-Encoding` header carries a list of transfer codings. The only transfer coding that all HTTP/1.1 implementations are required to support is `chunked`. The value `gzip` is a content coding, not a transfer coding -- when it appears alone in `Transfer-Encoding` without `chunked` as the final coding, the server likely cannot parse or frame the message body. + +### RFC Evidence + +The core requirement for handling unrecognized transfer codings is stated directly: + +> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 Section 6.1 + +The universal requirement to support chunked is also relevant: + +> "A recipient MUST be able to parse the chunked transfer coding because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 Section 6.1 + +The specification also requires that non-chunked transfer codings must be followed by chunked for proper framing: + +> "If any transfer coding other than chunked is applied to a request's content, the sender MUST apply chunked as the final transfer coding to ensure that the message is properly framed." -- RFC 9112 Section 6.1 + +### Chain of Reasoning + +1. The request sends `Transfer-Encoding: gzip` as the sole transfer coding, with no `Content-Length` header. This means the only framing information available is the `Transfer-Encoding` header. +2. If the server does not understand `gzip` as a transfer coding (and most servers only recognize `chunked`), it has no way to determine the message body boundaries. +3. The RFC recommends `501 (Not Implemented)` for this scenario. A `400` is also a reasonable rejection -- the request lacks proper framing. +4. Without either a recognized transfer coding or a `Content-Length`, the server cannot safely read the body. Any attempt to guess body boundaries could lead to desynchronization on a persistent connection. +5. Connection close is also an acceptable response because it eliminates the desynchronization risk entirely. + +### Scoring Justification + +This test is **scored**. The RFC provides a clear SHOULD-level recommendation for `501` when the transfer coding is not understood, and the absence of any alternative framing (no `Content-Length`) makes the request unprocessable. `400`/`501` or close = **Pass**, `2xx` (processing a request with unknown framing) = **Fail**. + +## Sources + +- [RFC 9112 Section 6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/request-line/version-case.md b/docs/content/docs/request-line/version-case.md index 41e05ca..6170eff 100644 --- a/docs/content/docs/request-line/version-case.md +++ b/docs/content/docs/request-line/version-case.md @@ -1,47 +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) +--- +title: "Version Case — HTTP/1.1 Compliance" +description: "A GET request with lowercase http/1.1 instead of HTTP/1.1. Tested against RFC 9112 §2.3." +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/request-line/version-leading-zeros.md b/docs/content/docs/request-line/version-leading-zeros.md index 2ad78c0..33ef259 100644 --- a/docs/content/docs/request-line/version-leading-zeros.md +++ b/docs/content/docs/request-line/version-leading-zeros.md @@ -1,77 +1,77 @@ ---- -title: "VERSION-LEADING-ZEROS" -description: "VERSION-LEADING-ZEROS test documentation" -weight: 16 ---- - -| | | -|---|---| -| **Test ID** | `COMP-VERSION-LEADING-ZEROS` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -A request with `HTTP/01.01` as the version -- leading zeros on both the major and minor version digits. - -```http -GET / HTTP/01.01\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 §2.3 - -The grammar specifies exactly one `DIGIT` on each side of the dot. `01` is two digits, not one. `HTTP/01.01` does not match the production rule, making it a syntactically invalid version string. Since the version is malformed, the entire request-line is invalid: - -> "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 Section 3 - -## Why it matters - -Leading zeros may cause version comparison bugs. A parser that strips leading zeros might interpret `HTTP/01.01` as `HTTP/1.1`, while another parser might reject it or treat it as an unknown version. This disagreement between parsers can lead to inconsistent behavior in proxy chains, where one component processes the request differently than another. - -## Deep Analysis - -### Relevant ABNF - -``` -HTTP-version = HTTP-name "/" DIGIT "." DIGIT -HTTP-name = %s"HTTP" -DIGIT = %x30-39 ; 0-9 -``` - -The `DIGIT` rule (from RFC 5234) matches exactly one octet in the range `%x30-39`. It is not `1*DIGIT` or `*DIGIT` -- the production calls for a single `DIGIT` on each side of the dot. `HTTP/01.01` has two digits (`01`) for both major and minor, which means neither `01` matches the `DIGIT` production. The version string is syntactically invalid. - -### RFC Evidence - -The ABNF is stated clearly in the version definition: - -> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 Section 2.3 - -The version field is part of the request-line, and a malformed version makes the entire line invalid: - -> "A request-line begins with a method token, followed by a single space (SP), the request-target, and another single space (SP), and ends with the protocol version." -- RFC 9112 Section 3 - -The recommendation for invalid request-lines applies: - -> "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 Section 3 - -### Chain of Reasoning - -1. The ABNF specifies `DIGIT` (singular), not `1*DIGIT`. `HTTP/01.01` uses two digits for both major and minor version numbers, which exceeds the grammar's allowance. -2. There is no leniency clause in RFC 9112 Section 2.3 for leading zeros -- unlike the request-line SP/whitespace flexibility in Section 3, the version grammar has no MAY-level relaxation. -3. A parser that strips leading zeros would interpret `HTTP/01.01` as `HTTP/1.1`, but a strict parser would reject it. This creates a parser differential: one component in a proxy chain might accept the request while another might not. -4. If the front-end strips zeros and forwards `HTTP/1.1` but the back-end sees the original `HTTP/01.01` and rejects it, the front-end has already committed to the connection. If the front-end accepts and the back-end also accepts after stripping, they may still disagree on version semantics. -5. The strict grammar with no relaxation clause makes this a clear MUST-level violation. - -### Scoring Justification - -This test is **scored**. The `HTTP-version` ABNF is a normative grammar rule with no MAY-level leniency for extra digits. `HTTP/01.01` is unambiguously invalid. `400` or close = **Pass**, `2xx` (accepting a malformed version) = **Fail**. - -## Sources - -- [RFC 9112 §2.3 -- HTTP Version](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) +--- +title: "Version Leading Zeros — HTTP/1.1 Compliance" +description: "A request with HTTP/01.01 as the version -- leading zeros on both the major and minor version digits. Tested against RFC 9112 §2.3." +weight: 16 +--- + +| | | +|---|---| +| **Test ID** | `COMP-VERSION-LEADING-ZEROS` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +A request with `HTTP/01.01` as the version -- leading zeros on both the major and minor version digits. + +```http +GET / HTTP/01.01\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 §2.3 + +The grammar specifies exactly one `DIGIT` on each side of the dot. `01` is two digits, not one. `HTTP/01.01` does not match the production rule, making it a syntactically invalid version string. Since the version is malformed, the entire request-line is invalid: + +> "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 Section 3 + +## Why it matters + +Leading zeros may cause version comparison bugs. A parser that strips leading zeros might interpret `HTTP/01.01` as `HTTP/1.1`, while another parser might reject it or treat it as an unknown version. This disagreement between parsers can lead to inconsistent behavior in proxy chains, where one component processes the request differently than another. + +## Deep Analysis + +### Relevant ABNF + +``` +HTTP-version = HTTP-name "/" DIGIT "." DIGIT +HTTP-name = %s"HTTP" +DIGIT = %x30-39 ; 0-9 +``` + +The `DIGIT` rule (from RFC 5234) matches exactly one octet in the range `%x30-39`. It is not `1*DIGIT` or `*DIGIT` -- the production calls for a single `DIGIT` on each side of the dot. `HTTP/01.01` has two digits (`01`) for both major and minor, which means neither `01` matches the `DIGIT` production. The version string is syntactically invalid. + +### RFC Evidence + +The ABNF is stated clearly in the version definition: + +> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 Section 2.3 + +The version field is part of the request-line, and a malformed version makes the entire line invalid: + +> "A request-line begins with a method token, followed by a single space (SP), the request-target, and another single space (SP), and ends with the protocol version." -- RFC 9112 Section 3 + +The recommendation for invalid request-lines applies: + +> "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 Section 3 + +### Chain of Reasoning + +1. The ABNF specifies `DIGIT` (singular), not `1*DIGIT`. `HTTP/01.01` uses two digits for both major and minor version numbers, which exceeds the grammar's allowance. +2. There is no leniency clause in RFC 9112 Section 2.3 for leading zeros -- unlike the request-line SP/whitespace flexibility in Section 3, the version grammar has no MAY-level relaxation. +3. A parser that strips leading zeros would interpret `HTTP/01.01` as `HTTP/1.1`, but a strict parser would reject it. This creates a parser differential: one component in a proxy chain might accept the request while another might not. +4. If the front-end strips zeros and forwards `HTTP/1.1` but the back-end sees the original `HTTP/01.01` and rejects it, the front-end has already committed to the connection. If the front-end accepts and the back-end also accepts after stripping, they may still disagree on version semantics. +5. The strict grammar with no relaxation clause makes this a clear MUST-level violation. + +### Scoring Justification + +This test is **scored**. The `HTTP-version` ABNF is a normative grammar rule with no MAY-level leniency for extra digits. `HTTP/01.01` is unambiguously invalid. `400` or close = **Pass**, `2xx` (accepting a malformed version) = **Fail**. + +## Sources + +- [RFC 9112 §2.3 -- HTTP Version](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) diff --git a/docs/content/docs/request-line/version-missing-minor.md b/docs/content/docs/request-line/version-missing-minor.md index d71b902..e338bce 100644 --- a/docs/content/docs/request-line/version-missing-minor.md +++ b/docs/content/docs/request-line/version-missing-minor.md @@ -1,79 +1,79 @@ ---- -title: "VERSION-MISSING-MINOR" -description: "VERSION-MISSING-MINOR test documentation" -weight: 15 ---- - -| | | -|---|---| -| **Test ID** | `COMP-VERSION-MISSING-MINOR` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -A request with `HTTP/1` as the version -- missing the dot and minor version digit. - -```http -GET / HTTP/1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 §2.3 - -> "HTTP-version is case-sensitive." -- RFC 9112 §2.3 - -The HTTP version string requires exactly one digit, a dot, and one digit after `HTTP/`. `HTTP/1` omits the dot and the minor version digit entirely, so it does not match the grammar. Since the version field is malformed, the entire request-line is invalid: - -> "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 Section 3 - -## Why it matters - -A truncated version string creates ambiguity about the client's capabilities. If a server guesses the minor version (e.g., assumes `HTTP/1.0` or `HTTP/1.1`), it may enable or disable features like persistent connections, chunked encoding, or Host header requirements incorrectly. Strict parsing prevents this guesswork. - -## Deep Analysis - -### Relevant ABNF - -``` -HTTP-version = HTTP-name "/" DIGIT "." DIGIT -HTTP-name = %s"HTTP" -DIGIT = %x30-39 -``` - -The `HTTP-version` production requires five fixed components in sequence: the literal `HTTP`, a `/`, one `DIGIT`, a `.`, and one `DIGIT`. The string `HTTP/1` is missing the final two components (the dot and the minor version digit), so it does not match the grammar. - -### RFC Evidence - -The ABNF defines the required structure: - -> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 Section 2.3 - -The version string is explicitly described as a major.minor pair: - -> "HTTP uses a '.' numbering scheme to indicate versions of the protocol. This specification defines version '1.1'." -- RFC 9112 Section 2.3 - -The version field is also case-sensitive, reinforcing that it must be parsed exactly: - -> "HTTP-version is case-sensitive." -- RFC 9112 Section 2.3 - -### Chain of Reasoning - -1. `HTTP/1` contains only the HTTP-name, a `/`, and one `DIGIT`. The required `.` and second `DIGIT` are absent. This is a clear grammar violation -- the string is truncated. -2. A server that receives `HTTP/1` cannot determine the minor version. Is it `HTTP/1.0` (no persistent connections by default, no chunked TE requirement) or `HTTP/1.1` (persistent connections, Host header required, chunked TE supported)? -3. Guessing the minor version is dangerous. If the server assumes `HTTP/1.1` and sends a chunked response, an `HTTP/1.0`-only client will fail to parse it. If the server assumes `HTTP/1.0` and closes the connection, it may break pipelining expectations of an `HTTP/1.1` client. -4. The RFC provides no leniency clause for truncated version strings. Unlike the whitespace flexibility in Section 3, the version grammar in Section 2.3 has no MAY-level relaxation. -5. Because the grammar is strictly defined and the ambiguity is unresolvable, rejection with `400` or connection close is the correct behavior. - -### Scoring Justification - -This test is **scored**. The `HTTP-version` ABNF is a normative MUST-level grammar rule. `HTTP/1` fails to match the production because it is missing the dot and minor digit. There is no MAY clause that permits accepting a truncated version. `400` or close = **Pass**, `2xx` = **Fail**. - -## Sources - -- [RFC 9112 §2.3 -- HTTP Version](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) +--- +title: "Version Missing Minor — HTTP/1.1 Compliance" +description: "A request with HTTP/1 as the version -- missing the dot and minor version digit. Tested against RFC 9112 §2.3." +weight: 15 +--- + +| | | +|---|---| +| **Test ID** | `COMP-VERSION-MISSING-MINOR` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +A request with `HTTP/1` as the version -- missing the dot and minor version digit. + +```http +GET / HTTP/1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 §2.3 + +> "HTTP-version is case-sensitive." -- RFC 9112 §2.3 + +The HTTP version string requires exactly one digit, a dot, and one digit after `HTTP/`. `HTTP/1` omits the dot and the minor version digit entirely, so it does not match the grammar. Since the version field is malformed, the entire request-line is invalid: + +> "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 Section 3 + +## Why it matters + +A truncated version string creates ambiguity about the client's capabilities. If a server guesses the minor version (e.g., assumes `HTTP/1.0` or `HTTP/1.1`), it may enable or disable features like persistent connections, chunked encoding, or Host header requirements incorrectly. Strict parsing prevents this guesswork. + +## Deep Analysis + +### Relevant ABNF + +``` +HTTP-version = HTTP-name "/" DIGIT "." DIGIT +HTTP-name = %s"HTTP" +DIGIT = %x30-39 +``` + +The `HTTP-version` production requires five fixed components in sequence: the literal `HTTP`, a `/`, one `DIGIT`, a `.`, and one `DIGIT`. The string `HTTP/1` is missing the final two components (the dot and the minor version digit), so it does not match the grammar. + +### RFC Evidence + +The ABNF defines the required structure: + +> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 Section 2.3 + +The version string is explicitly described as a major.minor pair: + +> "HTTP uses a '.' numbering scheme to indicate versions of the protocol. This specification defines version '1.1'." -- RFC 9112 Section 2.3 + +The version field is also case-sensitive, reinforcing that it must be parsed exactly: + +> "HTTP-version is case-sensitive." -- RFC 9112 Section 2.3 + +### Chain of Reasoning + +1. `HTTP/1` contains only the HTTP-name, a `/`, and one `DIGIT`. The required `.` and second `DIGIT` are absent. This is a clear grammar violation -- the string is truncated. +2. A server that receives `HTTP/1` cannot determine the minor version. Is it `HTTP/1.0` (no persistent connections by default, no chunked TE requirement) or `HTTP/1.1` (persistent connections, Host header required, chunked TE supported)? +3. Guessing the minor version is dangerous. If the server assumes `HTTP/1.1` and sends a chunked response, an `HTTP/1.0`-only client will fail to parse it. If the server assumes `HTTP/1.0` and closes the connection, it may break pipelining expectations of an `HTTP/1.1` client. +4. The RFC provides no leniency clause for truncated version strings. Unlike the whitespace flexibility in Section 3, the version grammar in Section 2.3 has no MAY-level relaxation. +5. Because the grammar is strictly defined and the ambiguity is unresolvable, rejection with `400` or connection close is the correct behavior. + +### Scoring Justification + +This test is **scored**. The `HTTP-version` ABNF is a normative MUST-level grammar rule. `HTTP/1` fails to match the production because it is missing the dot and minor digit. There is no MAY clause that permits accepting a truncated version. `400` or close = **Pass**, `2xx` = **Fail**. + +## Sources + +- [RFC 9112 §2.3 -- HTTP Version](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) diff --git a/docs/content/docs/request-line/version-whitespace.md b/docs/content/docs/request-line/version-whitespace.md index 139c58d..62a4542 100644 --- a/docs/content/docs/request-line/version-whitespace.md +++ b/docs/content/docs/request-line/version-whitespace.md @@ -1,77 +1,77 @@ ---- -title: "VERSION-WHITESPACE" -description: "VERSION-WHITESPACE test documentation" -weight: 17 ---- - -| | | -|---|---| -| **Test ID** | `COMP-VERSION-WHITESPACE` | -| **Category** | Compliance | -| **RFC** | [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -A request with `HTTP/ 1.1` as the version -- a space character inserted between `HTTP/` and `1.1`. - -```http -GET / HTTP/ 1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 §2.3 - -The HTTP-version is a single contiguous token with no internal whitespace. The space between the slash and the version digits breaks the token, making the request-line invalid. The grammar does not allow any SP or HTAB inside the version string. - -> "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 Section 3 - -## Why it matters - -A server that is lenient about whitespace inside the version token could be tricked into parsing the request-line differently than a strict proxy in front of it. For example, a lenient parser might read `HTTP/` followed by ` 1.1` and strip the space, while a strict parser sees an invalid version. This differential creates opportunities for request smuggling. - -## Deep Analysis - -### Relevant ABNF - -``` -HTTP-version = HTTP-name "/" DIGIT "." DIGIT -HTTP-name = %s"HTTP" -DIGIT = %x30-39 -``` - -The `HTTP-version` production is a single contiguous token: `HTTP` `/` `DIGIT` `.` `DIGIT` -- with no whitespace permitted between any of the components. The string `HTTP/ 1.1` inserts a `SP` (`%x20`) between the `/` and the first `DIGIT`, which does not match the grammar. - -### RFC Evidence - -The ABNF requires the version to be a contiguous token: - -> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 Section 2.3 - -The version field terminates the request-line, and the request-line grammar provides the context: - -> "A request-line begins with a method token, followed by a single space (SP), the request-target, and another single space (SP), and ends with the protocol version." -- RFC 9112 Section 3 - -The lenient whitespace parsing clause in Section 3 applies to the separators between the three request-line components (method, request-target, version) but not to whitespace within any individual component. For the version specifically, there is no relaxation: - -> "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 Section 3 - -### Chain of Reasoning - -1. `HTTP/ 1.1` contains a space between `/` and `1`, which is not permitted by the `HTTP-version` ABNF. The grammar specifies a direct concatenation: `HTTP-name "/" DIGIT "." DIGIT`. -2. A whitespace-delimited parser splitting the request-line `GET / HTTP/ 1.1` on word boundaries would see four tokens: `GET`, `/`, `HTTP/`, and `1.1`. This does not match the three-component structure of `method SP request-target SP HTTP-version`. -3. The lenient parsing clause in RFC 9112 Section 3 permits treating whitespace as the `SP` separator between components, but it does not permit inserting whitespace inside any component. The version is one component, not two. -4. A proxy that reconstructs the version by concatenating `HTTP/` and `1.1` would silently "fix" the request, while a strict parser would reject it. This parser differential could allow an attacker to bypass front-end validation. -5. No MAY-level relaxation exists for whitespace inside the version token, making this a clear grammar violation. - -### Scoring Justification - -This test is **scored**. The `HTTP-version` ABNF is a normative MUST-level grammar rule that requires a contiguous token with no internal whitespace. The lenient parsing clause in Section 3 does not extend to whitespace within individual request-line components. `400` or close = **Pass**, `2xx` = **Fail**. - -## Sources - -- [RFC 9112 §2.3 -- HTTP Version](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) +--- +title: "Version Whitespace — HTTP/1.1 Compliance" +description: "A request with HTTP/ 1.1 as the version -- a space character inserted between HTTP/ and 1.1. Tested against RFC 9112 §2.3." +weight: 17 +--- + +| | | +|---|---| +| **Test ID** | `COMP-VERSION-WHITESPACE` | +| **Category** | Compliance | +| **RFC** | [RFC 9112 §2.3](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +A request with `HTTP/ 1.1` as the version -- a space character inserted between `HTTP/` and `1.1`. + +```http +GET / HTTP/ 1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 §2.3 + +The HTTP-version is a single contiguous token with no internal whitespace. The space between the slash and the version digits breaks the token, making the request-line invalid. The grammar does not allow any SP or HTAB inside the version string. + +> "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 Section 3 + +## Why it matters + +A server that is lenient about whitespace inside the version token could be tricked into parsing the request-line differently than a strict proxy in front of it. For example, a lenient parser might read `HTTP/` followed by ` 1.1` and strip the space, while a strict parser sees an invalid version. This differential creates opportunities for request smuggling. + +## Deep Analysis + +### Relevant ABNF + +``` +HTTP-version = HTTP-name "/" DIGIT "." DIGIT +HTTP-name = %s"HTTP" +DIGIT = %x30-39 +``` + +The `HTTP-version` production is a single contiguous token: `HTTP` `/` `DIGIT` `.` `DIGIT` -- with no whitespace permitted between any of the components. The string `HTTP/ 1.1` inserts a `SP` (`%x20`) between the `/` and the first `DIGIT`, which does not match the grammar. + +### RFC Evidence + +The ABNF requires the version to be a contiguous token: + +> "HTTP-version = HTTP-name '/' DIGIT '.' DIGIT" -- RFC 9112 Section 2.3 + +The version field terminates the request-line, and the request-line grammar provides the context: + +> "A request-line begins with a method token, followed by a single space (SP), the request-target, and another single space (SP), and ends with the protocol version." -- RFC 9112 Section 3 + +The lenient whitespace parsing clause in Section 3 applies to the separators between the three request-line components (method, request-target, version) but not to whitespace within any individual component. For the version specifically, there is no relaxation: + +> "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 Section 3 + +### Chain of Reasoning + +1. `HTTP/ 1.1` contains a space between `/` and `1`, which is not permitted by the `HTTP-version` ABNF. The grammar specifies a direct concatenation: `HTTP-name "/" DIGIT "." DIGIT`. +2. A whitespace-delimited parser splitting the request-line `GET / HTTP/ 1.1` on word boundaries would see four tokens: `GET`, `/`, `HTTP/`, and `1.1`. This does not match the three-component structure of `method SP request-target SP HTTP-version`. +3. The lenient parsing clause in RFC 9112 Section 3 permits treating whitespace as the `SP` separator between components, but it does not permit inserting whitespace inside any component. The version is one component, not two. +4. A proxy that reconstructs the version by concatenating `HTTP/` and `1.1` would silently "fix" the request, while a strict parser would reject it. This parser differential could allow an attacker to bypass front-end validation. +5. No MAY-level relaxation exists for whitespace inside the version token, making this a clear grammar violation. + +### Scoring Justification + +This test is **scored**. The `HTTP-version` ABNF is a normative MUST-level grammar rule that requires a contiguous token with no internal whitespace. The lenient parsing clause in Section 3 does not extend to whitespace within individual request-line components. `400` or close = **Pass**, `2xx` = **Fail**. + +## Sources + +- [RFC 9112 §2.3 -- HTTP Version](https://www.rfc-editor.org/rfc/rfc9112#section-2.3) diff --git a/docs/content/docs/rfc-basics.md b/docs/content/docs/rfc-basics.md index 0fc338a..c0bd7e0 100644 --- a/docs/content/docs/rfc-basics.md +++ b/docs/content/docs/rfc-basics.md @@ -1,6 +1,6 @@ --- title: RFC Basics -description: "RFC Basics — Http11Probe documentation" +description: "An introduction to RFCs and RFC 2119 requirement levels (MUST/SHOULD/MAY), and how RFC 9110 and RFC 9112 define the HTTP/1.1 protocol." weight: 3 --- diff --git a/docs/content/docs/smuggling/_index.md b/docs/content/docs/smuggling/_index.md index a642936..e15d847 100644 --- a/docs/content/docs/smuggling/_index.md +++ b/docs/content/docs/smuggling/_index.md @@ -1,6 +1,6 @@ --- title: Request Smuggling -description: "Request Smuggling — Http11Probe documentation" +description: "Request smuggling test documentation covering Content-Length/Transfer-Encoding conflicts, obfuscated framing headers, and pipeline injection vectors." weight: 10 sidebar: open: false diff --git a/docs/content/docs/smuggling/absolute-uri-host-mismatch.md b/docs/content/docs/smuggling/absolute-uri-host-mismatch.md index 093b33c..639f916 100644 --- a/docs/content/docs/smuggling/absolute-uri-host-mismatch.md +++ b/docs/content/docs/smuggling/absolute-uri-host-mismatch.md @@ -1,86 +1,86 @@ ---- -title: "ABSOLUTE-URI-HOST-MISMATCH" -description: "ABSOLUTE-URI-HOST-MISMATCH test documentation" -weight: 57 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-ABSOLUTE-URI-HOST-MISMATCH` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2) | -| **Requirement** | Unscored | -| **Expected** | `400` or `2xx` | - -## What it sends - -A GET request using absolute-form URI with a host that differs from the Host header. - -```http -GET http://other.example.com/ HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -The request-target uses absolute-form with `other.example.com` while the Host header says `localhost:8080`. - - -## What the RFC says - -> "When an origin server receives a request with an absolute-form of request-target, the origin server MUST ignore the received Host header field (if any) and instead use the host information of the request-target." — RFC 9112 §3.2.2 - -> "A server MUST accept the absolute-form in requests even though most HTTP/1.1 clients will only send the absolute-form to a proxy." — RFC 9112 §3.2.2 - -When a server receives absolute-form, the URI host takes priority over the Host header. However, not all servers support absolute-form, and some may ignore the URI and use the Host header regardless. - -## Why this test is unscored - -The RFC requires the origin server to use the URI host from the absolute-form request-target, but not all servers support absolute-form requests. A server that rejects the request with `400` is being strict but safe, while a server that accepts and processes the request is handling the absolute-form correctly per the RFC. Both are defensible behaviors. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (handles the mismatch in some way). - -## Why it matters - -If a proxy routes requests based on the Host header (`localhost:8080`) but the origin server resolves the target based on the URI host (`other.example.com`), routing confusion occurs. An attacker can use this mismatch to access virtual hosts that should be restricted, bypass access controls, or poison caches for a different domain. This is especially dangerous in reverse proxy configurations where the proxy and origin have different URI-vs-Host precedence rules. - -## Deep Analysis - -### Relevant ABNF - -From RFC 9112 Section 3.2.2: - -``` -absolute-form = absolute-URI -``` - -The request-target in absolute-form contains the full URI including scheme and authority (host), which may differ from the `Host` header value. - -### RFC Evidence - -> "When an origin server receives a request with an absolute-form of request-target, the origin server MUST ignore the received Host header field (if any) and instead use the host information of the request-target." -- RFC 9112 Section 3.2.2 - -> "A client MUST send a Host header field in an HTTP/1.1 request even if the request-target is in absolute-form." -- RFC 9112 Section 3.2.2 - -> "When a proxy receives a request with an absolute-form of request-target, the proxy MUST ignore the received Host header field (if any) and instead replace it with the host information of the request-target." -- RFC 9112 Section 3.2.2 - -> "A server MUST accept the absolute-form in requests even though most HTTP/1.1 clients will only send the absolute-form to a proxy." -- RFC 9112 Section 3.2.2 - -### Chain of Reasoning - -1. **The specification establishes dual authority sources.** When a request uses absolute-form, the URI authority (`http://other.example.com/`) and the `Host` header (`localhost:8080`) both carry host information. RFC 9112 Section 3.2.2 resolves this by mandating that the URI authority takes precedence. - -2. **Not all implementations follow the precedence rule.** Many origin servers never see absolute-form requests in practice (clients typically send them only to proxies). As a result, some servers ignore the absolute-form URI entirely and route based on the `Host` header. This creates an inconsistency between what the RFC requires and what actually happens. - -3. **The mismatch enables routing confusion.** In a reverse proxy deployment, the proxy might resolve the URI host (`other.example.com`) for routing while the origin server uses the `Host` header (`localhost:8080`), or vice versa. This disagreement about which host is authoritative is the foundation of host-header attacks: cache poisoning (the proxy caches a response under the wrong host), SSRF (the origin processes a request intended for an internal service), and virtual host bypass (an attacker reaches a restricted vhost). - -4. **Attack scenario.** An attacker sends `GET http://internal.corp/ HTTP/1.1` with `Host: public.example.com`. If the proxy uses the Host header for routing (to `public.example.com`) but the origin server uses the URI authority, the attacker's request reaches `internal.corp` on the origin while the proxy believes it went to the public site. Alternatively, if the proxy follows the RFC and routes to `internal.corp` while the origin ignores the absolute-form and uses the Host header, the response may be cached under the wrong host key. - -### Scored / Unscored Justification - -This test is **unscored** (`Scored = false`). The RFC says the server MUST ignore the Host header and use the URI authority -- but it does not say the server MUST reject mismatches. A server that correctly prioritizes the URI authority over the Host header and responds `2xx` is technically compliant. A server that rejects the mismatch with `400` is being defensively strict. Since both behaviors are defensible under the RFC, neither can be penalized. The test flags `2xx` as a warning to surface the behavior for human review, since the routing confusion risk depends on the deployment topology, not on the server alone. - -## Sources - -- [RFC 9112 §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2) -- [RFC 9110 §7.2](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) +--- +title: "Absolute URI Host Mismatch — Request Smuggling" +description: "A GET request using absolute-form URI with a host that differs from the Host header. Tested against RFC 9112 §3.2.2." +weight: 57 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-ABSOLUTE-URI-HOST-MISMATCH` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2) | +| **Requirement** | Unscored | +| **Expected** | `400` or `2xx` | + +## What it sends + +A GET request using absolute-form URI with a host that differs from the Host header. + +```http +GET http://other.example.com/ HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +The request-target uses absolute-form with `other.example.com` while the Host header says `localhost:8080`. + + +## What the RFC says + +> "When an origin server receives a request with an absolute-form of request-target, the origin server MUST ignore the received Host header field (if any) and instead use the host information of the request-target." — RFC 9112 §3.2.2 + +> "A server MUST accept the absolute-form in requests even though most HTTP/1.1 clients will only send the absolute-form to a proxy." — RFC 9112 §3.2.2 + +When a server receives absolute-form, the URI host takes priority over the Host header. However, not all servers support absolute-form, and some may ignore the URI and use the Host header regardless. + +## Why this test is unscored + +The RFC requires the origin server to use the URI host from the absolute-form request-target, but not all servers support absolute-form requests. A server that rejects the request with `400` is being strict but safe, while a server that accepts and processes the request is handling the absolute-form correctly per the RFC. Both are defensible behaviors. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (handles the mismatch in some way). + +## Why it matters + +If a proxy routes requests based on the Host header (`localhost:8080`) but the origin server resolves the target based on the URI host (`other.example.com`), routing confusion occurs. An attacker can use this mismatch to access virtual hosts that should be restricted, bypass access controls, or poison caches for a different domain. This is especially dangerous in reverse proxy configurations where the proxy and origin have different URI-vs-Host precedence rules. + +## Deep Analysis + +### Relevant ABNF + +From RFC 9112 Section 3.2.2: + +``` +absolute-form = absolute-URI +``` + +The request-target in absolute-form contains the full URI including scheme and authority (host), which may differ from the `Host` header value. + +### RFC Evidence + +> "When an origin server receives a request with an absolute-form of request-target, the origin server MUST ignore the received Host header field (if any) and instead use the host information of the request-target." -- RFC 9112 Section 3.2.2 + +> "A client MUST send a Host header field in an HTTP/1.1 request even if the request-target is in absolute-form." -- RFC 9112 Section 3.2.2 + +> "When a proxy receives a request with an absolute-form of request-target, the proxy MUST ignore the received Host header field (if any) and instead replace it with the host information of the request-target." -- RFC 9112 Section 3.2.2 + +> "A server MUST accept the absolute-form in requests even though most HTTP/1.1 clients will only send the absolute-form to a proxy." -- RFC 9112 Section 3.2.2 + +### Chain of Reasoning + +1. **The specification establishes dual authority sources.** When a request uses absolute-form, the URI authority (`http://other.example.com/`) and the `Host` header (`localhost:8080`) both carry host information. RFC 9112 Section 3.2.2 resolves this by mandating that the URI authority takes precedence. + +2. **Not all implementations follow the precedence rule.** Many origin servers never see absolute-form requests in practice (clients typically send them only to proxies). As a result, some servers ignore the absolute-form URI entirely and route based on the `Host` header. This creates an inconsistency between what the RFC requires and what actually happens. + +3. **The mismatch enables routing confusion.** In a reverse proxy deployment, the proxy might resolve the URI host (`other.example.com`) for routing while the origin server uses the `Host` header (`localhost:8080`), or vice versa. This disagreement about which host is authoritative is the foundation of host-header attacks: cache poisoning (the proxy caches a response under the wrong host), SSRF (the origin processes a request intended for an internal service), and virtual host bypass (an attacker reaches a restricted vhost). + +4. **Attack scenario.** An attacker sends `GET http://internal.corp/ HTTP/1.1` with `Host: public.example.com`. If the proxy uses the Host header for routing (to `public.example.com`) but the origin server uses the URI authority, the attacker's request reaches `internal.corp` on the origin while the proxy believes it went to the public site. Alternatively, if the proxy follows the RFC and routes to `internal.corp` while the origin ignores the absolute-form and uses the Host header, the response may be cached under the wrong host key. + +### Scored / Unscored Justification + +This test is **unscored** (`Scored = false`). The RFC says the server MUST ignore the Host header and use the URI authority -- but it does not say the server MUST reject mismatches. A server that correctly prioritizes the URI authority over the Host header and responds `2xx` is technically compliant. A server that rejects the mismatch with `400` is being defensively strict. Since both behaviors are defensible under the RFC, neither can be penalized. The test flags `2xx` as a warning to surface the behavior for human review, since the routing confusion risk depends on the deployment topology, not on the server alone. + +## Sources + +- [RFC 9112 §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2) +- [RFC 9110 §7.2](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) diff --git a/docs/content/docs/smuggling/bare-cr-header-value.md b/docs/content/docs/smuggling/bare-cr-header-value.md index e171239..b0dea6c 100644 --- a/docs/content/docs/smuggling/bare-cr-header-value.md +++ b/docs/content/docs/smuggling/bare-cr-header-value.md @@ -1,80 +1,80 @@ ---- -title: "BARE-CR-HEADER-VALUE" -description: "BARE-CR-HEADER-VALUE test documentation" -weight: 19 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-BARE-CR-HEADER-VALUE` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | -| **Requirement** | MUST reject or replace with SP | -| **Expected** | `400` or close | - -## What it sends - -Header value containing a bare CR (0x0D not followed by LF). - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5\r\n -X-Test: val\rue\r\n -\r\n -hello -``` - -The `X-Test` header value contains a bare CR (`\r` / `0x0D`) between `val` and `ue`. - - -## What the RFC says - -> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content." — RFC 9112 §2.2 - -> "A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." — RFC 9112 §2.2 - -## Why it matters - -Bare CR in header values can cause parsers to disagree on header boundaries. A parser that treats bare CR as a line terminator may see the bytes after the CR as a new header line, while a parser that only recognizes CRLF sees them as part of the original value. If a front-end and back-end disagree on where headers begin and end, an attacker can inject headers visible to one parser but not the other -- enabling request smuggling. - -## Deep Analysis - -### Relevant ABNF - -From RFC 9112 Section 2.2, HTTP/1.1 messages use CRLF as the standard line terminator: - -``` -HTTP-message = start-line CRLF - *( field-line CRLF ) - CRLF - [ message-body ] -``` - -A bare CR (0x0D not followed by 0x0A) is not a valid protocol element outside of message content. - -### RFC Evidence - -> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content." -- RFC 9112 Section 2.2 - -> "A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." -- RFC 9112 Section 2.2 - -> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." -- RFC 9112 Section 2.2 - -### Chain of Reasoning - -1. **The RFC draws a hard line against bare CR.** The MUST NOT / MUST language in Section 2.2 is unambiguous: bare CR in protocol elements (headers, request-line, etc.) is prohibited by senders and must be treated as invalid or replaced with SP by recipients. There is no third option -- the recipient cannot silently pass it through unchanged. - -2. **Parser disagreement is the core danger.** Consider the header `X-Test: val\rue`. A parser that treats bare CR as a line terminator sees `X-Test: val` followed by a new header line starting with `ue`. A parser that treats only CRLF as a line terminator sees the entire value as `val\rue`. A parser that replaces bare CR with SP sees `val ue`. These three interpretations produce three different header sets from the same bytes on the wire. - -3. **The split enables header injection.** If an attacker places `\rEvil-Header: payload` inside a header value, a CR-as-terminator parser will see a new header `Evil-Header: payload` that is invisible to parsers using CRLF. This is particularly dangerous when the front-end proxy forwards the bare CR unchanged while the back-end splits on it, allowing attacker-controlled headers to reach the back-end. - -4. **Attack scenario.** An attacker sends `X-Forwarded-For: 127.0.0.1\rTransfer-Encoding: chunked`. The proxy sees one header (`X-Forwarded-For`) with a strange value. The back-end, splitting on bare CR, sees two headers -- including `Transfer-Encoding: chunked` -- enabling a CL.TE smuggling attack that the proxy never detected. - -### Scored / Unscored Justification - -This test is **scored**. The RFC uses double-MUST language: senders MUST NOT generate bare CR, and recipients MUST either reject the element as invalid or replace bare CR with SP. There is no MAY or SHOULD qualifier -- the requirement is absolute. A server that silently passes through bare CR unchanged violates a MUST-level requirement, making it appropriate to score this test and fail servers that do not reject or sanitize. - -## Sources - -- [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) +--- +title: "Bare CR Header Value — Request Smuggling" +description: "Header value containing a bare CR (0x0D not followed by LF). Tested against RFC 9112 §2.2." +weight: 19 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-BARE-CR-HEADER-VALUE` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | +| **Requirement** | MUST reject or replace with SP | +| **Expected** | `400` or close | + +## What it sends + +Header value containing a bare CR (0x0D not followed by LF). + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +X-Test: val\rue\r\n +\r\n +hello +``` + +The `X-Test` header value contains a bare CR (`\r` / `0x0D`) between `val` and `ue`. + + +## What the RFC says + +> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content." — RFC 9112 §2.2 + +> "A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." — RFC 9112 §2.2 + +## Why it matters + +Bare CR in header values can cause parsers to disagree on header boundaries. A parser that treats bare CR as a line terminator may see the bytes after the CR as a new header line, while a parser that only recognizes CRLF sees them as part of the original value. If a front-end and back-end disagree on where headers begin and end, an attacker can inject headers visible to one parser but not the other -- enabling request smuggling. + +## Deep Analysis + +### Relevant ABNF + +From RFC 9112 Section 2.2, HTTP/1.1 messages use CRLF as the standard line terminator: + +``` +HTTP-message = start-line CRLF + *( field-line CRLF ) + CRLF + [ message-body ] +``` + +A bare CR (0x0D not followed by 0x0A) is not a valid protocol element outside of message content. + +### RFC Evidence + +> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content." -- RFC 9112 Section 2.2 + +> "A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." -- RFC 9112 Section 2.2 + +> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." -- RFC 9112 Section 2.2 + +### Chain of Reasoning + +1. **The RFC draws a hard line against bare CR.** The MUST NOT / MUST language in Section 2.2 is unambiguous: bare CR in protocol elements (headers, request-line, etc.) is prohibited by senders and must be treated as invalid or replaced with SP by recipients. There is no third option -- the recipient cannot silently pass it through unchanged. + +2. **Parser disagreement is the core danger.** Consider the header `X-Test: val\rue`. A parser that treats bare CR as a line terminator sees `X-Test: val` followed by a new header line starting with `ue`. A parser that treats only CRLF as a line terminator sees the entire value as `val\rue`. A parser that replaces bare CR with SP sees `val ue`. These three interpretations produce three different header sets from the same bytes on the wire. + +3. **The split enables header injection.** If an attacker places `\rEvil-Header: payload` inside a header value, a CR-as-terminator parser will see a new header `Evil-Header: payload` that is invisible to parsers using CRLF. This is particularly dangerous when the front-end proxy forwards the bare CR unchanged while the back-end splits on it, allowing attacker-controlled headers to reach the back-end. + +4. **Attack scenario.** An attacker sends `X-Forwarded-For: 127.0.0.1\rTransfer-Encoding: chunked`. The proxy sees one header (`X-Forwarded-For`) with a strange value. The back-end, splitting on bare CR, sees two headers -- including `Transfer-Encoding: chunked` -- enabling a CL.TE smuggling attack that the proxy never detected. + +### Scored / Unscored Justification + +This test is **scored**. The RFC uses double-MUST language: senders MUST NOT generate bare CR, and recipients MUST either reject the element as invalid or replace bare CR with SP. There is no MAY or SHOULD qualifier -- the requirement is absolute. A server that silently passes through bare CR unchanged violates a MUST-level requirement, making it appropriate to score this test and fail servers that do not reject or sanitize. + +## Sources + +- [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) diff --git a/docs/content/docs/smuggling/chunk-bare-cr-term.md b/docs/content/docs/smuggling/chunk-bare-cr-term.md index bf236ea..ff0b8c8 100644 --- a/docs/content/docs/smuggling/chunk-bare-cr-term.md +++ b/docs/content/docs/smuggling/chunk-bare-cr-term.md @@ -1,101 +1,101 @@ ---- -title: "CHUNK-BARE-CR-TERM" -description: "CHUNK-BARE-CR-TERM test documentation" -weight: 53 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-BARE-CR-TERM` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -A chunked request where the chunk size line is terminated by bare CR (`\r`) without LF. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -\r\n -5\r -hello\r\n -0\r\n -\r\n -``` - -The chunk size `5` is followed by a bare CR (`\r`) instead of the required CRLF (`\r\n`). The bytes `hello` immediately follow the bare CR. - - -## What the RFC says - -The chunk line grammar requires CRLF terminators: - -> chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF -> -> — RFC 9112 §7.1 - -And RFC 9112 §2.2 explicitly forbids bare CR: - -> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content. A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." -> -> — RFC 9112 §2.2 - -Note: the RFC permits MAY-accept for bare LF (§2.2), but makes no such allowance for bare CR. A bare CR in a chunk-size line is explicitly invalid. - -## Why it matters - -Some parsers treat CR alone as a line ending (a behavior inherited from old Mac-style line endings). If one parser accepts bare CR as a chunk-size terminator and another requires CRLF, they disagree on where the chunk data begins. The strict parser sees `5\rhello` as a malformed chunk size (containing `\r`, `h`, `e`, `l`, `l`, `o`), while the lenient parser sees chunk size 5 followed by `hello` as chunk data. This boundary disagreement enables chunk-level desynchronization. - -## Deep Analysis - -### Relevant ABNF (RFC 9112 §7.1) - -``` -chunked-body = *chunk last-chunk trailer-section CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG -chunk-data = 1*OCTET ; a sequence of chunk-size octets -last-chunk = 1*("0") [ chunk-ext ] CRLF -``` - -### RFC Evidence - -**RFC 9112 §7.1** defines the `chunk` production as requiring CRLF in two positions -- after the chunk-size line and after the chunk-data: - -> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" - -**RFC 9112 §2.2** explicitly addresses bare CR: - -> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content. A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." - -**RFC 9112 §2.2** also includes a MAY-level allowance for bare LF, but notably provides **no such allowance for bare CR**: - -> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." - -### Step-by-Step ABNF Violation - -1. The parser begins reading a `chunk` and expects `chunk-size`, which is `1*HEXDIG`. It reads `5` -- valid so far. -2. Next the parser expects either `chunk-ext` (starting with `;`) or `CRLF` (the sequence `\r\n`). -3. The parser encounters `\r` (0x0D). This could be the start of CRLF, so it looks for `\n` (0x0A) next. -4. Instead of `\n`, the next byte is `h` (0x68, the start of `hello`). The `\r` is therefore a **bare CR** -- it is not followed by LF. -5. The ABNF requires `CRLF` at this position. A bare CR does not satisfy the `CRLF` production. The parse fails. -6. Per §2.2, the recipient MUST either consider the element invalid (reject with 400) or replace the bare CR with SP. Replacing with SP yields ` 5 hello`, which is not a valid chunk-size line either. - -### Real-World Smuggling Scenario - -Bare CR as a line terminator is a legacy behavior from classic Mac OS (pre-OS X). Some parsers inherited from that era treat `\r` alone as a line ending. - -**Attack vector:** An attacker sends `5\rhello\r\n0\r\n\r\n`. A lenient parser that treats bare CR as a line terminator reads chunk-size `5`, then reads 5 bytes of chunk data (`hello`), and processes the message normally. A strict parser rejects the message or interprets it differently -- for example, it may try to parse `5\rhello` as a single chunk-size token, fail, and close the connection. If the lenient parser is a front-end proxy and the strict parser is the back-end, the front-end forwards a request the back-end cannot parse, potentially leaving leftover bytes in the connection that get prepended to the next request. - -This class of bare-CR chunk terminator confusion is closely related to the techniques described in research on HTTP request smuggling via chunked encoding ambiguities (e.g., the "TERM" vector class identified by James Kettle's HTTP/2 downgrade smuggling research). - -## Sources - -- [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunk Bare CR Term — Request Smuggling" +description: "A chunked request where the chunk size line is terminated by bare CR (\\r) without LF. Tested against RFC 9112 §2.2." +weight: 53 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-BARE-CR-TERM` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +A chunked request where the chunk size line is terminated by bare CR (`\r`) without LF. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r +hello\r\n +0\r\n +\r\n +``` + +The chunk size `5` is followed by a bare CR (`\r`) instead of the required CRLF (`\r\n`). The bytes `hello` immediately follow the bare CR. + + +## What the RFC says + +The chunk line grammar requires CRLF terminators: + +> chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF +> +> — RFC 9112 §7.1 + +And RFC 9112 §2.2 explicitly forbids bare CR: + +> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content. A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." +> +> — RFC 9112 §2.2 + +Note: the RFC permits MAY-accept for bare LF (§2.2), but makes no such allowance for bare CR. A bare CR in a chunk-size line is explicitly invalid. + +## Why it matters + +Some parsers treat CR alone as a line ending (a behavior inherited from old Mac-style line endings). If one parser accepts bare CR as a chunk-size terminator and another requires CRLF, they disagree on where the chunk data begins. The strict parser sees `5\rhello` as a malformed chunk size (containing `\r`, `h`, `e`, `l`, `l`, `o`), while the lenient parser sees chunk size 5 followed by `hello` as chunk data. This boundary disagreement enables chunk-level desynchronization. + +## Deep Analysis + +### Relevant ABNF (RFC 9112 §7.1) + +``` +chunked-body = *chunk last-chunk trailer-section CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG +chunk-data = 1*OCTET ; a sequence of chunk-size octets +last-chunk = 1*("0") [ chunk-ext ] CRLF +``` + +### RFC Evidence + +**RFC 9112 §7.1** defines the `chunk` production as requiring CRLF in two positions -- after the chunk-size line and after the chunk-data: + +> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" + +**RFC 9112 §2.2** explicitly addresses bare CR: + +> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content. A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." + +**RFC 9112 §2.2** also includes a MAY-level allowance for bare LF, but notably provides **no such allowance for bare CR**: + +> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." + +### Step-by-Step ABNF Violation + +1. The parser begins reading a `chunk` and expects `chunk-size`, which is `1*HEXDIG`. It reads `5` -- valid so far. +2. Next the parser expects either `chunk-ext` (starting with `;`) or `CRLF` (the sequence `\r\n`). +3. The parser encounters `\r` (0x0D). This could be the start of CRLF, so it looks for `\n` (0x0A) next. +4. Instead of `\n`, the next byte is `h` (0x68, the start of `hello`). The `\r` is therefore a **bare CR** -- it is not followed by LF. +5. The ABNF requires `CRLF` at this position. A bare CR does not satisfy the `CRLF` production. The parse fails. +6. Per §2.2, the recipient MUST either consider the element invalid (reject with 400) or replace the bare CR with SP. Replacing with SP yields ` 5 hello`, which is not a valid chunk-size line either. + +### Real-World Smuggling Scenario + +Bare CR as a line terminator is a legacy behavior from classic Mac OS (pre-OS X). Some parsers inherited from that era treat `\r` alone as a line ending. + +**Attack vector:** An attacker sends `5\rhello\r\n0\r\n\r\n`. A lenient parser that treats bare CR as a line terminator reads chunk-size `5`, then reads 5 bytes of chunk data (`hello`), and processes the message normally. A strict parser rejects the message or interprets it differently -- for example, it may try to parse `5\rhello` as a single chunk-size token, fail, and close the connection. If the lenient parser is a front-end proxy and the strict parser is the back-end, the front-end forwards a request the back-end cannot parse, potentially leaving leftover bytes in the connection that get prepended to the next request. + +This class of bare-CR chunk terminator confusion is closely related to the techniques described in research on HTTP request smuggling via chunked encoding ambiguities (e.g., the "TERM" vector class identified by James Kettle's HTTP/2 downgrade smuggling research). + +## Sources + +- [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/smuggling/chunk-bare-semicolon.md b/docs/content/docs/smuggling/chunk-bare-semicolon.md index c4e1a46..2d6fd85 100644 --- a/docs/content/docs/smuggling/chunk-bare-semicolon.md +++ b/docs/content/docs/smuggling/chunk-bare-semicolon.md @@ -1,104 +1,104 @@ ---- -title: "CHUNK-BARE-SEMICOLON" -description: "CHUNK-BARE-SEMICOLON test documentation" -weight: 18 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-BARE-SEMICOLON` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -Chunk size `5;` with a semicolon but no extension name. - -```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 -\r\n -``` - -The chunk size line `5;` has a semicolon but no extension name after it. - - -## What the RFC says - -> chunk-ext = *( BWS ";" BWS chunk-ext-name [ BWS "=" BWS chunk-ext-val ] ) -> -> chunk-ext-name = token -> -> — RFC 9112 §7.1.1 - -The grammar requires a `chunk-ext-name` (which is a `token`, i.e., one or more `tchar` characters) after each semicolon. A bare semicolon with no extension name does not match the production and is therefore invalid. - -## Why it matters - -A bare semicolon can cause parser confusion about chunk boundaries. A lenient parser might skip the empty extension and parse the chunk normally, while a strict parser rejects the line. If these two parsers sit in sequence (front-end / back-end), they disagree on whether the message is valid, enabling request smuggling. - -## Deep Analysis - -### Relevant ABNF (RFC 9112 §7.1 and §7.1.1) - -``` -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG - -chunk-ext = *( BWS ";" BWS chunk-ext-name - [ BWS "=" BWS chunk-ext-val ] ) -chunk-ext-name = token -chunk-ext-val = token / quoted-string - -token = 1*tchar -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" - / "-" / "." / "^" / "_" / "`" / "|" / "~" - / DIGIT / ALPHA -``` - -### RFC Evidence - -**RFC 9112 §7.1.1** defines the chunk extension grammar: - -> "chunk-ext = *( BWS ';' BWS chunk-ext-name [ BWS '=' BWS chunk-ext-val ] )" - -This means after the semicolon delimiter and optional whitespace, a `chunk-ext-name` is **mandatory**. The `chunk-ext-name` is defined as `token`, which is `1*tchar` -- it requires at least one character. - -**RFC 9112 §7.1.1** also states: - -> "A recipient MUST ignore unrecognized chunk extensions." - -This applies to well-formed but unknown extensions, not to syntactically invalid ones like a bare semicolon with no name. - -**RFC 9112 §7.1.1** further notes: - -> "A server ought to limit the total length of chunk extensions received in a request to an amount reasonable for the services provided, in the same way that it applies length limitations and timeouts for other parts of a message, and generate an appropriate 4xx (Client Error) response if that amount is exceeded." - -### Step-by-Step ABNF Violation - -1. The parser reads `chunk-size` and gets `5` (valid HEXDIG). -2. The parser encounters `;` -- this starts a `chunk-ext` production. -3. Inside `chunk-ext`, after the `;` and optional BWS, the parser expects `chunk-ext-name`, which is `token` = `1*tchar`. -4. The next character is `\r` (0x0D, start of CRLF). The character `\r` is not a `tchar` -- it is a control character. -5. A `token` requires **at least one** `tchar`. Zero `tchar` characters means the `chunk-ext-name` production fails. -6. Since the `chunk-ext` production cannot be satisfied, the entire `chunk` production fails. The message is syntactically invalid. - -### Real-World Smuggling Scenario - -A bare semicolon creates ambiguity in how parsers determine chunk boundaries: - -**Attack vector:** A front-end proxy encounters `5;\r\n` and strips the empty extension, forwarding it as `5\r\n` followed by 5 bytes of chunk data. A back-end parser sees the raw `5;\r\n` and either (a) rejects it, causing the connection to desynchronize, or (b) interprets the semicolon differently -- some parsers treat the semicolon as the start of an extension and scan forward for the name, potentially consuming the CRLF and chunk data bytes as part of the extension name. - -This type of chunk extension parsing ambiguity was documented in the PortSwigger research on HTTP request smuggling, where malformed chunk extensions caused front-end/back-end disagreements on message framing. CVE-2023-44487 and related HTTP/2-to-HTTP/1.1 downgrade issues demonstrated that chunk extension handling inconsistencies are a practical attack surface. - -## Sources - -- [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) +--- +title: "Chunk Bare Semicolon — Request Smuggling" +description: "Chunk size 5; with a semicolon but no extension name. Tested against RFC 9112 §7.1.1." +weight: 18 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-BARE-SEMICOLON` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +Chunk size `5;` with a semicolon but no extension name. + +```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 +\r\n +``` + +The chunk size line `5;` has a semicolon but no extension name after it. + + +## What the RFC says + +> chunk-ext = *( BWS ";" BWS chunk-ext-name [ BWS "=" BWS chunk-ext-val ] ) +> +> chunk-ext-name = token +> +> — RFC 9112 §7.1.1 + +The grammar requires a `chunk-ext-name` (which is a `token`, i.e., one or more `tchar` characters) after each semicolon. A bare semicolon with no extension name does not match the production and is therefore invalid. + +## Why it matters + +A bare semicolon can cause parser confusion about chunk boundaries. A lenient parser might skip the empty extension and parse the chunk normally, while a strict parser rejects the line. If these two parsers sit in sequence (front-end / back-end), they disagree on whether the message is valid, enabling request smuggling. + +## Deep Analysis + +### Relevant ABNF (RFC 9112 §7.1 and §7.1.1) + +``` +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG + +chunk-ext = *( BWS ";" BWS chunk-ext-name + [ BWS "=" BWS chunk-ext-val ] ) +chunk-ext-name = token +chunk-ext-val = token / quoted-string + +token = 1*tchar +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" + / "-" / "." / "^" / "_" / "`" / "|" / "~" + / DIGIT / ALPHA +``` + +### RFC Evidence + +**RFC 9112 §7.1.1** defines the chunk extension grammar: + +> "chunk-ext = *( BWS ';' BWS chunk-ext-name [ BWS '=' BWS chunk-ext-val ] )" + +This means after the semicolon delimiter and optional whitespace, a `chunk-ext-name` is **mandatory**. The `chunk-ext-name` is defined as `token`, which is `1*tchar` -- it requires at least one character. + +**RFC 9112 §7.1.1** also states: + +> "A recipient MUST ignore unrecognized chunk extensions." + +This applies to well-formed but unknown extensions, not to syntactically invalid ones like a bare semicolon with no name. + +**RFC 9112 §7.1.1** further notes: + +> "A server ought to limit the total length of chunk extensions received in a request to an amount reasonable for the services provided, in the same way that it applies length limitations and timeouts for other parts of a message, and generate an appropriate 4xx (Client Error) response if that amount is exceeded." + +### Step-by-Step ABNF Violation + +1. The parser reads `chunk-size` and gets `5` (valid HEXDIG). +2. The parser encounters `;` -- this starts a `chunk-ext` production. +3. Inside `chunk-ext`, after the `;` and optional BWS, the parser expects `chunk-ext-name`, which is `token` = `1*tchar`. +4. The next character is `\r` (0x0D, start of CRLF). The character `\r` is not a `tchar` -- it is a control character. +5. A `token` requires **at least one** `tchar`. Zero `tchar` characters means the `chunk-ext-name` production fails. +6. Since the `chunk-ext` production cannot be satisfied, the entire `chunk` production fails. The message is syntactically invalid. + +### Real-World Smuggling Scenario + +A bare semicolon creates ambiguity in how parsers determine chunk boundaries: + +**Attack vector:** A front-end proxy encounters `5;\r\n` and strips the empty extension, forwarding it as `5\r\n` followed by 5 bytes of chunk data. A back-end parser sees the raw `5;\r\n` and either (a) rejects it, causing the connection to desynchronize, or (b) interprets the semicolon differently -- some parsers treat the semicolon as the start of an extension and scan forward for the name, potentially consuming the CRLF and chunk data bytes as part of the extension name. + +This type of chunk extension parsing ambiguity was documented in the PortSwigger research on HTTP request smuggling, where malformed chunk extensions caused front-end/back-end disagreements on message framing. CVE-2023-44487 and related HTTP/2-to-HTTP/1.1 downgrade issues demonstrated that chunk extension handling inconsistencies are a practical attack surface. + +## Sources + +- [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) diff --git a/docs/content/docs/smuggling/chunk-ext-cr.md b/docs/content/docs/smuggling/chunk-ext-cr.md index 4d9cf4c..8db2bb7 100644 --- a/docs/content/docs/smuggling/chunk-ext-cr.md +++ b/docs/content/docs/smuggling/chunk-ext-cr.md @@ -1,37 +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) +--- +title: "Chunk Ext CR — Request Smuggling" +description: "A chunk-size line where a bare CR appears inside the extension area, not as a valid CRLF terminator. Tested against RFC 9112 §7.1.1." +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/chunk-ext-ctrl.md b/docs/content/docs/smuggling/chunk-ext-ctrl.md index c219bae..739d6c4 100644 --- a/docs/content/docs/smuggling/chunk-ext-ctrl.md +++ b/docs/content/docs/smuggling/chunk-ext-ctrl.md @@ -1,113 +1,113 @@ ---- -title: "CHUNK-EXT-CTRL" -description: "CHUNK-EXT-CTRL test documentation" -weight: 28 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-EXT-CTRL` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -A chunked request with a NUL byte (`0x00`) embedded in the chunk extension: `5;\x00ext`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -\r\n -5;\x00ext\r\n -hello\r\n -0\r\n -\r\n -``` - -The chunk extension contains a NUL byte (`\x00`) before `ext`. - - -## What the RFC says - -> chunk-ext = *( BWS ";" BWS chunk-ext-name [ BWS "=" BWS chunk-ext-val ] ) -> -> chunk-ext-name = token -> -> chunk-ext-val = token / quoted-string -> -> — RFC 9112 §7.1.1 - -A `token` is defined as `1*tchar`, where `tchar` only includes visible ASCII characters and a limited set of symbols (RFC 9110 §5.6.2). NUL (`0x00`) and other control characters (except HTAB in specific contexts) are not valid `tchar` characters and therefore cannot appear in a chunk extension name or unquoted value. - -## Why it matters - -NUL bytes in chunk extensions can cause parsers to truncate or misinterpret the extension, leading to disagreements about chunk boundaries. C-based string functions often treat NUL as a string terminator, creating divergent behavior between parsers. - -## Deep Analysis - -### Relevant ABNF (RFC 9112 §7.1 and §7.1.1, RFC 9110 §5.6.2) - -``` -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG - -chunk-ext = *( BWS ";" BWS chunk-ext-name - [ BWS "=" BWS chunk-ext-val ] ) -chunk-ext-name = token -chunk-ext-val = token / quoted-string - -token = 1*tchar -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" - / "-" / "." / "^" / "_" / "`" / "|" / "~" - / DIGIT / ALPHA -``` - -### RFC Evidence - -**RFC 9110 §5.6.2** defines `token` as `1*tchar`, where `tchar` is an exhaustive list of permitted characters: - -> "tchar = '!' / '#' / '$' / '%' / '&' / ''' / '*' / '+' / '-' / '.' / '^' / '_' / '`' / '|' / '~' / DIGIT / ALPHA" - -NUL (0x00) is not in this list. No control characters other than HTAB are valid in HTTP token positions. - -**RFC 9112 §7.1.1** specifies: - -> "chunk-ext-name = token" - -And provides context: - -> "A recipient MUST ignore unrecognized chunk extensions." - -This "ignore" directive applies to syntactically valid extensions with unrecognized names, not to extensions containing illegal characters. - -**RFC 9112 §7.1.1** also states: - -> "A server ought to limit the total length of chunk extensions received in a request to an amount reasonable for the services provided, in the same way that it applies length limitations and timeouts for other parts of a message, and generate an appropriate 4xx (Client Error) response if that amount is exceeded." - -### Step-by-Step ABNF Violation - -1. The parser reads `chunk-size` = `5` (valid HEXDIG). -2. The parser encounters `;` -- this starts a `chunk-ext` production. -3. After `;` and optional BWS, the parser expects `chunk-ext-name` = `token` = `1*tchar`. -4. The next byte is `\x00` (NUL). NUL is not a `tchar` -- it is not in the exhaustive list of permitted characters. -5. Since the first character fails to match `tchar`, the `token` production requires at least one `tchar`, so `chunk-ext-name` fails. -6. The `chunk-ext` production cannot be satisfied, and the entire `chunk` production is invalid. - -### Real-World Smuggling Scenario - -NUL bytes are particularly dangerous because of how C-based parsers handle them: - -**Attack vector:** An attacker sends `5;\x00ext\r\n`. A C-based parser using `strlen()` or similar string functions may treat the NUL byte as a string terminator, seeing only `5;` (effectively a bare semicolon). It might then ignore the truncated extension and parse the chunk normally. Meanwhile, a parser that processes the raw byte stream sees the NUL as an invalid character and rejects the message. This disagreement enables desynchronization. - -**Memory safety implications:** NUL bytes in unexpected positions have historically caused buffer over-reads and information disclosure in HTTP servers. CVE-2019-5482 (curl) involved NUL byte handling in TFTP URLs, and similar NUL injection issues have been found in HTTP parsers where control characters cause truncation or bypass validation logic. - -The broader class of control-character injection in chunk extensions was identified as a smuggling vector in chunked encoding research, where parsers disagree on whether to reject, truncate, or pass through control characters in extension fields. - -## Sources - -- [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) -- [RFC 9110 §5.6.2](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.2) +--- +title: "Chunk Ext Ctrl — Request Smuggling" +description: "A chunked request with a NUL byte (0x00) embedded in the chunk extension: 5;\\x00ext. Tested against RFC 9112 §7.1.1." +weight: 28 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-EXT-CTRL` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +A chunked request with a NUL byte (`0x00`) embedded in the chunk extension: `5;\x00ext`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +5;\x00ext\r\n +hello\r\n +0\r\n +\r\n +``` + +The chunk extension contains a NUL byte (`\x00`) before `ext`. + + +## What the RFC says + +> chunk-ext = *( BWS ";" BWS chunk-ext-name [ BWS "=" BWS chunk-ext-val ] ) +> +> chunk-ext-name = token +> +> chunk-ext-val = token / quoted-string +> +> — RFC 9112 §7.1.1 + +A `token` is defined as `1*tchar`, where `tchar` only includes visible ASCII characters and a limited set of symbols (RFC 9110 §5.6.2). NUL (`0x00`) and other control characters (except HTAB in specific contexts) are not valid `tchar` characters and therefore cannot appear in a chunk extension name or unquoted value. + +## Why it matters + +NUL bytes in chunk extensions can cause parsers to truncate or misinterpret the extension, leading to disagreements about chunk boundaries. C-based string functions often treat NUL as a string terminator, creating divergent behavior between parsers. + +## Deep Analysis + +### Relevant ABNF (RFC 9112 §7.1 and §7.1.1, RFC 9110 §5.6.2) + +``` +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG + +chunk-ext = *( BWS ";" BWS chunk-ext-name + [ BWS "=" BWS chunk-ext-val ] ) +chunk-ext-name = token +chunk-ext-val = token / quoted-string + +token = 1*tchar +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" + / "-" / "." / "^" / "_" / "`" / "|" / "~" + / DIGIT / ALPHA +``` + +### RFC Evidence + +**RFC 9110 §5.6.2** defines `token` as `1*tchar`, where `tchar` is an exhaustive list of permitted characters: + +> "tchar = '!' / '#' / '$' / '%' / '&' / ''' / '*' / '+' / '-' / '.' / '^' / '_' / '`' / '|' / '~' / DIGIT / ALPHA" + +NUL (0x00) is not in this list. No control characters other than HTAB are valid in HTTP token positions. + +**RFC 9112 §7.1.1** specifies: + +> "chunk-ext-name = token" + +And provides context: + +> "A recipient MUST ignore unrecognized chunk extensions." + +This "ignore" directive applies to syntactically valid extensions with unrecognized names, not to extensions containing illegal characters. + +**RFC 9112 §7.1.1** also states: + +> "A server ought to limit the total length of chunk extensions received in a request to an amount reasonable for the services provided, in the same way that it applies length limitations and timeouts for other parts of a message, and generate an appropriate 4xx (Client Error) response if that amount is exceeded." + +### Step-by-Step ABNF Violation + +1. The parser reads `chunk-size` = `5` (valid HEXDIG). +2. The parser encounters `;` -- this starts a `chunk-ext` production. +3. After `;` and optional BWS, the parser expects `chunk-ext-name` = `token` = `1*tchar`. +4. The next byte is `\x00` (NUL). NUL is not a `tchar` -- it is not in the exhaustive list of permitted characters. +5. Since the first character fails to match `tchar`, the `token` production requires at least one `tchar`, so `chunk-ext-name` fails. +6. The `chunk-ext` production cannot be satisfied, and the entire `chunk` production is invalid. + +### Real-World Smuggling Scenario + +NUL bytes are particularly dangerous because of how C-based parsers handle them: + +**Attack vector:** An attacker sends `5;\x00ext\r\n`. A C-based parser using `strlen()` or similar string functions may treat the NUL byte as a string terminator, seeing only `5;` (effectively a bare semicolon). It might then ignore the truncated extension and parse the chunk normally. Meanwhile, a parser that processes the raw byte stream sees the NUL as an invalid character and rejects the message. This disagreement enables desynchronization. + +**Memory safety implications:** NUL bytes in unexpected positions have historically caused buffer over-reads and information disclosure in HTTP servers. CVE-2019-5482 (curl) involved NUL byte handling in TFTP URLs, and similar NUL injection issues have been found in HTTP parsers where control characters cause truncation or bypass validation logic. + +The broader class of control-character injection in chunk extensions was identified as a smuggling vector in chunked encoding research, where parsers disagree on whether to reject, truncate, or pass through control characters in extension fields. + +## Sources + +- [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) +- [RFC 9110 §5.6.2](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.2) diff --git a/docs/content/docs/smuggling/chunk-ext-invalid-token.md b/docs/content/docs/smuggling/chunk-ext-invalid-token.md index 8bcac09..3a44689 100644 --- a/docs/content/docs/smuggling/chunk-ext-invalid-token.md +++ b/docs/content/docs/smuggling/chunk-ext-invalid-token.md @@ -1,46 +1,46 @@ ---- -title: "CHUNK-EXT-INVALID-TOKEN" -description: "SMUG-CHUNK-EXT-INVALID-TOKEN test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-EXT-INVALID-TOKEN` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) | -| **Requirement** | Implicit MUST (grammar violation) | -| **Expected** | `400` or close | - -## What it sends - -A chunk extension with an invalid token character in the extension name (`bad[`): - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -\r\n -5;bad[=x\r\n -hello\r\n -0\r\n -\r\n -``` - -## What the RFC says - -> "chunk-ext-name = token" -- RFC 9112 Section 7.1.1 - -`[` is not a valid token character, so the extension syntax is invalid. - -## Partial Coverage Note - -Existing tests already cover malformed chunk extensions (`SMUG-CHUNK-BARE-SEMICOLON`, `SMUG-CHUNK-EXT-CTRL`, `SMUG-CHUNK-EXT-CR`, `SMUG-CHUNK-EXT-LF`). This case specifically targets invalid token characters in extension names. - -## Why it matters - -Different extension parsers may tokenize this differently, creating front-end/back-end framing inconsistencies. - -## Sources - -- [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) +--- +title: "Chunk Ext Invalid Token — Request Smuggling" +description: "A chunk extension with an invalid token character in the extension name (bad[):. Tested against RFC 9112 §7.1.1." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-EXT-INVALID-TOKEN` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) | +| **Requirement** | Implicit MUST (grammar violation) | +| **Expected** | `400` or close | + +## What it sends + +A chunk extension with an invalid token character in the extension name (`bad[`): + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +5;bad[=x\r\n +hello\r\n +0\r\n +\r\n +``` + +## What the RFC says + +> "chunk-ext-name = token" -- RFC 9112 Section 7.1.1 + +`[` is not a valid token character, so the extension syntax is invalid. + +## Partial Coverage Note + +Existing tests already cover malformed chunk extensions (`SMUG-CHUNK-BARE-SEMICOLON`, `SMUG-CHUNK-EXT-CTRL`, `SMUG-CHUNK-EXT-CR`, `SMUG-CHUNK-EXT-LF`). This case specifically targets invalid token characters in extension names. + +## Why it matters + +Different extension parsers may tokenize this differently, creating front-end/back-end framing inconsistencies. + +## Sources + +- [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) diff --git a/docs/content/docs/smuggling/chunk-ext-lf.md b/docs/content/docs/smuggling/chunk-ext-lf.md index 17bcf52..fb0505e 100644 --- a/docs/content/docs/smuggling/chunk-ext-lf.md +++ b/docs/content/docs/smuggling/chunk-ext-lf.md @@ -1,119 +1,119 @@ ---- -title: "CHUNK-EXT-LF" -description: "CHUNK-EXT-LF test documentation" -weight: 25 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-EXT-LF` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) | -| **Requirement** | MAY accept bare LF | -| **Expected** | `400` or `2xx` | - -## What it sends - -A chunked request where the chunk extension area contains a bare `LF` instead of `CRLF`: `5;\nhello`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -\r\n -5;\n -hello\r\n -0\r\n -\r\n -``` - -The chunk size line `5;` is terminated with bare LF (`\n`) instead of CRLF. - - -## What the RFC says - -The chunk line grammar requires CRLF as the terminator: - -> chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF -> -> — RFC 9112 §7.1 - -However, RFC 9112 §2.2 provides a MAY-level allowance: - -> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." -> -> — RFC 9112 §2.2 - -This means a server MAY accept bare LF -- both strict rejection and lenient acceptance are RFC-compliant. - -## Why this test is unscored - -The MAY-level allowance for bare LF in RFC 9112 Section 2.2 means both strict rejection and lenient acceptance are RFC-compliant behaviors. Neither response can be considered wrong, so the test cannot be scored. - -**Pass:** Server rejects with `400` (strict CRLF enforcement, safest behavior). -**Warn:** Server accepts and responds `2xx` (RFC-valid per Section 2.2 MAY-level bare LF acceptance). - -## Why it matters - -This is the **TERM.EXT** vector from chunked encoding research. If a parser accepts bare LF in chunk extensions, it may parse chunk boundaries differently from a strict parser, enabling desynchronization and smuggling. - -## Deep Analysis - -### Relevant ABNF (RFC 9112 §7.1 and §2.2) - -``` -chunked-body = *chunk last-chunk trailer-section CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG - -chunk-ext = *( BWS ";" BWS chunk-ext-name - [ BWS "=" BWS chunk-ext-val ] ) -chunk-ext-name = token -chunk-ext-val = token / quoted-string -``` - -### RFC Evidence - -**RFC 9112 §7.1** defines the chunk production with CRLF terminators: - -> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" - -The `CRLF` production is defined in RFC 5234 §B.1 as the two-byte sequence `\r\n` (0x0D 0x0A). A bare LF (0x0A alone) does not match `CRLF`. - -**RFC 9112 §2.2** provides a MAY-level robustness allowance: - -> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." - -This MAY language means recipients are permitted but not required to accept bare LF. Both strict rejection and lenient acceptance are RFC-compliant. - -**RFC 9112 §2.2** draws an explicit asymmetry between bare LF and bare CR: - -> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content. A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." - -Bare LF gets a MAY-accept allowance; bare CR gets a MUST-reject requirement. This test targets the bare LF case. - -### Step-by-Step ABNF Analysis - -1. The parser reads `chunk-size` = `5` (valid HEXDIG). -2. The parser encounters `;` -- this starts a `chunk-ext` production. -3. After the semicolon, the parser expects `chunk-ext-name` (a `token`). But the next byte is `\n` (0x0A). -4. `\n` is not a `tchar`, so it cannot begin a `token`. The `chunk-ext-name` production fails. -5. However, per §2.2 MAY language, a recipient MAY recognize bare LF as a line terminator. If the parser does so, it effectively treats `5;\n` as `5;\r\n` -- a chunk-size line with an empty/absent extension terminated by a line ending. -6. Under strict parsing: the bare LF is not CRLF, and the extension name after `;` is missing. The parse fails at two levels. -7. Under lenient parsing: the bare LF acts as a line terminator, the bare semicolon may be ignored, and the parser reads 5 bytes of chunk data. - -### Real-World Smuggling Scenario - -This is the **TERM.EXT** vector from chunked encoding smuggling research: - -**Attack vector:** An attacker sends `5;\nhello\r\n0\r\n\r\n`. A lenient front-end proxy that accepts bare LF as a line terminator parses this as: chunk-size 5, skip empty extension, read `hello` as chunk data, then read the `0\r\n\r\n` terminator. It forwards the processed request to the back-end. A strict back-end that requires CRLF sees the bare LF differently -- it may interpret `;\nhello\r\n` as a malformed extension containing raw bytes, fail to find the CRLF terminator at the expected position, and reject or misparse the message. - -This exact technique was demonstrated in research by James Kettle on HTTP/2 downgrade smuggling, where HTTP/2 front-ends converted requests to HTTP/1.1 with bare LF terminators that back-end servers interpreted differently. The bare-LF-in-extension variant specifically exploits the fact that the §2.2 MAY language creates implementation-defined behavior, guaranteeing disagreement between strict and lenient parsers. - -## Sources - -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) -- [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) +--- +title: "Chunk Ext LF — Request Smuggling" +description: "A chunked request where the chunk extension area contains a bare LF instead of CRLF: 5;\\nhello. Tested against RFC 9112 §7.1.1." +weight: 25 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-EXT-LF` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1) | +| **Requirement** | MAY accept bare LF | +| **Expected** | `400` or `2xx` | + +## What it sends + +A chunked request where the chunk extension area contains a bare `LF` instead of `CRLF`: `5;\nhello`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +5;\n +hello\r\n +0\r\n +\r\n +``` + +The chunk size line `5;` is terminated with bare LF (`\n`) instead of CRLF. + + +## What the RFC says + +The chunk line grammar requires CRLF as the terminator: + +> chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF +> +> — RFC 9112 §7.1 + +However, RFC 9112 §2.2 provides a MAY-level allowance: + +> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." +> +> — RFC 9112 §2.2 + +This means a server MAY accept bare LF -- both strict rejection and lenient acceptance are RFC-compliant. + +## Why this test is unscored + +The MAY-level allowance for bare LF in RFC 9112 Section 2.2 means both strict rejection and lenient acceptance are RFC-compliant behaviors. Neither response can be considered wrong, so the test cannot be scored. + +**Pass:** Server rejects with `400` (strict CRLF enforcement, safest behavior). +**Warn:** Server accepts and responds `2xx` (RFC-valid per Section 2.2 MAY-level bare LF acceptance). + +## Why it matters + +This is the **TERM.EXT** vector from chunked encoding research. If a parser accepts bare LF in chunk extensions, it may parse chunk boundaries differently from a strict parser, enabling desynchronization and smuggling. + +## Deep Analysis + +### Relevant ABNF (RFC 9112 §7.1 and §2.2) + +``` +chunked-body = *chunk last-chunk trailer-section CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG + +chunk-ext = *( BWS ";" BWS chunk-ext-name + [ BWS "=" BWS chunk-ext-val ] ) +chunk-ext-name = token +chunk-ext-val = token / quoted-string +``` + +### RFC Evidence + +**RFC 9112 §7.1** defines the chunk production with CRLF terminators: + +> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" + +The `CRLF` production is defined in RFC 5234 §B.1 as the two-byte sequence `\r\n` (0x0D 0x0A). A bare LF (0x0A alone) does not match `CRLF`. + +**RFC 9112 §2.2** provides a MAY-level robustness allowance: + +> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." + +This MAY language means recipients are permitted but not required to accept bare LF. Both strict rejection and lenient acceptance are RFC-compliant. + +**RFC 9112 §2.2** draws an explicit asymmetry between bare LF and bare CR: + +> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content. A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." + +Bare LF gets a MAY-accept allowance; bare CR gets a MUST-reject requirement. This test targets the bare LF case. + +### Step-by-Step ABNF Analysis + +1. The parser reads `chunk-size` = `5` (valid HEXDIG). +2. The parser encounters `;` -- this starts a `chunk-ext` production. +3. After the semicolon, the parser expects `chunk-ext-name` (a `token`). But the next byte is `\n` (0x0A). +4. `\n` is not a `tchar`, so it cannot begin a `token`. The `chunk-ext-name` production fails. +5. However, per §2.2 MAY language, a recipient MAY recognize bare LF as a line terminator. If the parser does so, it effectively treats `5;\n` as `5;\r\n` -- a chunk-size line with an empty/absent extension terminated by a line ending. +6. Under strict parsing: the bare LF is not CRLF, and the extension name after `;` is missing. The parse fails at two levels. +7. Under lenient parsing: the bare LF acts as a line terminator, the bare semicolon may be ignored, and the parser reads 5 bytes of chunk data. + +### Real-World Smuggling Scenario + +This is the **TERM.EXT** vector from chunked encoding smuggling research: + +**Attack vector:** An attacker sends `5;\nhello\r\n0\r\n\r\n`. A lenient front-end proxy that accepts bare LF as a line terminator parses this as: chunk-size 5, skip empty extension, read `hello` as chunk data, then read the `0\r\n\r\n` terminator. It forwards the processed request to the back-end. A strict back-end that requires CRLF sees the bare LF differently -- it may interpret `;\nhello\r\n` as a malformed extension containing raw bytes, fail to find the CRLF terminator at the expected position, and reject or misparse the message. + +This exact technique was demonstrated in research by James Kettle on HTTP/2 downgrade smuggling, where HTTP/2 front-ends converted requests to HTTP/1.1 with bare LF terminators that back-end servers interpreted differently. The bare-LF-in-extension variant specifically exploits the fact that the §2.2 MAY language creates implementation-defined behavior, guaranteeing disagreement between strict and lenient parsers. + +## Sources + +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +- [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) diff --git a/docs/content/docs/smuggling/chunk-hex-prefix.md b/docs/content/docs/smuggling/chunk-hex-prefix.md index 416c2ad..3ceaf67 100644 --- a/docs/content/docs/smuggling/chunk-hex-prefix.md +++ b/docs/content/docs/smuggling/chunk-hex-prefix.md @@ -1,104 +1,104 @@ ---- -title: "CHUNK-HEX-PREFIX" -description: "CHUNK-HEX-PREFIX test documentation" -weight: 25 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-HEX-PREFIX` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -Chunk size `0x5` — with C-style hex prefix. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -\r\n -0x5\r\n -hello\r\n -0\r\n -\r\n -``` - -The chunk size uses a `0x` hex prefix. - - -## What the RFC says - -> "The chunk-size field is a string of hex digits indicating the size of the chunk-data in octets." — RFC 9112 §7.1 - -The ABNF grammar is strict: - -> `chunk-size = 1*HEXDIG` - -The grammar allows only hexadecimal digits (`0`-`9`, `A`-`F`, `a`-`f`). The `0x` prefix is not part of the `HEXDIG` production (RFC 5234 §B.1), so `0x5` is invalid: the parser encounters `x` where it expects either another HEXDIG, a chunk extension semicolon, or CRLF. - -> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 §7.1 - -## Why it matters - -`0x5` is valid hex notation in C, Python, and many other languages, but invalid in HTTP chunked encoding. If a server uses a general-purpose hex parser that accepts the `0x` prefix, it reads chunk size 5 -- but a strict parser sees `0` as chunk size zero (the last-chunk), ending the message prematurely. This disagreement on message boundaries enables desynchronization. - -## Deep Analysis - -### Relevant ABNF (RFC 9112 §7.1) - -``` -chunked-body = *chunk last-chunk trailer-section CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG -last-chunk = 1*("0") [ chunk-ext ] CRLF - -HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" - ; case-insensitive per RFC 5234 -``` - -### RFC Evidence - -**RFC 9112 §7.1** defines the chunk-size production: - -> "chunk-size = 1*HEXDIG" - -The `HEXDIG` rule is defined in RFC 5234 §B.1 and allows only the characters `0`-`9`, `A`-`F`, and `a`-`f`. No prefix notation is permitted. - -**RFC 9112 §7.1** also mandates overflow protection: - -> "A recipient MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer conversion." - -This requirement reinforces that the chunk-size is a raw hexadecimal numeral -- not a programming language literal with prefix notation. - -**RFC 9112 §7.1** defines the last-chunk: - -> "last-chunk = 1*('0') [ chunk-ext ] CRLF" - -This is important because a parser that stops at the first non-HEXDIG character in `0x5` would read `0` as the chunk-size, matching the `last-chunk` production and prematurely ending the chunked body. - -### Step-by-Step ABNF Violation - -1. The parser begins reading `chunk-size` = `1*HEXDIG`. -2. It reads `0` -- valid HEXDIG. Current chunk-size value: 0. -3. It reads `x` -- **not a HEXDIG**. The character `x` is not in `0-9`, `A-F`, or `a-f`. -4. At this point, the parser has two options depending on implementation: - - **Strict parser:** Expects either another HEXDIG, a `;` (chunk-ext), or CRLF after the chunk-size. `x` matches none of these. Parse failure -- reject with 400. - - **Lenient parser (stops at non-HEXDIG):** Accepts chunk-size `0`, which matches `last-chunk`. The chunked body ends immediately. The remaining bytes `x5\r\nhello\r\n0\r\n\r\n` are left in the connection buffer. - - **Lenient parser (accepts 0x prefix):** Reads `0x5` as hexadecimal 5, consuming 5 bytes of chunk data. -5. In all cases, the `0x` prefix violates the ABNF because `x` is not HEXDIG and the grammar has no provision for prefix notation. - -### Real-World Smuggling Scenario - -**Attack vector:** An attacker sends `0x5\r\nhello\r\n0\r\n\r\n`. A parser using a general-purpose hex conversion function (like C's `strtol()` with base 16, or Python's `int("0x5", 16)`) accepts the `0x` prefix and reads chunk-size 5. A strict parser reads chunk-size `0` (stopping at `x`), treats it as the last-chunk, and considers the chunked body complete. The remaining bytes `x5\r\nhello\r\n0\r\n\r\n` are left in the TCP buffer and prepended to the next HTTP request on that connection -- this is request smuggling. - -This is particularly dangerous because the `0x` prefix is the universal hex notation in C, Python, JavaScript, Java, and most other languages. Library functions like `strtol()`, `parseInt()`, and `int()` all accept it by default, making it a likely implementation error when a developer uses a standard library integer parser instead of a purpose-built HEXDIG-only parser. - -## Sources - -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunk Hex Prefix — Request Smuggling" +description: "Chunk size 0x5 — with C-style hex prefix. Tested against RFC 9112 §7.1." +weight: 25 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-HEX-PREFIX` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +Chunk size `0x5` — with C-style hex prefix. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +0x5\r\n +hello\r\n +0\r\n +\r\n +``` + +The chunk size uses a `0x` hex prefix. + + +## What the RFC says + +> "The chunk-size field is a string of hex digits indicating the size of the chunk-data in octets." — RFC 9112 §7.1 + +The ABNF grammar is strict: + +> `chunk-size = 1*HEXDIG` + +The grammar allows only hexadecimal digits (`0`-`9`, `A`-`F`, `a`-`f`). The `0x` prefix is not part of the `HEXDIG` production (RFC 5234 §B.1), so `0x5` is invalid: the parser encounters `x` where it expects either another HEXDIG, a chunk extension semicolon, or CRLF. + +> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 §7.1 + +## Why it matters + +`0x5` is valid hex notation in C, Python, and many other languages, but invalid in HTTP chunked encoding. If a server uses a general-purpose hex parser that accepts the `0x` prefix, it reads chunk size 5 -- but a strict parser sees `0` as chunk size zero (the last-chunk), ending the message prematurely. This disagreement on message boundaries enables desynchronization. + +## Deep Analysis + +### Relevant ABNF (RFC 9112 §7.1) + +``` +chunked-body = *chunk last-chunk trailer-section CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG +last-chunk = 1*("0") [ chunk-ext ] CRLF + +HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" + ; case-insensitive per RFC 5234 +``` + +### RFC Evidence + +**RFC 9112 §7.1** defines the chunk-size production: + +> "chunk-size = 1*HEXDIG" + +The `HEXDIG` rule is defined in RFC 5234 §B.1 and allows only the characters `0`-`9`, `A`-`F`, and `a`-`f`. No prefix notation is permitted. + +**RFC 9112 §7.1** also mandates overflow protection: + +> "A recipient MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer conversion." + +This requirement reinforces that the chunk-size is a raw hexadecimal numeral -- not a programming language literal with prefix notation. + +**RFC 9112 §7.1** defines the last-chunk: + +> "last-chunk = 1*('0') [ chunk-ext ] CRLF" + +This is important because a parser that stops at the first non-HEXDIG character in `0x5` would read `0` as the chunk-size, matching the `last-chunk` production and prematurely ending the chunked body. + +### Step-by-Step ABNF Violation + +1. The parser begins reading `chunk-size` = `1*HEXDIG`. +2. It reads `0` -- valid HEXDIG. Current chunk-size value: 0. +3. It reads `x` -- **not a HEXDIG**. The character `x` is not in `0-9`, `A-F`, or `a-f`. +4. At this point, the parser has two options depending on implementation: + - **Strict parser:** Expects either another HEXDIG, a `;` (chunk-ext), or CRLF after the chunk-size. `x` matches none of these. Parse failure -- reject with 400. + - **Lenient parser (stops at non-HEXDIG):** Accepts chunk-size `0`, which matches `last-chunk`. The chunked body ends immediately. The remaining bytes `x5\r\nhello\r\n0\r\n\r\n` are left in the connection buffer. + - **Lenient parser (accepts 0x prefix):** Reads `0x5` as hexadecimal 5, consuming 5 bytes of chunk data. +5. In all cases, the `0x` prefix violates the ABNF because `x` is not HEXDIG and the grammar has no provision for prefix notation. + +### Real-World Smuggling Scenario + +**Attack vector:** An attacker sends `0x5\r\nhello\r\n0\r\n\r\n`. A parser using a general-purpose hex conversion function (like C's `strtol()` with base 16, or Python's `int("0x5", 16)`) accepts the `0x` prefix and reads chunk-size 5. A strict parser reads chunk-size `0` (stopping at `x`), treats it as the last-chunk, and considers the chunked body complete. The remaining bytes `x5\r\nhello\r\n0\r\n\r\n` are left in the TCP buffer and prepended to the next HTTP request on that connection -- this is request smuggling. + +This is particularly dangerous because the `0x` prefix is the universal hex notation in C, Python, JavaScript, Java, and most other languages. Library functions like `strtol()`, `parseInt()`, and `int()` all accept it by default, making it a likely implementation error when a developer uses a standard library integer parser instead of a purpose-built HEXDIG-only parser. + +## Sources + +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/smuggling/chunk-invalid-size-desync.md b/docs/content/docs/smuggling/chunk-invalid-size-desync.md index c9fadd3..976bc60 100644 --- a/docs/content/docs/smuggling/chunk-invalid-size-desync.md +++ b/docs/content/docs/smuggling/chunk-invalid-size-desync.md @@ -1,49 +1,49 @@ ---- -title: "CHUNK-INVALID-SIZE-DESYNC" -description: "SMUG-CHUNK-INVALID-SIZE-DESYNC test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-INVALID-SIZE-DESYNC` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -A two-step sequence: invalid chunk-size `+0` with poison byte `X`, then a clean `GET`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -\r\n -+0\r\n -\r\n -X - -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "chunk-size = 1*HEXDIG" -- RFC 9112 Section 7.1 - -Invalid chunk-size is a framing error. This sequence confirms whether acceptance leads to follow-up parsing corruption. - -## Partial Coverage Note - -Existing tests (`SMUG-CHUNK-NEGATIVE`, `SMUG-CHUNK-HEX-PREFIX`, `SMUG-CHUNK-SPILL`, `MAL-CHUNK-SIZE-OVERFLOW`) cover invalid chunk primitives. This test adds explicit desync confirmation via a follow-up request. - -## Why it matters - -If invalid chunk-size is tolerated and the connection remains open, poison bytes can be interpreted as the next request. - -## Sources - -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunk Invalid Size Desync — Request Smuggling" +description: "A two-step sequence: invalid chunk-size +0 with poison byte X, then a clean GET. Tested against RFC 9112 §7.1." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-INVALID-SIZE-DESYNC` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +A two-step sequence: invalid chunk-size `+0` with poison byte `X`, then a clean `GET`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n ++0\r\n +\r\n +X + +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "chunk-size = 1*HEXDIG" -- RFC 9112 Section 7.1 + +Invalid chunk-size is a framing error. This sequence confirms whether acceptance leads to follow-up parsing corruption. + +## Partial Coverage Note + +Existing tests (`SMUG-CHUNK-NEGATIVE`, `SMUG-CHUNK-HEX-PREFIX`, `SMUG-CHUNK-SPILL`, `MAL-CHUNK-SIZE-OVERFLOW`) cover invalid chunk primitives. This test adds explicit desync confirmation via a follow-up request. + +## Why it matters + +If invalid chunk-size is tolerated and the connection remains open, poison bytes can be interpreted as the next request. + +## Sources + +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/smuggling/chunk-leading-sp.md b/docs/content/docs/smuggling/chunk-leading-sp.md index eef8d14..d27681b 100644 --- a/docs/content/docs/smuggling/chunk-leading-sp.md +++ b/docs/content/docs/smuggling/chunk-leading-sp.md @@ -1,103 +1,103 @@ ---- -title: "CHUNK-LEADING-SP" -description: "CHUNK-LEADING-SP test documentation" -weight: 28 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-LEADING-SP` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -Chunk size ` 5` — with leading space. - -```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 -\r\n -``` - -The chunk size ` 5` has a leading space. - - -## What the RFC says - -> "The chunk-size field is a string of hex digits indicating the size of the chunk-data in octets." — RFC 9112 §7.1 - -> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 §7.1 - -The ABNF grammar is: - -> `chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF` -> -> `chunk-size = 1*HEXDIG` - -The chunk line begins directly with `chunk-size`, which is `1*HEXDIG`. There is no optional whitespace (OWS or BWS) before the chunk size in the grammar. A leading space character (`0x20`) is not a HEXDIG and does not match any production at that position. - -## Why it matters - -Leading whitespace in chunk sizes can cause parser disagreements. A lenient parser might strip the space and parse `5` as the chunk size, while a strict parser fails on the leading space. If a front-end tolerates the space and a back-end does not, the back-end sees different message boundaries -- enabling smuggling. - -## Deep Analysis - -### Relevant ABNF (RFC 9112 §7.1) - -``` -chunked-body = *chunk last-chunk trailer-section CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG -last-chunk = 1*("0") [ chunk-ext ] CRLF -``` - -### RFC Evidence - -**RFC 9112 §7.1** defines the chunk production: - -> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" - -The `chunk` production begins directly with `chunk-size`. There is no optional whitespace (OWS, BWS, or SP) before `chunk-size` in the grammar. - -**RFC 9112 §7.1** defines chunk-size: - -> "chunk-size = 1*HEXDIG" - -The `HEXDIG` production allows only `0`-`9`, `A`-`F`, `a`-`f`. Space (0x20) is not a HEXDIG. - -**RFC 9112 §7.1.1** uses BWS (bad whitespace) only **within** chunk extensions, not before the chunk-size: - -> "chunk-ext = *( BWS ';' BWS chunk-ext-name [ BWS '=' BWS chunk-ext-val ] )" - -This is a deliberate design: BWS is permitted around the semicolons and equals signs inside extensions, but the start of each chunk line has no whitespace allowance. - -### Step-by-Step ABNF Violation - -1. After the preceding chunk's trailing CRLF (or the headers' CRLF for the first chunk), the parser expects the start of a new `chunk` production. -2. A `chunk` begins with `chunk-size` = `1*HEXDIG`. -3. The first byte is SP (0x20, space). SP is not a HEXDIG. -4. The `1*HEXDIG` production requires at least one HEXDIG as the first character. The space character fails this requirement immediately. -5. No ABNF production at this position in the grammar permits whitespace. The parse fails. -6. A conforming parser must reject the message because the chunked body cannot be decoded. - -### Real-World Smuggling Scenario - -**Attack vector:** An attacker sends ` 5\r\nhello\r\n0\r\n\r\n` (note the leading space before `5`). A lenient parser that strips leading whitespace (a common behavior in general-purpose line parsers) reads chunk-size 5 and processes the chunk normally. A strict parser fails on the leading space because it does not match `HEXDIG`. - -This is exploitable in proxy chains: if the front-end strips the space and forwards the request with chunked body intact, but the back-end applies different whitespace handling (or rejects the message), the two parsers disagree on message boundaries. - -Leading whitespace tolerance in chunk-size parsing was identified as a real-world issue in HTTP server implementations. For example, Node.js's llhttp parser had multiple CVEs related to lenient whitespace handling in HTTP parsing (CVE-2022-32213, CVE-2022-32214), where extra whitespace in framing-critical positions caused parser disagreements. - -## Sources - -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunk Leading Space — Request Smuggling" +description: "Chunk size 5 — with leading space. Tested against RFC 9112 §7.1." +weight: 28 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-LEADING-SP` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +Chunk size ` 5` — with leading space. + +```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 +\r\n +``` + +The chunk size ` 5` has a leading space. + + +## What the RFC says + +> "The chunk-size field is a string of hex digits indicating the size of the chunk-data in octets." — RFC 9112 §7.1 + +> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 §7.1 + +The ABNF grammar is: + +> `chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF` +> +> `chunk-size = 1*HEXDIG` + +The chunk line begins directly with `chunk-size`, which is `1*HEXDIG`. There is no optional whitespace (OWS or BWS) before the chunk size in the grammar. A leading space character (`0x20`) is not a HEXDIG and does not match any production at that position. + +## Why it matters + +Leading whitespace in chunk sizes can cause parser disagreements. A lenient parser might strip the space and parse `5` as the chunk size, while a strict parser fails on the leading space. If a front-end tolerates the space and a back-end does not, the back-end sees different message boundaries -- enabling smuggling. + +## Deep Analysis + +### Relevant ABNF (RFC 9112 §7.1) + +``` +chunked-body = *chunk last-chunk trailer-section CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG +last-chunk = 1*("0") [ chunk-ext ] CRLF +``` + +### RFC Evidence + +**RFC 9112 §7.1** defines the chunk production: + +> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" + +The `chunk` production begins directly with `chunk-size`. There is no optional whitespace (OWS, BWS, or SP) before `chunk-size` in the grammar. + +**RFC 9112 §7.1** defines chunk-size: + +> "chunk-size = 1*HEXDIG" + +The `HEXDIG` production allows only `0`-`9`, `A`-`F`, `a`-`f`. Space (0x20) is not a HEXDIG. + +**RFC 9112 §7.1.1** uses BWS (bad whitespace) only **within** chunk extensions, not before the chunk-size: + +> "chunk-ext = *( BWS ';' BWS chunk-ext-name [ BWS '=' BWS chunk-ext-val ] )" + +This is a deliberate design: BWS is permitted around the semicolons and equals signs inside extensions, but the start of each chunk line has no whitespace allowance. + +### Step-by-Step ABNF Violation + +1. After the preceding chunk's trailing CRLF (or the headers' CRLF for the first chunk), the parser expects the start of a new `chunk` production. +2. A `chunk` begins with `chunk-size` = `1*HEXDIG`. +3. The first byte is SP (0x20, space). SP is not a HEXDIG. +4. The `1*HEXDIG` production requires at least one HEXDIG as the first character. The space character fails this requirement immediately. +5. No ABNF production at this position in the grammar permits whitespace. The parse fails. +6. A conforming parser must reject the message because the chunked body cannot be decoded. + +### Real-World Smuggling Scenario + +**Attack vector:** An attacker sends ` 5\r\nhello\r\n0\r\n\r\n` (note the leading space before `5`). A lenient parser that strips leading whitespace (a common behavior in general-purpose line parsers) reads chunk-size 5 and processes the chunk normally. A strict parser fails on the leading space because it does not match `HEXDIG`. + +This is exploitable in proxy chains: if the front-end strips the space and forwards the request with chunked body intact, but the back-end applies different whitespace handling (or rejects the message), the two parsers disagree on message boundaries. + +Leading whitespace tolerance in chunk-size parsing was identified as a real-world issue in HTTP server implementations. For example, Node.js's llhttp parser had multiple CVEs related to lenient whitespace handling in HTTP parsing (CVE-2022-32213, CVE-2022-32214), where extra whitespace in framing-critical positions caused parser disagreements. + +## Sources + +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/smuggling/chunk-lf-term.md b/docs/content/docs/smuggling/chunk-lf-term.md index 9a4c477..b76039c 100644 --- a/docs/content/docs/smuggling/chunk-lf-term.md +++ b/docs/content/docs/smuggling/chunk-lf-term.md @@ -1,116 +1,116 @@ ---- -title: "CHUNK-LF-TERM" -description: "CHUNK-LF-TERM test documentation" -weight: 27 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-LF-TERM` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MAY accept bare LF | -| **Expected** | `400` or `2xx` | - -## What it sends - -A chunked request where the chunk data terminator is a bare `LF` (`\n`) instead of `CRLF` (`\r\n`): `5\r\nhello\n0\r\n\r\n`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -\r\n -5\r\n -hello\n -0\r\n -\r\n -``` - -The chunk data `hello` is terminated with bare LF (`\n`) instead of CRLF (`\r\n`). - - -## What the RFC says - -The chunk grammar requires CRLF after chunk data: - -> chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF -> -> — RFC 9112 §7.1 - -However, RFC 9112 §2.2 provides a MAY-level allowance: - -> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." -> -> — RFC 9112 §2.2 - -This means a server MAY accept bare LF as a chunk data terminator -- both strict rejection and lenient acceptance are RFC-compliant. - -## Why this test is unscored - -The MAY-level allowance for bare LF in RFC 9112 Section 2.2 means both strict rejection and lenient acceptance are RFC-compliant behaviors. Neither response can be considered wrong, so the test cannot be scored. - -**Pass:** Server rejects with `400` (strict CRLF enforcement, safest behavior). -**Warn:** Server accepts and responds `2xx` (RFC-valid per Section 2.2 MAY-level bare LF acceptance). - -## Why it matters - -If one parser accepts bare LF as a chunk data terminator and another requires strict CRLF, they disagree on where the chunk data ends. The byte that the strict parser considers part of chunk data is treated as the next chunk-size line by the lenient parser — a classic desynchronization vector. - -## Deep Analysis - -### Relevant ABNF (RFC 9112 §7.1) - -``` -chunked-body = *chunk last-chunk trailer-section CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG -chunk-data = 1*OCTET ; a sequence of chunk-size octets - -CRLF = CR LF ; \r\n (0x0D 0x0A) -``` - -### RFC Evidence - -**RFC 9112 §7.1** specifies CRLF in two positions per chunk: - -> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" - -The second `CRLF` -- after `chunk-data` -- is the one violated by this test. A bare LF (0x0A) does not satisfy the `CRLF` production (which requires the two-byte sequence 0x0D 0x0A). - -**RFC 9112 §2.2** provides the MAY-level robustness allowance: - -> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." - -This explicitly grants recipients the option to accept bare LF. Both strict rejection and lenient acceptance are valid. - -**RFC 9112 §2.2** contrasts this with the bare CR rule: - -> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content. A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." - -The asymmetry is clear: bare LF gets MAY-accept treatment, while bare CR gets MUST-reject treatment. - -### Step-by-Step ABNF Analysis - -1. The parser reads `chunk-size` = `5` (valid), then CRLF (valid). It now expects `chunk-data`. -2. The parser reads exactly 5 bytes: `h`, `e`, `l`, `l`, `o`. This satisfies `chunk-data`. -3. The parser now expects `CRLF` (0x0D 0x0A) to terminate the chunk data. -4. The next byte is `\n` (0x0A) -- a bare LF without a preceding CR. -5. **Strict interpretation:** `\n` alone does not match the `CRLF` production. The parse fails. -6. **Lenient interpretation (per §2.2 MAY):** The parser recognizes bare LF as a line terminator. It treats `hello\n` as equivalent to `hello\r\n` and proceeds to the next chunk. -7. Under lenient parsing, the next bytes `0\r\n\r\n` form a valid last-chunk and trailer terminator. The message is complete. - -### Real-World Smuggling Scenario - -**Attack vector:** An attacker sends `5\r\nhello\n0\r\n\r\n`. A lenient parser treats the bare LF after `hello` as the chunk-data terminator and processes the message normally (5 bytes of body: `hello`). A strict parser does not recognize bare LF as CRLF. After reading 5 bytes (`hello`), it expects `\r\n` but gets `\n0`. The `\n` is not `\r`, so the parser considers the chunk framing broken. - -**Byte-level desync:** The strict parser may interpret the stream differently -- for example, treating the `\n` as part of the next data segment, or closing the connection. If the strict parser is a back-end and the lenient parser is a front-end proxy, the front-end considers the message complete and moves on, while the back-end sees leftover bytes. On a keep-alive connection, those leftover bytes become the start of the "next" request -- enabling smuggling. - -This bare-LF chunk data terminator vector is part of the "TERM" class of chunked smuggling techniques. It was demonstrated in practice in HAProxy + various back-end combinations, where HAProxy's lenient LF acceptance conflicted with stricter back-end parsers. CVE-2023-25725 (HAProxy) involved related chunked parsing discrepancies in the context of HTTP request smuggling. - -## Sources - -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) -- [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) +--- +title: "Chunk LF Term — Request Smuggling" +description: "A chunked request where the chunk data terminator is a bare LF (\\n) instead of CRLF (\\r\\n): 5\\r\\nhello\\n0\\r\\n\\r\\n. Tested against RFC 9112 §7.1." +weight: 27 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-LF-TERM` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MAY accept bare LF | +| **Expected** | `400` or `2xx` | + +## What it sends + +A chunked request where the chunk data terminator is a bare `LF` (`\n`) instead of `CRLF` (`\r\n`): `5\r\nhello\n0\r\n\r\n`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\n +0\r\n +\r\n +``` + +The chunk data `hello` is terminated with bare LF (`\n`) instead of CRLF (`\r\n`). + + +## What the RFC says + +The chunk grammar requires CRLF after chunk data: + +> chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF +> +> — RFC 9112 §7.1 + +However, RFC 9112 §2.2 provides a MAY-level allowance: + +> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." +> +> — RFC 9112 §2.2 + +This means a server MAY accept bare LF as a chunk data terminator -- both strict rejection and lenient acceptance are RFC-compliant. + +## Why this test is unscored + +The MAY-level allowance for bare LF in RFC 9112 Section 2.2 means both strict rejection and lenient acceptance are RFC-compliant behaviors. Neither response can be considered wrong, so the test cannot be scored. + +**Pass:** Server rejects with `400` (strict CRLF enforcement, safest behavior). +**Warn:** Server accepts and responds `2xx` (RFC-valid per Section 2.2 MAY-level bare LF acceptance). + +## Why it matters + +If one parser accepts bare LF as a chunk data terminator and another requires strict CRLF, they disagree on where the chunk data ends. The byte that the strict parser considers part of chunk data is treated as the next chunk-size line by the lenient parser — a classic desynchronization vector. + +## Deep Analysis + +### Relevant ABNF (RFC 9112 §7.1) + +``` +chunked-body = *chunk last-chunk trailer-section CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG +chunk-data = 1*OCTET ; a sequence of chunk-size octets + +CRLF = CR LF ; \r\n (0x0D 0x0A) +``` + +### RFC Evidence + +**RFC 9112 §7.1** specifies CRLF in two positions per chunk: + +> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" + +The second `CRLF` -- after `chunk-data` -- is the one violated by this test. A bare LF (0x0A) does not satisfy the `CRLF` production (which requires the two-byte sequence 0x0D 0x0A). + +**RFC 9112 §2.2** provides the MAY-level robustness allowance: + +> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." + +This explicitly grants recipients the option to accept bare LF. Both strict rejection and lenient acceptance are valid. + +**RFC 9112 §2.2** contrasts this with the bare CR rule: + +> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content. A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." + +The asymmetry is clear: bare LF gets MAY-accept treatment, while bare CR gets MUST-reject treatment. + +### Step-by-Step ABNF Analysis + +1. The parser reads `chunk-size` = `5` (valid), then CRLF (valid). It now expects `chunk-data`. +2. The parser reads exactly 5 bytes: `h`, `e`, `l`, `l`, `o`. This satisfies `chunk-data`. +3. The parser now expects `CRLF` (0x0D 0x0A) to terminate the chunk data. +4. The next byte is `\n` (0x0A) -- a bare LF without a preceding CR. +5. **Strict interpretation:** `\n` alone does not match the `CRLF` production. The parse fails. +6. **Lenient interpretation (per §2.2 MAY):** The parser recognizes bare LF as a line terminator. It treats `hello\n` as equivalent to `hello\r\n` and proceeds to the next chunk. +7. Under lenient parsing, the next bytes `0\r\n\r\n` form a valid last-chunk and trailer terminator. The message is complete. + +### Real-World Smuggling Scenario + +**Attack vector:** An attacker sends `5\r\nhello\n0\r\n\r\n`. A lenient parser treats the bare LF after `hello` as the chunk-data terminator and processes the message normally (5 bytes of body: `hello`). A strict parser does not recognize bare LF as CRLF. After reading 5 bytes (`hello`), it expects `\r\n` but gets `\n0`. The `\n` is not `\r`, so the parser considers the chunk framing broken. + +**Byte-level desync:** The strict parser may interpret the stream differently -- for example, treating the `\n` as part of the next data segment, or closing the connection. If the strict parser is a back-end and the lenient parser is a front-end proxy, the front-end considers the message complete and moves on, while the back-end sees leftover bytes. On a keep-alive connection, those leftover bytes become the start of the "next" request -- enabling smuggling. + +This bare-LF chunk data terminator vector is part of the "TERM" class of chunked smuggling techniques. It was demonstrated in practice in HAProxy + various back-end combinations, where HAProxy's lenient LF acceptance conflicted with stricter back-end parsers. CVE-2023-25725 (HAProxy) involved related chunked parsing discrepancies in the context of HTTP request smuggling. + +## Sources + +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +- [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) diff --git a/docs/content/docs/smuggling/chunk-lf-trailer.md b/docs/content/docs/smuggling/chunk-lf-trailer.md index a87a418..3ad27df 100644 --- a/docs/content/docs/smuggling/chunk-lf-trailer.md +++ b/docs/content/docs/smuggling/chunk-lf-trailer.md @@ -1,114 +1,114 @@ ---- -title: "CHUNK-LF-TRAILER" -description: "CHUNK-LF-TRAILER test documentation" -weight: 29 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-LF-TRAILER` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MAY accept bare LF | -| **Expected** | `400` or `2xx` | - -## What it sends - -A chunked request where the final trailer section terminator uses bare `LF` instead of `CRLF`: `0\r\n\n` instead of `0\r\n\r\n`. - -```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 -\n -``` - -The final trailer terminator uses bare LF (`\n`) instead of CRLF (`\r\n`). - - -## What the RFC says - -The chunked body grammar requires CRLF to terminate the trailer section: - -> chunked-body = *chunk last-chunk trailer-section CRLF -> -> — RFC 9112 §7.1 - -However, RFC 9112 §2.2 provides a MAY-level allowance: - -> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." -> -> — RFC 9112 §2.2 - -This means a server MAY accept bare LF as the trailer terminator -- both strict rejection and lenient acceptance are RFC-compliant. - -## Why this test is unscored - -The MAY-level allowance for bare LF in RFC 9112 Section 2.2 means both strict rejection and lenient acceptance are RFC-compliant behaviors. Neither response can be considered wrong, so the test cannot be scored. - -**Pass:** Server rejects with `400` (strict CRLF enforcement, safest behavior). -**Warn:** Server accepts and responds `2xx` (RFC-valid per Section 2.2 MAY-level bare LF acceptance). - -## Why it matters - -If a front-end parser accepts bare LF as the end of the chunked body but a back-end requires strict CRLF, the back-end may continue waiting for data or interpret subsequent bytes differently. This desync between message boundary detection is a smuggling vector. - -## Deep Analysis - -### Relevant ABNF (RFC 9112 §7.1) - -``` -chunked-body = *chunk last-chunk trailer-section CRLF - -last-chunk = 1*("0") [ chunk-ext ] CRLF -trailer-section = *( field-line CRLF ) -``` - -The final `CRLF` in `chunked-body` terminates the entire chunked message. After the `last-chunk` and any trailer fields, this CRLF signals the end of the chunked body. - -### RFC Evidence - -**RFC 9112 §7.1** defines the chunked-body terminator: - -> "chunked-body = *chunk last-chunk trailer-section CRLF" - -The final `CRLF` is mandatory. It follows the `trailer-section` (which may be empty, producing zero trailer field lines). The `CRLF` production requires the two-byte sequence 0x0D 0x0A. - -**RFC 9112 §2.2** provides the MAY-level bare LF allowance: - -> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." - -This MAY language applies to all line terminators in the protocol, including the final chunked-body CRLF. - -**RFC 9112 §2.2** contrasts this with bare CR handling: - -> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content. A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." - -### Step-by-Step ABNF Analysis - -1. The parser reads the first chunk: `5\r\nhello\r\n` -- valid chunk with size 5 and data `hello`. -2. The parser reads `0\r\n` -- this matches `last-chunk` = `1*("0") [ chunk-ext ] CRLF`. -3. The parser now expects the `trailer-section` followed by `CRLF`. Since there are no trailer fields, the `trailer-section` is empty (zero repetitions). -4. The parser expects the final `CRLF` (0x0D 0x0A) to complete the `chunked-body`. -5. The next byte is `\n` (0x0A) -- a bare LF. -6. **Strict interpretation:** Bare LF does not satisfy the `CRLF` production. The chunked body is not properly terminated. -7. **Lenient interpretation (per §2.2 MAY):** The parser accepts bare LF as a line terminator. The chunked body is considered complete. - -### Real-World Smuggling Scenario - -The final trailer CRLF is the boundary between the current HTTP message and the next one on a keep-alive connection. Disagreement on whether the message has ended is directly exploitable: - -**Attack vector:** An attacker sends `5\r\nhello\r\n0\r\n\n` followed immediately by a smuggled request (e.g., `GET /admin HTTP/1.1\r\n...`). A lenient front-end accepts the bare LF as the end of the chunked body and considers the first message complete. It then sees the `GET /admin` as a new, separate request. A strict back-end does not accept bare LF as the final CRLF -- it continues waiting for `\r\n`. The back-end reads the `GET /admin` bytes as continuation of the first message (possibly as trailer data or an error). This disagreement means the front-end routes the `GET /admin` independently, while the back-end never sees it as a standalone request. - -Alternatively, the reverse scenario: if the back-end is lenient and the front-end is strict, the front-end may buffer additional data while the back-end considers the message done and begins reading the next request from the remaining bytes. - -This trailer-termination confusion was part of the attack surface explored in HTTP desynchronization research. CVE-2023-25725 (HAProxy) involved chunked message boundary detection issues where front-end and back-end disagreed on when the chunked body ended. - -## Sources - -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) -- [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) +--- +title: "Chunk LF Trailer — Request Smuggling" +description: "A chunked request where the final trailer section terminator uses bare LF instead of CRLF: 0\\r\\n\\n instead of 0\\r\\n\\r\\n. Tested against RFC 9112 §7.1." +weight: 29 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-LF-TRAILER` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MAY accept bare LF | +| **Expected** | `400` or `2xx` | + +## What it sends + +A chunked request where the final trailer section terminator uses bare `LF` instead of `CRLF`: `0\r\n\n` instead of `0\r\n\r\n`. + +```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 +\n +``` + +The final trailer terminator uses bare LF (`\n`) instead of CRLF (`\r\n`). + + +## What the RFC says + +The chunked body grammar requires CRLF to terminate the trailer section: + +> chunked-body = *chunk last-chunk trailer-section CRLF +> +> — RFC 9112 §7.1 + +However, RFC 9112 §2.2 provides a MAY-level allowance: + +> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." +> +> — RFC 9112 §2.2 + +This means a server MAY accept bare LF as the trailer terminator -- both strict rejection and lenient acceptance are RFC-compliant. + +## Why this test is unscored + +The MAY-level allowance for bare LF in RFC 9112 Section 2.2 means both strict rejection and lenient acceptance are RFC-compliant behaviors. Neither response can be considered wrong, so the test cannot be scored. + +**Pass:** Server rejects with `400` (strict CRLF enforcement, safest behavior). +**Warn:** Server accepts and responds `2xx` (RFC-valid per Section 2.2 MAY-level bare LF acceptance). + +## Why it matters + +If a front-end parser accepts bare LF as the end of the chunked body but a back-end requires strict CRLF, the back-end may continue waiting for data or interpret subsequent bytes differently. This desync between message boundary detection is a smuggling vector. + +## Deep Analysis + +### Relevant ABNF (RFC 9112 §7.1) + +``` +chunked-body = *chunk last-chunk trailer-section CRLF + +last-chunk = 1*("0") [ chunk-ext ] CRLF +trailer-section = *( field-line CRLF ) +``` + +The final `CRLF` in `chunked-body` terminates the entire chunked message. After the `last-chunk` and any trailer fields, this CRLF signals the end of the chunked body. + +### RFC Evidence + +**RFC 9112 §7.1** defines the chunked-body terminator: + +> "chunked-body = *chunk last-chunk trailer-section CRLF" + +The final `CRLF` is mandatory. It follows the `trailer-section` (which may be empty, producing zero trailer field lines). The `CRLF` production requires the two-byte sequence 0x0D 0x0A. + +**RFC 9112 §2.2** provides the MAY-level bare LF allowance: + +> "Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." + +This MAY language applies to all line terminators in the protocol, including the final chunked-body CRLF. + +**RFC 9112 §2.2** contrasts this with bare CR handling: + +> "A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF) within any protocol elements other than the content. A recipient of such a bare CR MUST consider that element to be invalid or replace each bare CR with SP before processing the element or forwarding the message." + +### Step-by-Step ABNF Analysis + +1. The parser reads the first chunk: `5\r\nhello\r\n` -- valid chunk with size 5 and data `hello`. +2. The parser reads `0\r\n` -- this matches `last-chunk` = `1*("0") [ chunk-ext ] CRLF`. +3. The parser now expects the `trailer-section` followed by `CRLF`. Since there are no trailer fields, the `trailer-section` is empty (zero repetitions). +4. The parser expects the final `CRLF` (0x0D 0x0A) to complete the `chunked-body`. +5. The next byte is `\n` (0x0A) -- a bare LF. +6. **Strict interpretation:** Bare LF does not satisfy the `CRLF` production. The chunked body is not properly terminated. +7. **Lenient interpretation (per §2.2 MAY):** The parser accepts bare LF as a line terminator. The chunked body is considered complete. + +### Real-World Smuggling Scenario + +The final trailer CRLF is the boundary between the current HTTP message and the next one on a keep-alive connection. Disagreement on whether the message has ended is directly exploitable: + +**Attack vector:** An attacker sends `5\r\nhello\r\n0\r\n\n` followed immediately by a smuggled request (e.g., `GET /admin HTTP/1.1\r\n...`). A lenient front-end accepts the bare LF as the end of the chunked body and considers the first message complete. It then sees the `GET /admin` as a new, separate request. A strict back-end does not accept bare LF as the final CRLF -- it continues waiting for `\r\n`. The back-end reads the `GET /admin` bytes as continuation of the first message (possibly as trailer data or an error). This disagreement means the front-end routes the `GET /admin` independently, while the back-end never sees it as a standalone request. + +Alternatively, the reverse scenario: if the back-end is lenient and the front-end is strict, the front-end may buffer additional data while the back-end considers the message done and begins reading the next request from the remaining bytes. + +This trailer-termination confusion was part of the attack surface explored in HTTP desynchronization research. CVE-2023-25725 (HAProxy) involved chunked message boundary detection issues where front-end and back-end disagreed on when the chunked body ended. + +## Sources + +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +- [RFC 9112 §2.2](https://www.rfc-editor.org/rfc/rfc9112#section-2.2) diff --git a/docs/content/docs/smuggling/chunk-missing-trailing-crlf.md b/docs/content/docs/smuggling/chunk-missing-trailing-crlf.md index ed301e8..21d4392 100644 --- a/docs/content/docs/smuggling/chunk-missing-trailing-crlf.md +++ b/docs/content/docs/smuggling/chunk-missing-trailing-crlf.md @@ -1,104 +1,104 @@ ---- -title: "CHUNK-MISSING-TRAILING-CRLF" -description: "CHUNK-MISSING-TRAILING-CRLF test documentation" -weight: 29 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-MISSING-TRAILING-CRLF` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -Chunk data without the required trailing CRLF after data. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -\r\n -5\r\n -hello0\r\n -\r\n -``` - -The chunk data `hello` is not followed by `\r\n` — the `0` terminator runs directly into the chunk data, reading as `hello0`. - - -## What the RFC says - -> "The chunk-size field is a string of hex digits indicating the size of the chunk-data in octets." — RFC 9112 §7.1 - -> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 §7.1 - -The ABNF grammar specifies: - -> `chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF` - -The grammar requires two CRLFs per chunk: one after the chunk-size line and one after the chunk-data. The trailing CRLF after chunk-data is mandatory. When it is missing, the bytes of the next chunk (here `0\r\n`) are consumed as chunk data, corrupting the framing. - -## Why it matters - -Without the trailing CRLF, the parser reads past the declared chunk data into the next chunk-size line. A strict parser detects the missing CRLF and rejects the message, while a lenient parser may silently absorb the extra bytes as data. This disagreement on chunk boundaries between a front-end and back-end enables request smuggling. - -## Deep Analysis - -### Relevant ABNF (RFC 9112 §7.1) - -``` -chunked-body = *chunk last-chunk trailer-section CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG -chunk-data = 1*OCTET ; a sequence of chunk-size octets -last-chunk = 1*("0") [ chunk-ext ] CRLF -``` - -### RFC Evidence - -**RFC 9112 §7.1** specifies the `chunk` production with two mandatory CRLFs: - -> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" - -The second CRLF (after `chunk-data`) is the trailing terminator. Without it, the parser cannot determine where chunk-data ends and the next chunk-size begins. - -**RFC 9112 §7.1** defines chunk-data: - -> "chunk-data = 1*OCTET ; a sequence of chunk-size octets" - -The parser reads exactly `chunk-size` octets, then expects CRLF. This is a length-delimited protocol -- the CRLF after chunk-data serves as a framing verification marker. - -**RFC 9112 §7** establishes the decoding obligation: - -> "A recipient MUST be able to parse and decode the chunked transfer coding." - -If the chunked framing is violated (missing CRLF), the recipient cannot successfully decode the transfer coding. - -### Step-by-Step ABNF Violation - -1. The parser reads `chunk-size` = `5` (valid HEXDIG), then CRLF (valid). -2. The parser reads exactly 5 bytes of `chunk-data`: `h`, `e`, `l`, `l`, `o`. -3. The parser now expects CRLF (0x0D 0x0A) as the trailing chunk-data terminator. -4. The next bytes are `0\r\n` -- the intended last-chunk. But `0` (0x30) is not `\r` (0x0D). -5. The parser expected `\r` but got `0`. The trailing CRLF is missing. The `chunk` production fails. -6. From the wire perspective, the bytes are: `5\r\nhello0\r\n\r\n`. The `0` that was intended as the last-chunk is consumed into the data stream because it appears where the CRLF should be. - -### Real-World Smuggling Scenario - -**Attack vector:** An attacker sends a chunk declaring size 5, immediately followed by `hello0\r\n\r\n` (no CRLF between `hello` and `0`). A strict parser reads 5 bytes (`hello`), expects CRLF, finds `0` instead, and rejects the message. A lenient parser might: - -- Read the `0` as the 6th byte, expecting CRLF at offset 6 -- it finds `\r\n`, accepts it, and then reads `\r\n` as the next chunk-size line (empty/invalid). -- Or read 5 bytes (`hello`), skip the CRLF check, see `0` as the next chunk-size (last-chunk), and process the message as valid. - -In a proxy chain, the strict front-end rejects the request, but if the raw bytes somehow reach the back-end (e.g., via a non-parsing proxy), the lenient back-end processes a different message boundary than intended. The bytes after what the back-end considers the message end become the start of the next request. - -This missing-CRLF confusion is a fundamental chunked framing violation. The Apache HTTP Server had CVE-2015-3183, where improper chunk parsing allowed request smuggling via malformed chunk boundaries. Similarly, research on chunked encoding edge cases has consistently demonstrated that CRLF verification failures between proxies and back-ends remain a practical smuggling vector. - -## Sources - -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunk Missing Trailing CRLF — Request Smuggling" +description: "Chunk data without the required trailing CRLF after data. Tested against RFC 9112 §7.1." +weight: 29 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-MISSING-TRAILING-CRLF` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +Chunk data without the required trailing CRLF after data. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello0\r\n +\r\n +``` + +The chunk data `hello` is not followed by `\r\n` — the `0` terminator runs directly into the chunk data, reading as `hello0`. + + +## What the RFC says + +> "The chunk-size field is a string of hex digits indicating the size of the chunk-data in octets." — RFC 9112 §7.1 + +> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 §7.1 + +The ABNF grammar specifies: + +> `chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF` + +The grammar requires two CRLFs per chunk: one after the chunk-size line and one after the chunk-data. The trailing CRLF after chunk-data is mandatory. When it is missing, the bytes of the next chunk (here `0\r\n`) are consumed as chunk data, corrupting the framing. + +## Why it matters + +Without the trailing CRLF, the parser reads past the declared chunk data into the next chunk-size line. A strict parser detects the missing CRLF and rejects the message, while a lenient parser may silently absorb the extra bytes as data. This disagreement on chunk boundaries between a front-end and back-end enables request smuggling. + +## Deep Analysis + +### Relevant ABNF (RFC 9112 §7.1) + +``` +chunked-body = *chunk last-chunk trailer-section CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG +chunk-data = 1*OCTET ; a sequence of chunk-size octets +last-chunk = 1*("0") [ chunk-ext ] CRLF +``` + +### RFC Evidence + +**RFC 9112 §7.1** specifies the `chunk` production with two mandatory CRLFs: + +> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" + +The second CRLF (after `chunk-data`) is the trailing terminator. Without it, the parser cannot determine where chunk-data ends and the next chunk-size begins. + +**RFC 9112 §7.1** defines chunk-data: + +> "chunk-data = 1*OCTET ; a sequence of chunk-size octets" + +The parser reads exactly `chunk-size` octets, then expects CRLF. This is a length-delimited protocol -- the CRLF after chunk-data serves as a framing verification marker. + +**RFC 9112 §7** establishes the decoding obligation: + +> "A recipient MUST be able to parse and decode the chunked transfer coding." + +If the chunked framing is violated (missing CRLF), the recipient cannot successfully decode the transfer coding. + +### Step-by-Step ABNF Violation + +1. The parser reads `chunk-size` = `5` (valid HEXDIG), then CRLF (valid). +2. The parser reads exactly 5 bytes of `chunk-data`: `h`, `e`, `l`, `l`, `o`. +3. The parser now expects CRLF (0x0D 0x0A) as the trailing chunk-data terminator. +4. The next bytes are `0\r\n` -- the intended last-chunk. But `0` (0x30) is not `\r` (0x0D). +5. The parser expected `\r` but got `0`. The trailing CRLF is missing. The `chunk` production fails. +6. From the wire perspective, the bytes are: `5\r\nhello0\r\n\r\n`. The `0` that was intended as the last-chunk is consumed into the data stream because it appears where the CRLF should be. + +### Real-World Smuggling Scenario + +**Attack vector:** An attacker sends a chunk declaring size 5, immediately followed by `hello0\r\n\r\n` (no CRLF between `hello` and `0`). A strict parser reads 5 bytes (`hello`), expects CRLF, finds `0` instead, and rejects the message. A lenient parser might: + +- Read the `0` as the 6th byte, expecting CRLF at offset 6 -- it finds `\r\n`, accepts it, and then reads `\r\n` as the next chunk-size line (empty/invalid). +- Or read 5 bytes (`hello`), skip the CRLF check, see `0` as the next chunk-size (last-chunk), and process the message as valid. + +In a proxy chain, the strict front-end rejects the request, but if the raw bytes somehow reach the back-end (e.g., via a non-parsing proxy), the lenient back-end processes a different message boundary than intended. The bytes after what the back-end considers the message end become the start of the next request. + +This missing-CRLF confusion is a fundamental chunked framing violation. The Apache HTTP Server had CVE-2015-3183, where improper chunk parsing allowed request smuggling via malformed chunk boundaries. Similarly, research on chunked encoding edge cases has consistently demonstrated that CRLF verification failures between proxies and back-ends remain a practical smuggling vector. + +## Sources + +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/smuggling/chunk-negative.md b/docs/content/docs/smuggling/chunk-negative.md index 3aacf3b..797f36b 100644 --- a/docs/content/docs/smuggling/chunk-negative.md +++ b/docs/content/docs/smuggling/chunk-negative.md @@ -1,108 +1,108 @@ ---- -title: "CHUNK-NEGATIVE" -description: "CHUNK-NEGATIVE test documentation" -weight: 31 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-NEGATIVE` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -A chunked request with a negative chunk size: `-1\r\nhello\r\n0\r\n\r\n`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -\r\n --1\r\n -hello\r\n -0\r\n -\r\n -``` - -The chunk size `-1` is negative. - - -## What the RFC says - -> chunk-size = 1*HEXDIG -> -> — RFC 9112 §7.1 - -The `HEXDIG` production (RFC 5234 §B.1) allows only the characters `0`-`9`, `A`-`F`, and `a`-`f`. A minus sign (`-`) is not a HEXDIG, so `-1` does not match the `chunk-size` rule. Additionally, the RFC requires implementations to guard against overflow: - -> "...recipients MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer representation." -> -> — RFC 9112 §7.1 - -## Why it matters - -A parser that interprets `-1` as a signed integer may wrap it to a very large unsigned value, causing it to read far beyond the actual data. This can lead to buffer over-reads, denial of service, or desynchronization with a stricter parser that rejects the negative value. - -## Deep Analysis - -### Relevant ABNF (RFC 9112 §7.1) - -``` -chunked-body = *chunk last-chunk trailer-section CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG -chunk-data = 1*OCTET ; a sequence of chunk-size octets -last-chunk = 1*("0") [ chunk-ext ] CRLF - -HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" -DIGIT = %x30-39 ; 0-9 -``` - -### RFC Evidence - -**RFC 9112 §7.1** defines chunk-size: - -> "chunk-size = 1*HEXDIG" - -The `HEXDIG` production (RFC 5234 §B.1) permits only the characters `0`-`9`, `A`-`F`, and `a`-`f`. The minus sign (`-`, 0x2D) is not a HEXDIG. - -**RFC 9112 §7.1** mandates overflow protection: - -> "A recipient MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer conversion." - -This requirement specifically addresses integer parsing safety. A negative sign introduces signed integer semantics that the ABNF never permits -- the chunk-size is an unsigned hexadecimal numeral by definition. - -**RFC 9112 §7** establishes the decoding obligation: - -> "A recipient MUST be able to parse and decode the chunked transfer coding." - -If the chunk-size cannot be parsed according to the ABNF, the chunked coding cannot be decoded, and the message must be rejected. - -### Step-by-Step ABNF Violation - -1. The parser begins reading `chunk-size` = `1*HEXDIG`. -2. The first byte is `-` (0x2D, minus sign). -3. `-` is not a HEXDIG. The `1*HEXDIG` production requires at least one HEXDIG as the very first character. -4. The production fails immediately at the first character. No valid chunk-size can be read. -5. Since the `chunk` production fails, the entire `chunked-body` is invalid. - -### Real-World Smuggling Scenario - -**Attack vector:** An attacker sends `-1\r\nhello\r\n0\r\n\r\n`. A parser using a signed integer conversion function (like C's `strtol()` or `atoi()`) may interpret `-1` as the signed integer -1. When cast to an unsigned type (e.g., `size_t` on a 64-bit system), -1 becomes `0xFFFFFFFFFFFFFFFF` (18,446,744,073,709,551,615 bytes). The parser then attempts to read that many bytes of chunk data, causing: - -- **Buffer over-read:** The parser reads far beyond the actual data, potentially exposing memory contents from adjacent buffers (similar to Heartbleed-style information disclosure). -- **Denial of service:** The parser hangs waiting for approximately 18 exabytes of data that will never arrive. -- **Desynchronization:** If the parser reads some fixed buffer amount and then fails, leftover bytes corrupt the connection state. - -A strict parser rejects `-1` immediately because `-` is not HEXDIG. If a strict front-end rejects but a lenient back-end somehow receives the raw bytes (e.g., via a non-parsing L4 proxy), the back-end may exhibit undefined behavior. - -Integer signedness issues in HTTP chunk size parsing have been a recurring vulnerability class. CVE-2015-3183 (Apache httpd) involved chunk parsing errors that enabled smuggling. The signed/unsigned integer confusion in chunk sizes is a well-documented anti-pattern in HTTP security research, and the explicit `1*HEXDIG` grammar (excluding `-`) is intentionally designed to prevent it. - -## Sources - -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunk Negative — Request Smuggling" +description: "A chunked request with a negative chunk size: -1\\r\\nhello\\r\\n0\\r\\n\\r\\n. Tested against RFC 9112 §7.1." +weight: 31 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-NEGATIVE` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +A chunked request with a negative chunk size: `-1\r\nhello\r\n0\r\n\r\n`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +-1\r\n +hello\r\n +0\r\n +\r\n +``` + +The chunk size `-1` is negative. + + +## What the RFC says + +> chunk-size = 1*HEXDIG +> +> — RFC 9112 §7.1 + +The `HEXDIG` production (RFC 5234 §B.1) allows only the characters `0`-`9`, `A`-`F`, and `a`-`f`. A minus sign (`-`) is not a HEXDIG, so `-1` does not match the `chunk-size` rule. Additionally, the RFC requires implementations to guard against overflow: + +> "...recipients MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer representation." +> +> — RFC 9112 §7.1 + +## Why it matters + +A parser that interprets `-1` as a signed integer may wrap it to a very large unsigned value, causing it to read far beyond the actual data. This can lead to buffer over-reads, denial of service, or desynchronization with a stricter parser that rejects the negative value. + +## Deep Analysis + +### Relevant ABNF (RFC 9112 §7.1) + +``` +chunked-body = *chunk last-chunk trailer-section CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG +chunk-data = 1*OCTET ; a sequence of chunk-size octets +last-chunk = 1*("0") [ chunk-ext ] CRLF + +HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" +DIGIT = %x30-39 ; 0-9 +``` + +### RFC Evidence + +**RFC 9112 §7.1** defines chunk-size: + +> "chunk-size = 1*HEXDIG" + +The `HEXDIG` production (RFC 5234 §B.1) permits only the characters `0`-`9`, `A`-`F`, and `a`-`f`. The minus sign (`-`, 0x2D) is not a HEXDIG. + +**RFC 9112 §7.1** mandates overflow protection: + +> "A recipient MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer conversion." + +This requirement specifically addresses integer parsing safety. A negative sign introduces signed integer semantics that the ABNF never permits -- the chunk-size is an unsigned hexadecimal numeral by definition. + +**RFC 9112 §7** establishes the decoding obligation: + +> "A recipient MUST be able to parse and decode the chunked transfer coding." + +If the chunk-size cannot be parsed according to the ABNF, the chunked coding cannot be decoded, and the message must be rejected. + +### Step-by-Step ABNF Violation + +1. The parser begins reading `chunk-size` = `1*HEXDIG`. +2. The first byte is `-` (0x2D, minus sign). +3. `-` is not a HEXDIG. The `1*HEXDIG` production requires at least one HEXDIG as the very first character. +4. The production fails immediately at the first character. No valid chunk-size can be read. +5. Since the `chunk` production fails, the entire `chunked-body` is invalid. + +### Real-World Smuggling Scenario + +**Attack vector:** An attacker sends `-1\r\nhello\r\n0\r\n\r\n`. A parser using a signed integer conversion function (like C's `strtol()` or `atoi()`) may interpret `-1` as the signed integer -1. When cast to an unsigned type (e.g., `size_t` on a 64-bit system), -1 becomes `0xFFFFFFFFFFFFFFFF` (18,446,744,073,709,551,615 bytes). The parser then attempts to read that many bytes of chunk data, causing: + +- **Buffer over-read:** The parser reads far beyond the actual data, potentially exposing memory contents from adjacent buffers (similar to Heartbleed-style information disclosure). +- **Denial of service:** The parser hangs waiting for approximately 18 exabytes of data that will never arrive. +- **Desynchronization:** If the parser reads some fixed buffer amount and then fails, leftover bytes corrupt the connection state. + +A strict parser rejects `-1` immediately because `-` is not HEXDIG. If a strict front-end rejects but a lenient back-end somehow receives the raw bytes (e.g., via a non-parsing L4 proxy), the back-end may exhibit undefined behavior. + +Integer signedness issues in HTTP chunk size parsing have been a recurring vulnerability class. CVE-2015-3183 (Apache httpd) involved chunk parsing errors that enabled smuggling. The signed/unsigned integer confusion in chunk sizes is a well-documented anti-pattern in HTTP security research, and the explicit `1*HEXDIG` grammar (excluding `-`) is intentionally designed to prevent it. + +## Sources + +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/smuggling/chunk-size-plus.md b/docs/content/docs/smuggling/chunk-size-plus.md index 0ccbbb6..8e2cc26 100644 --- a/docs/content/docs/smuggling/chunk-size-plus.md +++ b/docs/content/docs/smuggling/chunk-size-plus.md @@ -1,42 +1,42 @@ ---- -title: "CHUNK-SIZE-PLUS" -description: "SMUG-CHUNK-SIZE-PLUS test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-SIZE-PLUS` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | Implicit MUST (grammar violation) | -| **Expected** | `400` or close | - -## What it sends - -A chunked request where chunk-size is prefixed by `+`. - -```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 -\r\n -``` - -## What the RFC says - -> "chunk-size = 1*HEXDIG" -- RFC 9112 Section 7.1 - -The plus sign is not a hexadecimal digit. The chunk-size token is invalid. - -## Why it matters - -Lenient numeric parsing (`+5`) in one component and strict parsing in another creates parser disagreement and desync opportunities. - -## Sources - -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunk Size Plus — Request Smuggling" +description: "A chunked request where chunk-size is prefixed by +. Tested against RFC 9112 §7.1." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-SIZE-PLUS` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | Implicit MUST (grammar violation) | +| **Expected** | `400` or close | + +## What it sends + +A chunked request where chunk-size is prefixed by `+`. + +```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 +\r\n +``` + +## What the RFC says + +> "chunk-size = 1*HEXDIG" -- RFC 9112 Section 7.1 + +The plus sign is not a hexadecimal digit. The chunk-size token is invalid. + +## Why it matters + +Lenient numeric parsing (`+5`) in one component and strict parsing in another creates parser disagreement and desync opportunities. + +## Sources + +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/smuggling/chunk-size-trailing-ows.md b/docs/content/docs/smuggling/chunk-size-trailing-ows.md index 5fa1762..fa63735 100644 --- a/docs/content/docs/smuggling/chunk-size-trailing-ows.md +++ b/docs/content/docs/smuggling/chunk-size-trailing-ows.md @@ -1,42 +1,42 @@ ---- -title: "CHUNK-SIZE-TRAILING-OWS" -description: "SMUG-CHUNK-SIZE-TRAILING-OWS test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-SIZE-TRAILING-OWS` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | Implicit MUST (grammar violation) | -| **Expected** | `400` or close | - -## What it sends - -A chunked request with trailing whitespace after the chunk-size token. - -```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 -\r\n -``` - -## What the RFC says - -> "chunk-size = 1*HEXDIG" -- RFC 9112 Section 7.1 - -Whitespace is not part of `HEXDIG`; trailing OWS in chunk-size is invalid. - -## Why it matters - -Some parsers trim this value while others reject it. Differential behavior can create request boundary disagreements. - -## Sources - -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunk Size Trailing OWS — Request Smuggling" +description: "A chunked request with trailing whitespace after the chunk-size token. Tested against RFC 9112 §7.1." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-SIZE-TRAILING-OWS` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | Implicit MUST (grammar violation) | +| **Expected** | `400` or close | + +## What it sends + +A chunked request with trailing whitespace after the chunk-size token. + +```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 +\r\n +``` + +## What the RFC says + +> "chunk-size = 1*HEXDIG" -- RFC 9112 Section 7.1 + +Whitespace is not part of `HEXDIG`; trailing OWS in chunk-size is invalid. + +## Why it matters + +Some parsers trim this value while others reject it. Differential behavior can create request boundary disagreements. + +## Sources + +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/smuggling/chunk-spill.md b/docs/content/docs/smuggling/chunk-spill.md index 8c4c67b..5b76fcb 100644 --- a/docs/content/docs/smuggling/chunk-spill.md +++ b/docs/content/docs/smuggling/chunk-spill.md @@ -1,114 +1,114 @@ ---- -title: "CHUNK-SPILL" -description: "CHUNK-SPILL test documentation" -weight: 26 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-SPILL` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -A chunked request that declares chunk size `5` but sends 7 bytes of data (`hello!!`), followed by the terminator. - -```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 -\r\n -``` - -The chunk size declares 5 bytes but the data is `hello!!` (7 bytes). - - -## What the RFC says - -> "The chunk-size field is a string of hex digits indicating the size of the chunk-data in octets." — RFC 9112 §7.1 - -> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 §7.1 - -The ABNF grammar specifies: - -> `chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF` -> -> `chunk-data = 1*OCTET ; a sequence of chunk-size octets` - -The grammar specifies that `chunk-data` is exactly `chunk-size` octets, followed by CRLF. A conforming parser reads exactly that many bytes, then expects `\r\n`. When more data is sent than declared (7 bytes vs. 5), the excess bytes (`!!`) land where the CRLF terminator should be, violating the grammar. - -## Why it matters - -An oversized chunk is a framing violation. If a lenient parser reads past the declared size, it desynchronizes from a strict parser that reads exactly `chunk-size` bytes. This discrepancy is exploitable for smuggling the excess bytes as part of the next request. - -## Deep Analysis - -### Relevant ABNF (RFC 9112 §7.1) - -``` -chunked-body = *chunk last-chunk trailer-section CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG -chunk-data = 1*OCTET ; a sequence of chunk-size octets -``` - -### RFC Evidence - -**RFC 9112 §7.1** defines `chunk-data`: - -> "chunk-data = 1*OCTET ; a sequence of chunk-size octets" - -The comment "a sequence of chunk-size octets" is normative context: the parser MUST read exactly `chunk-size` octets, no more and no fewer. After those octets, a CRLF must follow. - -**RFC 9112 §7.1** defines the full chunk production: - -> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" - -After reading `chunk-size` octets of data, the parser expects CRLF. If the bytes at position `chunk-size + 1` and `chunk-size + 2` (relative to the start of chunk-data) are not `\r\n`, the framing is violated. - -**RFC 9112 §7** establishes the parsing obligation: - -> "A recipient MUST be able to parse and decode the chunked transfer coding." - -Successful decoding requires that the declared chunk-size matches the actual data length. An oversized chunk makes decoding impossible without either truncating or absorbing extra bytes. - -### Step-by-Step ABNF Violation - -1. The parser reads `chunk-size` = `5` (valid HEXDIG, value 5), then CRLF (valid). -2. The parser reads exactly 5 bytes of `chunk-data`: `h`, `e`, `l`, `l`, `o`. -3. The parser now expects CRLF (0x0D 0x0A) at bytes 6-7 of the data segment. -4. Byte 6 is `!` (0x21) and byte 7 is `!` (0x21). These are not `\r\n`. -5. The trailing CRLF check fails. The `chunk` production is violated. -6. The actual data sent is `hello!!` (7 bytes), but only 5 were declared. The 2 excess bytes (`!!`) occupy the position where CRLF should be. - -### Real-World Smuggling Scenario - -**Attack vector:** An attacker sends a chunk declaring size 5 but containing 7 bytes (`hello!!`). The byte layout is: - -``` -5\r\n -- chunk-size line -hello!!\r\n -- 7 bytes + CRLF (but chunk-size said 5) -0\r\n\r\n -- last-chunk + trailer terminator -``` - -A strict parser reads 5 bytes (`hello`), then expects CRLF at position 6. It finds `!!` instead and rejects the message. A lenient parser might: - -- Read until it finds CRLF, effectively accepting `hello!!` (7 bytes) as chunk data despite the size mismatch. It then continues parsing normally. -- Read 5 bytes, find `!!` instead of CRLF, and try error recovery by scanning forward for CRLF. - -In a proxy chain, the lenient front-end accepts the 7-byte chunk, but if it re-encodes the body for the back-end (e.g., recalculating chunk-size), the back-end sees a different message. Alternatively, if the front-end passes through raw bytes, the back-end reads 5 bytes and then encounters `!!` where it expects a new chunk-size line -- potentially interpreting `!!` as the start of a new chunk, corrupting the request stream. - -Chunk size/data length mismatches are a core technique in HTTP smuggling. CVE-2015-3183 (Apache httpd) exploited improper chunk size validation to desynchronize front-end and back-end parsers. Research on chunk-spill attacks has demonstrated that oversized chunks are a practical vector for smuggling arbitrary data past proxy validation. - -## Sources - -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunk Spill — Request Smuggling" +description: "A chunked request that declares chunk size 5 but sends 7 bytes of data (hello!!), followed by the terminator. Tested against RFC 9112 §7.1." +weight: 26 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-SPILL` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +A chunked request that declares chunk size `5` but sends 7 bytes of data (`hello!!`), followed by the terminator. + +```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 +\r\n +``` + +The chunk size declares 5 bytes but the data is `hello!!` (7 bytes). + + +## What the RFC says + +> "The chunk-size field is a string of hex digits indicating the size of the chunk-data in octets." — RFC 9112 §7.1 + +> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 §7.1 + +The ABNF grammar specifies: + +> `chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF` +> +> `chunk-data = 1*OCTET ; a sequence of chunk-size octets` + +The grammar specifies that `chunk-data` is exactly `chunk-size` octets, followed by CRLF. A conforming parser reads exactly that many bytes, then expects `\r\n`. When more data is sent than declared (7 bytes vs. 5), the excess bytes (`!!`) land where the CRLF terminator should be, violating the grammar. + +## Why it matters + +An oversized chunk is a framing violation. If a lenient parser reads past the declared size, it desynchronizes from a strict parser that reads exactly `chunk-size` bytes. This discrepancy is exploitable for smuggling the excess bytes as part of the next request. + +## Deep Analysis + +### Relevant ABNF (RFC 9112 §7.1) + +``` +chunked-body = *chunk last-chunk trailer-section CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG +chunk-data = 1*OCTET ; a sequence of chunk-size octets +``` + +### RFC Evidence + +**RFC 9112 §7.1** defines `chunk-data`: + +> "chunk-data = 1*OCTET ; a sequence of chunk-size octets" + +The comment "a sequence of chunk-size octets" is normative context: the parser MUST read exactly `chunk-size` octets, no more and no fewer. After those octets, a CRLF must follow. + +**RFC 9112 §7.1** defines the full chunk production: + +> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" + +After reading `chunk-size` octets of data, the parser expects CRLF. If the bytes at position `chunk-size + 1` and `chunk-size + 2` (relative to the start of chunk-data) are not `\r\n`, the framing is violated. + +**RFC 9112 §7** establishes the parsing obligation: + +> "A recipient MUST be able to parse and decode the chunked transfer coding." + +Successful decoding requires that the declared chunk-size matches the actual data length. An oversized chunk makes decoding impossible without either truncating or absorbing extra bytes. + +### Step-by-Step ABNF Violation + +1. The parser reads `chunk-size` = `5` (valid HEXDIG, value 5), then CRLF (valid). +2. The parser reads exactly 5 bytes of `chunk-data`: `h`, `e`, `l`, `l`, `o`. +3. The parser now expects CRLF (0x0D 0x0A) at bytes 6-7 of the data segment. +4. Byte 6 is `!` (0x21) and byte 7 is `!` (0x21). These are not `\r\n`. +5. The trailing CRLF check fails. The `chunk` production is violated. +6. The actual data sent is `hello!!` (7 bytes), but only 5 were declared. The 2 excess bytes (`!!`) occupy the position where CRLF should be. + +### Real-World Smuggling Scenario + +**Attack vector:** An attacker sends a chunk declaring size 5 but containing 7 bytes (`hello!!`). The byte layout is: + +``` +5\r\n -- chunk-size line +hello!!\r\n -- 7 bytes + CRLF (but chunk-size said 5) +0\r\n\r\n -- last-chunk + trailer terminator +``` + +A strict parser reads 5 bytes (`hello`), then expects CRLF at position 6. It finds `!!` instead and rejects the message. A lenient parser might: + +- Read until it finds CRLF, effectively accepting `hello!!` (7 bytes) as chunk data despite the size mismatch. It then continues parsing normally. +- Read 5 bytes, find `!!` instead of CRLF, and try error recovery by scanning forward for CRLF. + +In a proxy chain, the lenient front-end accepts the 7-byte chunk, but if it re-encodes the body for the back-end (e.g., recalculating chunk-size), the back-end sees a different message. Alternatively, if the front-end passes through raw bytes, the back-end reads 5 bytes and then encounters `!!` where it expects a new chunk-size line -- potentially interpreting `!!` as the start of a new chunk, corrupting the request stream. + +Chunk size/data length mismatches are a core technique in HTTP smuggling. CVE-2015-3183 (Apache httpd) exploited improper chunk size validation to desynchronize front-end and back-end parsers. Research on chunk-spill attacks has demonstrated that oversized chunks are a practical vector for smuggling arbitrary data past proxy validation. + +## Sources + +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/smuggling/chunk-underscore.md b/docs/content/docs/smuggling/chunk-underscore.md index a3df714..59e376b 100644 --- a/docs/content/docs/smuggling/chunk-underscore.md +++ b/docs/content/docs/smuggling/chunk-underscore.md @@ -1,107 +1,107 @@ ---- -title: "CHUNK-UNDERSCORE" -description: "CHUNK-UNDERSCORE test documentation" -weight: 21 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNK-UNDERSCORE` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -Chunk size `1_0` — with underscore separator. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -\r\n -1_0\r\n -hello world!!!!!\r\n -0\r\n -\r\n -``` - -The chunk size `1_0` uses an underscore separator (like numeric literals in some languages). - - -## What the RFC says - -> "The chunk-size field is a string of hex digits indicating the size of the chunk-data in octets." — RFC 9112 §7.1 - -The ABNF grammar is strict: - -> `chunk-size = 1*HEXDIG` - -The `HEXDIG` production (RFC 5234 §B.1) allows only the characters `0`-`9`, `A`-`F`, and `a`-`f`. An underscore (`_`) is not a HEXDIG, so `1_0` does not match the `chunk-size` rule. - -> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 §7.1 - -## Why it matters - -Languages like Python, Rust, Java, and C# accept `_` as a visual separator in numeric literals (e.g., `1_000_000`). If a server uses such a parser internally and interprets `1_0` as hex 16 (decimal), it reads 16 bytes of chunk data instead of 1. The strict parser sees chunk size `1` (stopping at the underscore), causing a framing disagreement that enables smuggling. - -## Deep Analysis - -### Relevant ABNF (RFC 9112 §7.1) - -``` -chunked-body = *chunk last-chunk trailer-section CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG - -HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" -DIGIT = %x30-39 ; 0-9 -``` - -### RFC Evidence - -**RFC 9112 §7.1** defines chunk-size: - -> "chunk-size = 1*HEXDIG" - -The `HEXDIG` production (RFC 5234 §B.1) is an exhaustive list: `0`-`9`, `A`-`F`, `a`-`f`. The underscore character (`_`, 0x5F) is not in this list. - -**RFC 9112 §7.1** mandates overflow protection, which implies strict numeric parsing: - -> "A recipient MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer conversion." - -This requirement reinforces that chunk-size parsing must be implemented with careful numeric handling -- not delegated to general-purpose integer parsers that may accept non-HEXDIG characters. - -**RFC 9112 §7.1** defines the chunk production: - -> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" - -After `chunk-size`, only a `chunk-ext` (starting with `;`) or `CRLF` is valid. An underscore matches neither. - -### Step-by-Step ABNF Violation - -1. The parser begins reading `chunk-size` = `1*HEXDIG`. -2. It reads `1` -- valid HEXDIG. Current chunk-size value: 0x1 = 1. -3. It reads `_` (0x5F) -- **not a HEXDIG**. The underscore is not in `0-9`, `A-F`, `a-f`. -4. The parser should stop reading `chunk-size` here. But what happens next depends on implementation: - - **Strict parser:** After reading chunk-size `1`, it expects `;` or CRLF. `_` is neither. Parse failure -- reject with 400. - - **Parser that stops at non-HEXDIG:** Reads chunk-size `1`, stops at `_`, then expects `;` or CRLF. `_` is neither. Parse failure. - - **Language-aware parser (accepting `_` as digit separator):** Reads `1_0` as 0x10 = 16 decimal. It then reads 16 bytes of chunk data. -5. The intended chunk data is `hello world!!!!!` (16 bytes if interpreted as hex 0x10), but only 1 byte (`h`) if interpreted as hex 0x1. This creates a massive framing disagreement. - -### Real-World Smuggling Scenario - -**Attack vector:** An attacker sends `1_0\r\nhello world!!!!!\r\n0\r\n\r\n`. The key ambiguity: - -- A strict parser reads chunk-size `1` (stopping at `_`), expects CRLF, finds `_` instead, and rejects. -- A parser that treats `_` as a digit separator reads chunk-size 0x10 = 16, reads 16 bytes (`hello world!!!!!`), and processes the message normally. -- A parser that reads `1` and then tolerates `_0` as a chunk-ext or ignores it reads only 1 byte of chunk data (`h`), then expects CRLF where `e` is -- causing further cascading misparse. - -The underscore-as-digit-separator pattern is particularly insidious because it is valid syntax in Python (`0x1_0`), Rust (`0x1_0`), Java (`0x1_0`), C# (`0x1_0`), Ruby (`0x1_0`), and modern JavaScript (`0x1_0`). Any server that delegates chunk-size parsing to a standard library integer parser in these languages may silently accept `1_0` as 16. This was identified as a risk in HTTP parser security audits, where the recommendation is to use a purpose-built HEXDIG-only parser rather than general-purpose `parseInt()`-style functions. - -## Sources - -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunk Underscore — Request Smuggling" +description: "Chunk size 1_0 — with underscore separator. Tested against RFC 9112 §7.1." +weight: 21 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNK-UNDERSCORE` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +Chunk size `1_0` — with underscore separator. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +\r\n +1_0\r\n +hello world!!!!!\r\n +0\r\n +\r\n +``` + +The chunk size `1_0` uses an underscore separator (like numeric literals in some languages). + + +## What the RFC says + +> "The chunk-size field is a string of hex digits indicating the size of the chunk-data in octets." — RFC 9112 §7.1 + +The ABNF grammar is strict: + +> `chunk-size = 1*HEXDIG` + +The `HEXDIG` production (RFC 5234 §B.1) allows only the characters `0`-`9`, `A`-`F`, and `a`-`f`. An underscore (`_`) is not a HEXDIG, so `1_0` does not match the `chunk-size` rule. + +> "A recipient MUST be able to parse and decode the chunked transfer coding." — RFC 9112 §7.1 + +## Why it matters + +Languages like Python, Rust, Java, and C# accept `_` as a visual separator in numeric literals (e.g., `1_000_000`). If a server uses such a parser internally and interprets `1_0` as hex 16 (decimal), it reads 16 bytes of chunk data instead of 1. The strict parser sees chunk size `1` (stopping at the underscore), causing a framing disagreement that enables smuggling. + +## Deep Analysis + +### Relevant ABNF (RFC 9112 §7.1) + +``` +chunked-body = *chunk last-chunk trailer-section CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG + +HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" +DIGIT = %x30-39 ; 0-9 +``` + +### RFC Evidence + +**RFC 9112 §7.1** defines chunk-size: + +> "chunk-size = 1*HEXDIG" + +The `HEXDIG` production (RFC 5234 §B.1) is an exhaustive list: `0`-`9`, `A`-`F`, `a`-`f`. The underscore character (`_`, 0x5F) is not in this list. + +**RFC 9112 §7.1** mandates overflow protection, which implies strict numeric parsing: + +> "A recipient MUST anticipate potentially large hexadecimal numerals and prevent parsing errors due to integer conversion overflows or precision loss due to integer conversion." + +This requirement reinforces that chunk-size parsing must be implemented with careful numeric handling -- not delegated to general-purpose integer parsers that may accept non-HEXDIG characters. + +**RFC 9112 §7.1** defines the chunk production: + +> "chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF" + +After `chunk-size`, only a `chunk-ext` (starting with `;`) or `CRLF` is valid. An underscore matches neither. + +### Step-by-Step ABNF Violation + +1. The parser begins reading `chunk-size` = `1*HEXDIG`. +2. It reads `1` -- valid HEXDIG. Current chunk-size value: 0x1 = 1. +3. It reads `_` (0x5F) -- **not a HEXDIG**. The underscore is not in `0-9`, `A-F`, `a-f`. +4. The parser should stop reading `chunk-size` here. But what happens next depends on implementation: + - **Strict parser:** After reading chunk-size `1`, it expects `;` or CRLF. `_` is neither. Parse failure -- reject with 400. + - **Parser that stops at non-HEXDIG:** Reads chunk-size `1`, stops at `_`, then expects `;` or CRLF. `_` is neither. Parse failure. + - **Language-aware parser (accepting `_` as digit separator):** Reads `1_0` as 0x10 = 16 decimal. It then reads 16 bytes of chunk data. +5. The intended chunk data is `hello world!!!!!` (16 bytes if interpreted as hex 0x10), but only 1 byte (`h`) if interpreted as hex 0x1. This creates a massive framing disagreement. + +### Real-World Smuggling Scenario + +**Attack vector:** An attacker sends `1_0\r\nhello world!!!!!\r\n0\r\n\r\n`. The key ambiguity: + +- A strict parser reads chunk-size `1` (stopping at `_`), expects CRLF, finds `_` instead, and rejects. +- A parser that treats `_` as a digit separator reads chunk-size 0x10 = 16, reads 16 bytes (`hello world!!!!!`), and processes the message normally. +- A parser that reads `1` and then tolerates `_0` as a chunk-ext or ignores it reads only 1 byte of chunk data (`h`), then expects CRLF where `e` is -- causing further cascading misparse. + +The underscore-as-digit-separator pattern is particularly insidious because it is valid syntax in Python (`0x1_0`), Rust (`0x1_0`), Java (`0x1_0`), C# (`0x1_0`), Ruby (`0x1_0`), and modern JavaScript (`0x1_0`). Any server that delegates chunk-size parsing to a standard library integer parser in these languages may silently accept `1_0` as 16. This was identified as a risk in HTTP parser security audits, where the recommendation is to use a purpose-built HEXDIG-only parser rather than general-purpose `parseInt()`-style functions. + +## Sources + +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/smuggling/chunked-with-params.md b/docs/content/docs/smuggling/chunked-with-params.md index fc28f1a..e386e36 100644 --- a/docs/content/docs/smuggling/chunked-with-params.md +++ b/docs/content/docs/smuggling/chunked-with-params.md @@ -1,94 +1,94 @@ ---- -title: "CHUNKED-WITH-PARAMS" -description: "CHUNKED-WITH-PARAMS test documentation" -weight: 32 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CHUNKED-WITH-PARAMS` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | -| **Requirement** | Unscored | -| **Expected** | `400` or `2xx` | - -## What it sends - -`Transfer-Encoding: chunked;ext=val` — parameters on the chunked coding. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked;ext=val\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -The `chunked` encoding name has parameters appended (`;ext=val`). - - -## What the RFC says - -> "The chunked coding does not define any parameters. Their presence SHOULD be treated as an error." -> -> — RFC 9112 §7.1 - -## Why this test is unscored - -The RFC says parameters on `chunked` SHOULD be treated as an error -- this is a SHOULD-level requirement, not MUST. Some servers strip the parameters and decode the chunked body normally; others reject with 400. Both behaviors are defensible, so neither outcome is marked as a failure. - -## Why it matters - -If a front-end strips the parameter and forwards the body as chunked, but the back-end rejects the Transfer-Encoding value entirely and falls back to Content-Length, the two parsers disagree on message framing -- a classic CL/TE desynchronization vector. - -## Deep Analysis - -### Relevant ABNF (RFC 9112 §7.1) - -``` -chunked-body = *chunk last-chunk trailer-section CRLF - -chunk = chunk-size [ chunk-ext ] CRLF - chunk-data CRLF -chunk-size = 1*HEXDIG -``` - -Note: The `chunk-ext` grammar in §7.1.1 applies to extensions on individual **chunk-size lines**, not to the `Transfer-Encoding` header value itself. The `chunked` coding name in the header is a transfer coding token, separate from chunk-level extensions. - -### RFC Evidence - -**RFC 9112 §7.1** explicitly states: - -> "The chunked coding does not define any parameters. Their presence SHOULD be treated as an error." - -This is a SHOULD-level requirement (not MUST), meaning implementations are strongly recommended to treat parameters as an error but are not strictly required to do so. - -**RFC 9112 §7** establishes the baseline parsing obligation: - -> "A recipient MUST be able to parse and decode the chunked transfer coding." - -**RFC 9112 §7** also covers unrecognized codings: - -> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." - -### Step-by-Step Analysis - -1. The server receives `Transfer-Encoding: chunked;ext=val`. -2. It must parse the transfer coding token. The coding name is `chunked`. -3. After the coding name, `;ext=val` appears as a parameter on the coding itself (not a chunk extension on a chunk-size line). -4. Per §7.1, the chunked coding defines no parameters. The text says their presence "SHOULD be treated as an error." -5. A strict server rejects with 400. A lenient server strips the parameter and processes the body as chunked. Both behaviors are defensible under SHOULD-level language. -6. Critically, this request also includes `Content-Length: 5`. If the server rejects the Transfer-Encoding value and falls back to Content-Length, it reads the raw body `hello` as a flat 5-byte body rather than as chunked data. - -### Real-World Smuggling Scenario - -This is a classic **CL/TE desynchronization** vector: - -**Attack vector:** The request includes both `Transfer-Encoding: chunked;ext=val` and `Content-Length: 5`. A front-end proxy recognizes `chunked` (ignoring the parameter) and processes the body using chunked framing. The back-end rejects the parameterized `chunked;ext=val` as invalid and falls back to `Content-Length: 5`, reading 5 bytes of raw body. The remaining bytes after those 5 are left in the connection buffer and get prepended to the next request -- this is request smuggling. - -This pattern was demonstrated in James Kettle's original HTTP request smuggling research (2019) and is the foundation of CL/TE attacks. CVE-2021-22959 (llhttp) and CVE-2022-32213 (Node.js) involved similar Transfer-Encoding parsing discrepancies where malformed TE values caused some parsers to fall back to Content-Length. - -## Sources - -- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) +--- +title: "Chunked With Params — Request Smuggling" +description: "Transfer-Encoding: chunked;ext=val — parameters on the chunked coding. Tested against RFC 9112 §7.1." +weight: 32 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CHUNKED-WITH-PARAMS` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) | +| **Requirement** | Unscored | +| **Expected** | `400` or `2xx` | + +## What it sends + +`Transfer-Encoding: chunked;ext=val` — parameters on the chunked coding. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked;ext=val\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +The `chunked` encoding name has parameters appended (`;ext=val`). + + +## What the RFC says + +> "The chunked coding does not define any parameters. Their presence SHOULD be treated as an error." +> +> — RFC 9112 §7.1 + +## Why this test is unscored + +The RFC says parameters on `chunked` SHOULD be treated as an error -- this is a SHOULD-level requirement, not MUST. Some servers strip the parameters and decode the chunked body normally; others reject with 400. Both behaviors are defensible, so neither outcome is marked as a failure. + +## Why it matters + +If a front-end strips the parameter and forwards the body as chunked, but the back-end rejects the Transfer-Encoding value entirely and falls back to Content-Length, the two parsers disagree on message framing -- a classic CL/TE desynchronization vector. + +## Deep Analysis + +### Relevant ABNF (RFC 9112 §7.1) + +``` +chunked-body = *chunk last-chunk trailer-section CRLF + +chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF +chunk-size = 1*HEXDIG +``` + +Note: The `chunk-ext` grammar in §7.1.1 applies to extensions on individual **chunk-size lines**, not to the `Transfer-Encoding` header value itself. The `chunked` coding name in the header is a transfer coding token, separate from chunk-level extensions. + +### RFC Evidence + +**RFC 9112 §7.1** explicitly states: + +> "The chunked coding does not define any parameters. Their presence SHOULD be treated as an error." + +This is a SHOULD-level requirement (not MUST), meaning implementations are strongly recommended to treat parameters as an error but are not strictly required to do so. + +**RFC 9112 §7** establishes the baseline parsing obligation: + +> "A recipient MUST be able to parse and decode the chunked transfer coding." + +**RFC 9112 §7** also covers unrecognized codings: + +> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." + +### Step-by-Step Analysis + +1. The server receives `Transfer-Encoding: chunked;ext=val`. +2. It must parse the transfer coding token. The coding name is `chunked`. +3. After the coding name, `;ext=val` appears as a parameter on the coding itself (not a chunk extension on a chunk-size line). +4. Per §7.1, the chunked coding defines no parameters. The text says their presence "SHOULD be treated as an error." +5. A strict server rejects with 400. A lenient server strips the parameter and processes the body as chunked. Both behaviors are defensible under SHOULD-level language. +6. Critically, this request also includes `Content-Length: 5`. If the server rejects the Transfer-Encoding value and falls back to Content-Length, it reads the raw body `hello` as a flat 5-byte body rather than as chunked data. + +### Real-World Smuggling Scenario + +This is a classic **CL/TE desynchronization** vector: + +**Attack vector:** The request includes both `Transfer-Encoding: chunked;ext=val` and `Content-Length: 5`. A front-end proxy recognizes `chunked` (ignoring the parameter) and processes the body using chunked framing. The back-end rejects the parameterized `chunked;ext=val` as invalid and falls back to `Content-Length: 5`, reading 5 bytes of raw body. The remaining bytes after those 5 are left in the connection buffer and get prepended to the next request -- this is request smuggling. + +This pattern was demonstrated in James Kettle's original HTTP request smuggling research (2019) and is the foundation of CL/TE attacks. CVE-2021-22959 (llhttp) and CVE-2022-32213 (Node.js) involved similar Transfer-Encoding parsing discrepancies where malformed TE values caused some parsers to fall back to Content-Length. + +## Sources + +- [RFC 9112 §7.1](https://www.rfc-editor.org/rfc/rfc9112#section-7.1) diff --git a/docs/content/docs/smuggling/cl-comma-different.md b/docs/content/docs/smuggling/cl-comma-different.md index aee432d..78ff1fb 100644 --- a/docs/content/docs/smuggling/cl-comma-different.md +++ b/docs/content/docs/smuggling/cl-comma-different.md @@ -1,85 +1,85 @@ ---- -title: "CL-COMMA-DIFFERENT" -description: "CL-COMMA-DIFFERENT test documentation" -weight: 15 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-COMMA-DIFFERENT` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -`Content-Length: 5, 10` — comma-separated CL with different values. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5, 10\r\n -\r\n -hello -``` - - -## What the RFC says - -RFC 9110 §8.6 defines the Content-Length grammar: - -> "Content-Length = 1*DIGIT" - -A sender MUST NOT forward invalid Content-Length. RFC 9112 §6.3 specifies how invalid values must be handled: - -> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." - -The value `5, 10` can be parsed as a comma-separated list, but the two values differ. The exception does not apply, so this is an unrecoverable error: - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." - -## Why it matters - -Comma-separated CL values are equivalent to multiple CL headers. Different values create ambiguity about body length — if one parser picks 5 and another picks 10, they disagree on the body boundary, enabling request smuggling. - -## Deep Analysis - -### ABNF Violation - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) with no other characters. The value `5, 10` contains a comma and a space, neither of which is a DIGIT. Therefore `5, 10` does not match `1*DIGIT` and is invalid on its face. - -### RFC Evidence Chain - -**Step 1 -- The value is invalid per the grammar.** - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 - -**Step 2 -- The comma-separated list exception is evaluated.** - -RFC 9112 §6.3 provides a narrow exception for comma-separated Content-Length values: - -> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same (in which case, the message is processed with that single value used as the Content-Length field value)." - -Parsing `5, 10` as a comma-separated list yields two members: `5` and `10`. Both are individually valid `1*DIGIT` values, **but they are not the same**. The exception requires "all values in the list are the same", which fails here. Therefore the exception does not apply and this remains an unrecoverable error. - -**Step 3 -- The server must reject with 400.** - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 - -### Scored / Unscored Justification - -This test is **scored** (MUST reject). The comma-separated list exception explicitly requires all values to be identical. Since `5` and `10` differ, no exception applies, and the RFC mandates a 400 response. A `2xx` response is a compliance failure. - -### Real-World Smuggling Scenario - -In a reverse-proxy chain, the front-end may parse the comma-separated list and select the first value (`5`), reading only 5 bytes of body. The back-end may select the last value (`10`), expecting 10 bytes. The front-end forwards only 5 bytes of body, but the back-end waits for 5 more -- consuming the beginning of the next legitimate request as body data. Alternatively, if the front-end picks `10` and the back-end picks `5`, the extra 5 bytes spill forward and are interpreted as a new request. Either way, the attacker controls the boundary between requests. - -## Sources - -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL Comma Different — Request Smuggling" +description: "Content-Length: 5, 10 — comma-separated CL with different values. Tested against RFC 9110 Section 8.6." +weight: 15 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-COMMA-DIFFERENT` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +`Content-Length: 5, 10` — comma-separated CL with different values. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5, 10\r\n +\r\n +hello +``` + + +## What the RFC says + +RFC 9110 §8.6 defines the Content-Length grammar: + +> "Content-Length = 1*DIGIT" + +A sender MUST NOT forward invalid Content-Length. RFC 9112 §6.3 specifies how invalid values must be handled: + +> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." + +The value `5, 10` can be parsed as a comma-separated list, but the two values differ. The exception does not apply, so this is an unrecoverable error: + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." + +## Why it matters + +Comma-separated CL values are equivalent to multiple CL headers. Different values create ambiguity about body length — if one parser picks 5 and another picks 10, they disagree on the body boundary, enabling request smuggling. + +## Deep Analysis + +### ABNF Violation + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) with no other characters. The value `5, 10` contains a comma and a space, neither of which is a DIGIT. Therefore `5, 10` does not match `1*DIGIT` and is invalid on its face. + +### RFC Evidence Chain + +**Step 1 -- The value is invalid per the grammar.** + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 + +**Step 2 -- The comma-separated list exception is evaluated.** + +RFC 9112 §6.3 provides a narrow exception for comma-separated Content-Length values: + +> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same (in which case, the message is processed with that single value used as the Content-Length field value)." + +Parsing `5, 10` as a comma-separated list yields two members: `5` and `10`. Both are individually valid `1*DIGIT` values, **but they are not the same**. The exception requires "all values in the list are the same", which fails here. Therefore the exception does not apply and this remains an unrecoverable error. + +**Step 3 -- The server must reject with 400.** + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 + +### Scored / Unscored Justification + +This test is **scored** (MUST reject). The comma-separated list exception explicitly requires all values to be identical. Since `5` and `10` differ, no exception applies, and the RFC mandates a 400 response. A `2xx` response is a compliance failure. + +### Real-World Smuggling Scenario + +In a reverse-proxy chain, the front-end may parse the comma-separated list and select the first value (`5`), reading only 5 bytes of body. The back-end may select the last value (`10`), expecting 10 bytes. The front-end forwards only 5 bytes of body, but the back-end waits for 5 more -- consuming the beginning of the next legitimate request as body data. Alternatively, if the front-end picks `10` and the back-end picks `5`, the extra 5 bytes spill forward and are interpreted as a new request. Either way, the attacker controls the boundary between requests. + +## Sources + +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/cl-comma-same.md b/docs/content/docs/smuggling/cl-comma-same.md index cedb4fc..8695c21 100644 --- a/docs/content/docs/smuggling/cl-comma-same.md +++ b/docs/content/docs/smuggling/cl-comma-same.md @@ -1,95 +1,95 @@ ---- -title: "CL-COMMA-SAME" -description: "CL-COMMA-SAME test documentation" -weight: 31 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-COMMA-SAME` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **Requirement** | Unscored | -| **Expected** | `400` or `2xx` | - -## What it sends - -`Content-Length: 5, 5` — comma-separated CL with identical values. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5, 5\r\n -\r\n -hello -``` - -The Content-Length value `5, 5` has two identical comma-separated values. - - -## What the RFC says - -RFC 9110 §8.6 defines the Content-Length grammar: - -> "Content-Length = 1*DIGIT" - -The value `5, 5` does not match `1*DIGIT`, so it is technically invalid. However, RFC 9110 §8.6 provides an explicit exception: - -> "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" - -RFC 9112 §6.3 reinforces this: - -> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same (in which case, the message is processed with that single value used as the Content-Length field value)." - -## Why this test is unscored - -The RFC explicitly allows recipients to either reject or accept identical comma-separated CL values. Both `400` (strict rejection) and `2xx` (collapsing identical values) are RFC-compliant behaviors. - -## Why it matters - -While accepting identical comma-separated values is valid, it indicates the server's CL parser tolerates non-`1*DIGIT` input. This leniency could mask bugs in how the server handles other malformed Content-Length values. - -## Deep Analysis - -### ABNF Violation - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) with no other characters. The value `5, 5` contains a comma and a space, so it does not match `1*DIGIT`. Strictly speaking, this value is grammatically invalid. - -### RFC Evidence Chain - -**Step 1 -- The value is invalid per the grammar.** - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 - -**Step 2 -- The comma-separated list exception is evaluated.** - -RFC 9112 §6.3 provides a narrow exception: - -> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same (in which case, the message is processed with that single value used as the Content-Length field value)." - -Parsing `5, 5` as a comma-separated list yields two members: `5` and `5`. Both are valid `1*DIGIT` values, and both are the same. **The exception applies.** The recipient may process the message using `5` as the Content-Length. - -**Step 3 -- The recipient has explicit discretion.** - -RFC 9110 §8.6 reinforces this with an explicit MAY: - -> "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" - -Both rejection (400) and acceptance (collapsing to `5` and responding 2xx) are RFC-compliant. - -### Scored / Unscored Justification - -This test is **unscored**. The RFC explicitly grants recipients a MAY choice: reject or collapse. Neither behavior violates the specification. A 400 demonstrates stricter parsing (safer), while a 2xx demonstrates the permitted collapse behavior (also valid). Scoring either as wrong would contradict the RFC's own allowance. - -### Real-World Smuggling Scenario - -While identical comma-separated values do not directly create a body-length disagreement, accepting them reveals that the server's Content-Length parser tolerates non-`1*DIGIT` input. This leniency is a signal: if the parser strips commas and collapses values, it may also be lenient with other malformed Content-Length patterns (e.g., `5, 10` where values differ). Attackers probe with safe payloads like `5, 5` to fingerprint parser behavior before escalating to exploitable payloads. - -## Sources - -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL Comma Same — Request Smuggling" +description: "Content-Length: 5, 5 — comma-separated CL with identical values. Tested against RFC 9110 Section 8.6." +weight: 31 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-COMMA-SAME` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | Unscored | +| **Expected** | `400` or `2xx` | + +## What it sends + +`Content-Length: 5, 5` — comma-separated CL with identical values. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5, 5\r\n +\r\n +hello +``` + +The Content-Length value `5, 5` has two identical comma-separated values. + + +## What the RFC says + +RFC 9110 §8.6 defines the Content-Length grammar: + +> "Content-Length = 1*DIGIT" + +The value `5, 5` does not match `1*DIGIT`, so it is technically invalid. However, RFC 9110 §8.6 provides an explicit exception: + +> "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" + +RFC 9112 §6.3 reinforces this: + +> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same (in which case, the message is processed with that single value used as the Content-Length field value)." + +## Why this test is unscored + +The RFC explicitly allows recipients to either reject or accept identical comma-separated CL values. Both `400` (strict rejection) and `2xx` (collapsing identical values) are RFC-compliant behaviors. + +## Why it matters + +While accepting identical comma-separated values is valid, it indicates the server's CL parser tolerates non-`1*DIGIT` input. This leniency could mask bugs in how the server handles other malformed Content-Length values. + +## Deep Analysis + +### ABNF Violation + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) with no other characters. The value `5, 5` contains a comma and a space, so it does not match `1*DIGIT`. Strictly speaking, this value is grammatically invalid. + +### RFC Evidence Chain + +**Step 1 -- The value is invalid per the grammar.** + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 + +**Step 2 -- The comma-separated list exception is evaluated.** + +RFC 9112 §6.3 provides a narrow exception: + +> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same (in which case, the message is processed with that single value used as the Content-Length field value)." + +Parsing `5, 5` as a comma-separated list yields two members: `5` and `5`. Both are valid `1*DIGIT` values, and both are the same. **The exception applies.** The recipient may process the message using `5` as the Content-Length. + +**Step 3 -- The recipient has explicit discretion.** + +RFC 9110 §8.6 reinforces this with an explicit MAY: + +> "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" + +Both rejection (400) and acceptance (collapsing to `5` and responding 2xx) are RFC-compliant. + +### Scored / Unscored Justification + +This test is **unscored**. The RFC explicitly grants recipients a MAY choice: reject or collapse. Neither behavior violates the specification. A 400 demonstrates stricter parsing (safer), while a 2xx demonstrates the permitted collapse behavior (also valid). Scoring either as wrong would contradict the RFC's own allowance. + +### Real-World Smuggling Scenario + +While identical comma-separated values do not directly create a body-length disagreement, accepting them reveals that the server's Content-Length parser tolerates non-`1*DIGIT` input. This leniency is a signal: if the parser strips commas and collapses values, it may also be lenient with other malformed Content-Length patterns (e.g., `5, 10` where values differ). Attackers probe with safe payloads like `5, 5` to fingerprint parser behavior before escalating to exploitable payloads. + +## Sources + +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/cl-comma-triple.md b/docs/content/docs/smuggling/cl-comma-triple.md index aadf31a..b9561e8 100644 --- a/docs/content/docs/smuggling/cl-comma-triple.md +++ b/docs/content/docs/smuggling/cl-comma-triple.md @@ -1,37 +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) +--- +title: "CL Comma Triple — Request Smuggling" +description: "A POST request with three comma-separated identical Content-Length values, extending the duplicate-value merge test. Tested against RFC 9110 §8.6." +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/docs/content/docs/smuggling/cl-double-zero.md b/docs/content/docs/smuggling/cl-double-zero.md index eb9f500..43e2ab8 100644 --- a/docs/content/docs/smuggling/cl-double-zero.md +++ b/docs/content/docs/smuggling/cl-double-zero.md @@ -1,93 +1,93 @@ ---- -title: "CL-DOUBLE-ZERO" -description: "CL-DOUBLE-ZERO test documentation" -weight: 48 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-DOUBLE-ZERO` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **Requirement** | SHOULD | -| **Expected** | `400` or `2xx` | - -## What it sends - -Content-Length with a double-zero value: `Content-Length: 00`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 00\r\n -\r\n -``` - - -## What the RFC says - -RFC 9110 §8.6 defines the Content-Length grammar: - -> "Content-Length = 1*DIGIT" - -The value `00` matches the `1*DIGIT` grammar (two digits), so it is technically valid per the RFC. However, leading zeros create ambiguity when parsers interpret them differently — particularly when some treat leading-zero values as octal notation. - -RFC 9112 §6.3 states that an invalid Content-Length is an unrecoverable error: - -> "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." - -Since `00` is grammatically valid, the MUST-reject rule does not apply here. However, rejecting it is the safer behavior. - -## Pass / Warn - -The value `00` matches the `1*DIGIT` grammar, so it is technically valid. The RFC does not mandate rejection of grammatically valid Content-Length values. Both `400` (strict rejection of leading zeros) and `2xx` (accepting the valid grammar) are defensible, but rejection is preferred. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (RFC-valid but risky in proxy chains). - -## Why it matters - -While `00` happens to equal `0` in both decimal and octal, accepting leading zeros sets a precedent. If a server accepts `00`, it likely also accepts `010` (decimal 10 vs. octal 8) or `0200` (decimal 200 vs. octal 128). The safer behavior is to reject any Content-Length with leading zeros to eliminate the entire class of octal ambiguity attacks. - -## Deep Analysis - -### ABNF Analysis - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`). The value `00` consists of two ASCII digits, so it **does match** the grammar. It is syntactically valid per the ABNF. - -### RFC Evidence Chain - -**Step 1 -- The value is grammatically valid.** - -`00` satisfies `1*DIGIT` (two DIGITs). There is no RFC rule that prohibits leading zeros in Content-Length. The ABNF does not distinguish between `0`, `00`, and `000` -- all are sequences of one or more digits. - -**Step 2 -- No MUST-reject rule applies.** - -RFC 9112 §6.3 mandates rejection only for *invalid* Content-Length: - -> "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." - -Since `00` is valid per `1*DIGIT`, this MUST does not trigger. - -**Step 3 -- The forwarding rule still applies to intermediaries.** - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 - -Since `00` does match the ABNF, this rule does not prohibit forwarding it either. - -### Scored / SHOULD Justification - -This test is scored as **SHOULD** reject. While `00` is grammatically valid, rejecting it is the safer behavior because accepting leading zeros opens the door to octal interpretation ambiguity. The test awards Pass for 400 (strict) and Warn for 2xx (technically valid but risky). It is not scored as MUST because the RFC grammar explicitly permits it. - -### Real-World Smuggling Scenario - -The value `00` is a degenerate case where decimal (0) and octal (0) agree. However, accepting `00` reveals that the server's parser tolerates leading zeros. An attacker can escalate: if `00` is accepted, the server likely accepts `010` (decimal 10 vs. octal 8) or `0200` (decimal 200 vs. octal 128). In a proxy chain where the front-end interprets leading zeros as decimal and the back-end as octal, the body length disagreement enables request smuggling. Rejecting `00` eliminates the entire class of octal ambiguity attacks at the root. - -## Sources - -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL Double Zero — Request Smuggling" +description: "Content-Length with a double-zero value: Content-Length: 00. Tested against RFC 9110 §8.6." +weight: 48 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-DOUBLE-ZERO` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | SHOULD | +| **Expected** | `400` or `2xx` | + +## What it sends + +Content-Length with a double-zero value: `Content-Length: 00`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 00\r\n +\r\n +``` + + +## What the RFC says + +RFC 9110 §8.6 defines the Content-Length grammar: + +> "Content-Length = 1*DIGIT" + +The value `00` matches the `1*DIGIT` grammar (two digits), so it is technically valid per the RFC. However, leading zeros create ambiguity when parsers interpret them differently — particularly when some treat leading-zero values as octal notation. + +RFC 9112 §6.3 states that an invalid Content-Length is an unrecoverable error: + +> "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." + +Since `00` is grammatically valid, the MUST-reject rule does not apply here. However, rejecting it is the safer behavior. + +## Pass / Warn + +The value `00` matches the `1*DIGIT` grammar, so it is technically valid. The RFC does not mandate rejection of grammatically valid Content-Length values. Both `400` (strict rejection of leading zeros) and `2xx` (accepting the valid grammar) are defensible, but rejection is preferred. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (RFC-valid but risky in proxy chains). + +## Why it matters + +While `00` happens to equal `0` in both decimal and octal, accepting leading zeros sets a precedent. If a server accepts `00`, it likely also accepts `010` (decimal 10 vs. octal 8) or `0200` (decimal 200 vs. octal 128). The safer behavior is to reject any Content-Length with leading zeros to eliminate the entire class of octal ambiguity attacks. + +## Deep Analysis + +### ABNF Analysis + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`). The value `00` consists of two ASCII digits, so it **does match** the grammar. It is syntactically valid per the ABNF. + +### RFC Evidence Chain + +**Step 1 -- The value is grammatically valid.** + +`00` satisfies `1*DIGIT` (two DIGITs). There is no RFC rule that prohibits leading zeros in Content-Length. The ABNF does not distinguish between `0`, `00`, and `000` -- all are sequences of one or more digits. + +**Step 2 -- No MUST-reject rule applies.** + +RFC 9112 §6.3 mandates rejection only for *invalid* Content-Length: + +> "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." + +Since `00` is valid per `1*DIGIT`, this MUST does not trigger. + +**Step 3 -- The forwarding rule still applies to intermediaries.** + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 + +Since `00` does match the ABNF, this rule does not prohibit forwarding it either. + +### Scored / SHOULD Justification + +This test is scored as **SHOULD** reject. While `00` is grammatically valid, rejecting it is the safer behavior because accepting leading zeros opens the door to octal interpretation ambiguity. The test awards Pass for 400 (strict) and Warn for 2xx (technically valid but risky). It is not scored as MUST because the RFC grammar explicitly permits it. + +### Real-World Smuggling Scenario + +The value `00` is a degenerate case where decimal (0) and octal (0) agree. However, accepting `00` reveals that the server's parser tolerates leading zeros. An attacker can escalate: if `00` is accepted, the server likely accepts `010` (decimal 10 vs. octal 8) or `0200` (decimal 200 vs. octal 128). In a proxy chain where the front-end interprets leading zeros as decimal and the back-end as octal, the body length disagreement enables request smuggling. Rejecting `00` eliminates the entire class of octal ambiguity attacks at the root. + +## Sources + +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/cl-extra-leading-sp.md b/docs/content/docs/smuggling/cl-extra-leading-sp.md index f35afb0..19d767b 100644 --- a/docs/content/docs/smuggling/cl-extra-leading-sp.md +++ b/docs/content/docs/smuggling/cl-extra-leading-sp.md @@ -1,99 +1,99 @@ ---- -title: "CL-EXTRA-LEADING-SP" -description: "CL-EXTRA-LEADING-SP test documentation" -weight: 11 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-EXTRA-LEADING-SP` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | -| **Requirement** | SHOULD | -| **Expected** | `400` or `2xx` | - -## What it sends - -`Content-Length: 5` — extra space between colon and value. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -Note the double space after the colon (extra leading OWS). - -## What the RFC says - -RFC 9112 §5 defines the field-line syntax: - -> "field-line = field-name ":" OWS field-value OWS" - -The specification explicitly permits optional whitespace before the field value: - -> "A field line value might be preceded and/or followed by optional whitespace (OWS); a single SP preceding the field line value is preferred for consistent readability by humans. The field line value does not include that leading or trailing whitespace: OWS occurring before the first non-whitespace octet of the field line value, or after the last non-whitespace octet of the field line value, is excluded by parsers when extracting the field line value from a field line." - -The double space after the colon is valid OWS. The field value after OWS stripping is `5`, which is a valid `1*DIGIT` Content-Length. - -## Pass / Warn - -Leading OWS before the field value is explicitly permitted by RFC 9112 §5. Whether one or two spaces appear, the parser must strip them. Both `400` (strict) and `2xx` (standard OWS trimming) are acceptable behaviors, but rejection is preferred. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (RFC-valid OWS trimming). - -## Why it matters - -While OWS is permitted, some parsers may fail to strip it correctly, causing the Content-Length value to be parsed as ` 5` (with a leading space) rather than `5`. This could lead to parser disagreements in proxy chains. - -## Deep Analysis - -### ABNF Analysis - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -The raw field value before OWS stripping is ` 5` (with an extra leading space). However, RFC 9112 §5 defines the field-line syntax: - -> `field-line = field-name ":" OWS field-value OWS` - -The OWS (optional whitespace) before the field value is explicitly permitted and must be stripped by parsers: - -> "A field line value might be preceded and/or followed by optional whitespace (OWS); a single SP preceding the field line value is preferred for consistent readability by humans. The field line value does not include that leading or trailing whitespace: OWS occurring before the first non-whitespace octet of the field line value, or after the last non-whitespace octet of the field line value, is excluded by parsers when extracting the field line value from a field line." - -After stripping the leading OWS (both spaces), the extracted field value is `5`, which matches `1*DIGIT` and is valid. - -### RFC Evidence Chain - -**Step 1 -- OWS is explicitly permitted.** - -The double space between the colon and `5` is OWS. Whether one space or two, the RFC requires parsers to strip all leading whitespace before the first non-whitespace octet. The field value after extraction is `5`. - -**Step 2 -- The extracted value is valid.** - -After OWS stripping, `5` matches `1*DIGIT`. No invalid Content-Length rule is triggered. RFC 9112 §6.3 only applies to invalid Content-Length values: - -> "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." - -Since the extracted value `5` is valid, no MUST-reject obligation arises. - -**Step 3 -- Both responses are acceptable.** - -A server that correctly strips OWS and processes the request normally (2xx) is following RFC 9112 §5 precisely. A server that rejects with 400 is being stricter than required but not violating any RFC rule. - -### Scored / SHOULD Justification - -This test is scored as **SHOULD** reject. The extra leading space is explicitly allowed by RFC 9112 §5's OWS rule. Both `400` (strict rejection) and `2xx` (standard OWS trimming) are compliant behaviors, but rejection is the safer choice in proxy chains. - -### Real-World Smuggling Scenario - -If a front-end proxy fails to strip OWS correctly and passes the raw value ` 5` (with spaces) to its integer parser, the parse may fail or return 0. Meanwhile the back-end correctly strips OWS and reads 5 bytes of body. This disagreement on body length -- 0 vs. 5 -- means the front-end treats the body bytes as the start of the next request, enabling request smuggling. While the RFC is clear about OWS stripping, implementation bugs in this area are common. - -## Sources - -- [RFC 9112 §5](https://www.rfc-editor.org/rfc/rfc9112#section-5) -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +--- +title: "CL Extra Leading Space — Request Smuggling" +description: "Content-Length: 5 — extra space between colon and value. Tested against RFC 9112 §5." +weight: 11 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-EXTRA-LEADING-SP` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | +| **Requirement** | SHOULD | +| **Expected** | `400` or `2xx` | + +## What it sends + +`Content-Length: 5` — extra space between colon and value. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +Note the double space after the colon (extra leading OWS). + +## What the RFC says + +RFC 9112 §5 defines the field-line syntax: + +> "field-line = field-name ":" OWS field-value OWS" + +The specification explicitly permits optional whitespace before the field value: + +> "A field line value might be preceded and/or followed by optional whitespace (OWS); a single SP preceding the field line value is preferred for consistent readability by humans. The field line value does not include that leading or trailing whitespace: OWS occurring before the first non-whitespace octet of the field line value, or after the last non-whitespace octet of the field line value, is excluded by parsers when extracting the field line value from a field line." + +The double space after the colon is valid OWS. The field value after OWS stripping is `5`, which is a valid `1*DIGIT` Content-Length. + +## Pass / Warn + +Leading OWS before the field value is explicitly permitted by RFC 9112 §5. Whether one or two spaces appear, the parser must strip them. Both `400` (strict) and `2xx` (standard OWS trimming) are acceptable behaviors, but rejection is preferred. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (RFC-valid OWS trimming). + +## Why it matters + +While OWS is permitted, some parsers may fail to strip it correctly, causing the Content-Length value to be parsed as ` 5` (with a leading space) rather than `5`. This could lead to parser disagreements in proxy chains. + +## Deep Analysis + +### ABNF Analysis + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +The raw field value before OWS stripping is ` 5` (with an extra leading space). However, RFC 9112 §5 defines the field-line syntax: + +> `field-line = field-name ":" OWS field-value OWS` + +The OWS (optional whitespace) before the field value is explicitly permitted and must be stripped by parsers: + +> "A field line value might be preceded and/or followed by optional whitespace (OWS); a single SP preceding the field line value is preferred for consistent readability by humans. The field line value does not include that leading or trailing whitespace: OWS occurring before the first non-whitespace octet of the field line value, or after the last non-whitespace octet of the field line value, is excluded by parsers when extracting the field line value from a field line." + +After stripping the leading OWS (both spaces), the extracted field value is `5`, which matches `1*DIGIT` and is valid. + +### RFC Evidence Chain + +**Step 1 -- OWS is explicitly permitted.** + +The double space between the colon and `5` is OWS. Whether one space or two, the RFC requires parsers to strip all leading whitespace before the first non-whitespace octet. The field value after extraction is `5`. + +**Step 2 -- The extracted value is valid.** + +After OWS stripping, `5` matches `1*DIGIT`. No invalid Content-Length rule is triggered. RFC 9112 §6.3 only applies to invalid Content-Length values: + +> "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." + +Since the extracted value `5` is valid, no MUST-reject obligation arises. + +**Step 3 -- Both responses are acceptable.** + +A server that correctly strips OWS and processes the request normally (2xx) is following RFC 9112 §5 precisely. A server that rejects with 400 is being stricter than required but not violating any RFC rule. + +### Scored / SHOULD Justification + +This test is scored as **SHOULD** reject. The extra leading space is explicitly allowed by RFC 9112 §5's OWS rule. Both `400` (strict rejection) and `2xx` (standard OWS trimming) are compliant behaviors, but rejection is the safer choice in proxy chains. + +### Real-World Smuggling Scenario + +If a front-end proxy fails to strip OWS correctly and passes the raw value ` 5` (with spaces) to its integer parser, the parse may fail or return 0. Meanwhile the back-end correctly strips OWS and reads 5 bytes of body. This disagreement on body length -- 0 vs. 5 -- means the front-end treats the body bytes as the start of the next request, enabling request smuggling. While the RFC is clear about OWS stripping, implementation bugs in this area are common. + +## Sources + +- [RFC 9112 §5](https://www.rfc-editor.org/rfc/rfc9112#section-5) +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) diff --git a/docs/content/docs/smuggling/cl-hex-prefix.md b/docs/content/docs/smuggling/cl-hex-prefix.md index e70dfc1..4c1ddef 100644 --- a/docs/content/docs/smuggling/cl-hex-prefix.md +++ b/docs/content/docs/smuggling/cl-hex-prefix.md @@ -1,87 +1,87 @@ ---- -title: "CL-HEX-PREFIX" -description: "CL-HEX-PREFIX test documentation" -weight: 26 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-HEX-PREFIX` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -`Content-Length: 0x5` — CL with hex prefix. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 0x5\r\n -\r\n -hello -``` - - -## What the RFC says - -RFC 9110 §8.6 defines the Content-Length grammar: - -> "Content-Length = 1*DIGIT" - -The `1*DIGIT` grammar permits only ASCII digits 0-9. The value `0x5` contains `x`, which is not a digit, making this an invalid Content-Length. RFC 9110 §8.6 further requires: - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" - -RFC 9112 §6.3 mandates rejection: - -> "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." - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." - -## Why it matters - -If a server parses `0x5` as hexadecimal 5, it reads a different body length than a server that rejects it or truncates at the first non-digit. This parser disagreement is a smuggling vector. - -## Deep Analysis - -### ABNF Violation - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) exclusively. The value `0x5` contains the character `x`, which is not a DIGIT. Therefore `0x5` fails the `1*DIGIT` grammar and is unambiguously invalid. - -### RFC Evidence Chain - -**Step 1 -- The value is invalid per the grammar.** - -The character `x` (0x78) is not in the range `0`-`9` (0x30-0x39). The value `0x5` cannot be produced by `1*DIGIT`, regardless of how many digits surround the `x`. - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 - -**Step 2 -- The comma-separated list exception does not apply.** - -RFC 9112 §6.3 provides an exception only when the value "can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." The value `0x5` contains no commas, so it is a single-element list. That single element, `0x5`, is not valid `1*DIGIT`. The exception does not apply. - -**Step 3 -- The server must reject with 400.** - -> "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." -- RFC 9112 §6.3 - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 - -### Scored / Unscored Justification - -This test is **scored** (MUST reject). The value `0x5` is unambiguously invalid: `x` is not a DIGIT. The RFC chain from grammar violation through unrecoverable error to mandatory 400 is airtight. A `2xx` response is a compliance failure. - -### Real-World Smuggling Scenario - -C and C-derived languages (C++, Java, JavaScript) recognize `0x` as a hexadecimal prefix. If a server's parser calls a language-level integer function (e.g., `strtol` with base 0, or JavaScript's `parseInt`), `0x5` would be interpreted as hexadecimal 5 (decimal 5). A front-end that rejects or truncates at the `x` sees 0 bytes of body, while the back-end that parses hex sees 5 bytes. The 5 bytes the front-end considers the start of the next request are consumed as body by the back-end, desynchronizing the connection. - -## Sources - -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL Hex Prefix — Request Smuggling" +description: "Content-Length: 0x5 — CL with hex prefix. Tested against RFC 9110 Section 8.6." +weight: 26 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-HEX-PREFIX` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +`Content-Length: 0x5` — CL with hex prefix. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 0x5\r\n +\r\n +hello +``` + + +## What the RFC says + +RFC 9110 §8.6 defines the Content-Length grammar: + +> "Content-Length = 1*DIGIT" + +The `1*DIGIT` grammar permits only ASCII digits 0-9. The value `0x5` contains `x`, which is not a digit, making this an invalid Content-Length. RFC 9110 §8.6 further requires: + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" + +RFC 9112 §6.3 mandates rejection: + +> "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." + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." + +## Why it matters + +If a server parses `0x5` as hexadecimal 5, it reads a different body length than a server that rejects it or truncates at the first non-digit. This parser disagreement is a smuggling vector. + +## Deep Analysis + +### ABNF Violation + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) exclusively. The value `0x5` contains the character `x`, which is not a DIGIT. Therefore `0x5` fails the `1*DIGIT` grammar and is unambiguously invalid. + +### RFC Evidence Chain + +**Step 1 -- The value is invalid per the grammar.** + +The character `x` (0x78) is not in the range `0`-`9` (0x30-0x39). The value `0x5` cannot be produced by `1*DIGIT`, regardless of how many digits surround the `x`. + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 + +**Step 2 -- The comma-separated list exception does not apply.** + +RFC 9112 §6.3 provides an exception only when the value "can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." The value `0x5` contains no commas, so it is a single-element list. That single element, `0x5`, is not valid `1*DIGIT`. The exception does not apply. + +**Step 3 -- The server must reject with 400.** + +> "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." -- RFC 9112 §6.3 + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 + +### Scored / Unscored Justification + +This test is **scored** (MUST reject). The value `0x5` is unambiguously invalid: `x` is not a DIGIT. The RFC chain from grammar violation through unrecoverable error to mandatory 400 is airtight. A `2xx` response is a compliance failure. + +### Real-World Smuggling Scenario + +C and C-derived languages (C++, Java, JavaScript) recognize `0x` as a hexadecimal prefix. If a server's parser calls a language-level integer function (e.g., `strtol` with base 0, or JavaScript's `parseInt`), `0x5` would be interpreted as hexadecimal 5 (decimal 5). A front-end that rejects or truncates at the `x` sees 0 bytes of body, while the back-end that parses hex sees 5 bytes. The 5 bytes the front-end considers the start of the next request are consumed as body by the back-end, desynchronizing the connection. + +## Sources + +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/cl-internal-space.md b/docs/content/docs/smuggling/cl-internal-space.md index 006eb87..9cce5b7 100644 --- a/docs/content/docs/smuggling/cl-internal-space.md +++ b/docs/content/docs/smuggling/cl-internal-space.md @@ -1,91 +1,91 @@ ---- -title: "CL-INTERNAL-SPACE" -description: "CL-INTERNAL-SPACE test documentation" -weight: 27 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-INTERNAL-SPACE` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -`Content-Length: 1 0` — space inside the number. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 1 0\r\n -\r\n -hello12345 -``` - -The Content-Length value `1 0` has a space between the digits. - - -## What the RFC says - -RFC 9110 §8.6 defines the Content-Length grammar: - -> "Content-Length = 1*DIGIT" - -The `1*DIGIT` grammar permits only a contiguous sequence of ASCII digits 0-9. A space character is not a digit, so `1 0` does not match the grammar and is invalid. RFC 9110 §8.6 further requires: - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" - -RFC 9112 §6.3 mandates rejection: - -> "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." - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." - -## Why it matters - -A server that strips the internal space and interprets `1 0` as `10` reads 10 bytes of body. A server that reads only up to the first non-digit reads 1 byte. This disagreement on body boundaries is a smuggling vector. - -## Deep Analysis - -### ABNF Violation - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -The `1*DIGIT` production requires a contiguous sequence of one or more ASCII digits (`0`-`9`) with no intervening characters. The value `1 0` contains a space character (0x20) between the two digits. Space is not a DIGIT, so `1 0` does not match `1*DIGIT` and is invalid. - -Note that the space here is not leading or trailing OWS -- it is embedded *within* the field value, between two digits. RFC 9112 §5's OWS stripping only applies to whitespace before the first non-whitespace octet and after the last non-whitespace octet, not to whitespace in the middle. - -### RFC Evidence Chain - -**Step 1 -- The value is invalid per the grammar.** - -The space at position 2 breaks the contiguous digit sequence. `1*DIGIT` requires an unbroken run of digits; `1 0` is two separate digit groups separated by a non-DIGIT character. - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 - -**Step 2 -- The comma-separated list exception does not apply.** - -RFC 9112 §6.3 provides an exception for comma-separated lists where "all values in the list are valid, and all values in the list are the same." The value `1 0` contains no commas. As a single-element list, `1 0` itself must be valid `1*DIGIT` -- and it is not. The exception does not apply. - -**Step 3 -- The server must reject with 400.** - -> "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." -- RFC 9112 §6.3 - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 - -### Scored / Unscored Justification - -This test is **scored** (MUST reject). The space within the digit sequence makes the value unambiguously invalid. No exception applies. The RFC mandates 400 and connection close. A `2xx` response is a compliance failure. - -### Real-World Smuggling Scenario - -A server that strips internal whitespace would interpret `1 0` as `10` and read 10 bytes of body. A server that parses only up to the first non-DIGIT character would read `1` byte. This 9-byte disagreement is a smuggling vector: the first server consumes 10 bytes as body, while the second server consumes 1 byte as body and treats the remaining 9 bytes as the beginning of the next HTTP request. An attacker can embed a crafted request in those 9 bytes. - -## Sources - -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL Internal Space — Request Smuggling" +description: "Content-Length: 1 0 — space inside the number. Tested against RFC 9110 Section 8.6." +weight: 27 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-INTERNAL-SPACE` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +`Content-Length: 1 0` — space inside the number. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 1 0\r\n +\r\n +hello12345 +``` + +The Content-Length value `1 0` has a space between the digits. + + +## What the RFC says + +RFC 9110 §8.6 defines the Content-Length grammar: + +> "Content-Length = 1*DIGIT" + +The `1*DIGIT` grammar permits only a contiguous sequence of ASCII digits 0-9. A space character is not a digit, so `1 0` does not match the grammar and is invalid. RFC 9110 §8.6 further requires: + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" + +RFC 9112 §6.3 mandates rejection: + +> "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." + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." + +## Why it matters + +A server that strips the internal space and interprets `1 0` as `10` reads 10 bytes of body. A server that reads only up to the first non-digit reads 1 byte. This disagreement on body boundaries is a smuggling vector. + +## Deep Analysis + +### ABNF Violation + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +The `1*DIGIT` production requires a contiguous sequence of one or more ASCII digits (`0`-`9`) with no intervening characters. The value `1 0` contains a space character (0x20) between the two digits. Space is not a DIGIT, so `1 0` does not match `1*DIGIT` and is invalid. + +Note that the space here is not leading or trailing OWS -- it is embedded *within* the field value, between two digits. RFC 9112 §5's OWS stripping only applies to whitespace before the first non-whitespace octet and after the last non-whitespace octet, not to whitespace in the middle. + +### RFC Evidence Chain + +**Step 1 -- The value is invalid per the grammar.** + +The space at position 2 breaks the contiguous digit sequence. `1*DIGIT` requires an unbroken run of digits; `1 0` is two separate digit groups separated by a non-DIGIT character. + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 + +**Step 2 -- The comma-separated list exception does not apply.** + +RFC 9112 §6.3 provides an exception for comma-separated lists where "all values in the list are valid, and all values in the list are the same." The value `1 0` contains no commas. As a single-element list, `1 0` itself must be valid `1*DIGIT` -- and it is not. The exception does not apply. + +**Step 3 -- The server must reject with 400.** + +> "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." -- RFC 9112 §6.3 + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 + +### Scored / Unscored Justification + +This test is **scored** (MUST reject). The space within the digit sequence makes the value unambiguously invalid. No exception applies. The RFC mandates 400 and connection close. A `2xx` response is a compliance failure. + +### Real-World Smuggling Scenario + +A server that strips internal whitespace would interpret `1 0` as `10` and read 10 bytes of body. A server that parses only up to the first non-DIGIT character would read `1` byte. This 9-byte disagreement is a smuggling vector: the first server consumes 10 bytes as body, while the second server consumes 1 byte as body and treats the remaining 9 bytes as the beginning of the next HTTP request. An attacker can embed a crafted request in those 9 bytes. + +## Sources + +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/cl-leading-zeros-octal.md b/docs/content/docs/smuggling/cl-leading-zeros-octal.md index 198bdc2..74fe03b 100644 --- a/docs/content/docs/smuggling/cl-leading-zeros-octal.md +++ b/docs/content/docs/smuggling/cl-leading-zeros-octal.md @@ -1,109 +1,109 @@ ---- -title: "CL-LEADING-ZEROS-OCTAL" -description: "CL-LEADING-ZEROS-OCTAL test documentation" -weight: 49 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-LEADING-ZEROS-OCTAL` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **Requirement** | SHOULD | -| **Expected** | `400` or `2xx` | - -## What it sends - -Content-Length with a leading-zero value that differs between decimal and octal interpretation: `Content-Length: 0200` with 200 bytes of body (`A` repeated). - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 0200\r\n -\r\n -AAAAAAAAAA... (200 bytes) -``` - - -## What the RFC says - -RFC 9110 §8.6 defines the Content-Length grammar: - -> "Content-Length = 1*DIGIT" - -The value `0200` matches the `1*DIGIT` grammar (four ASCII digits), so it is technically valid. However, `0200` can be parsed as decimal 200 or octal 128 depending on the parser implementation. This is the critical ambiguity that leading zeros create. - -RFC 9112 §6.3 only mandates rejection for *invalid* Content-Length: - -> "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." - -Since `0200` is grammatically valid, the MUST-reject rule does not strictly apply. However, rejecting it is the safer behavior. - -## Pass / Warn - -The value `0200` matches the `1*DIGIT` grammar, so it is technically valid per the RFC. The RFC does not mandate rejection of grammatically valid Content-Length values with leading zeros. Both `400` (strict rejection) and `2xx` (accepting the valid grammar and parsing as decimal 200) are defensible. However, rejecting leading zeros is strongly recommended because of the octal ambiguity risk. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (RFC-valid but dangerous in proxy chains). - -## Why it matters - -This is a classic smuggling vector. If a front-end proxy reads `0200` as decimal 200, it forwards all 200 bytes as the body. If the back-end reads `0200` as octal 128, it only consumes 128 bytes — the remaining 72 bytes "spill" into the connection and are interpreted as the start of the next request. An attacker can craft those 72 bytes to be a complete malicious request, achieving request smuggling through parser disagreement on a single Content-Length value. - -## Deep Analysis - -### ABNF Analysis - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`). The value `0200` consists of four ASCII digits: `0`, `2`, `0`, `0`. All four are valid DIGITs, so `0200` **does match** the grammar. It is syntactically valid per the ABNF. - -### RFC Evidence Chain - -**Step 1 -- The value is grammatically valid.** - -`0200` satisfies `1*DIGIT` (four DIGITs). The ABNF does not distinguish between `200` and `0200`. There is no explicit RFC rule prohibiting leading zeros in Content-Length. - -**Step 2 -- No MUST-reject rule applies.** - -RFC 9112 §6.3 mandates rejection only for *invalid* Content-Length: - -> "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." - -Since `0200` is valid per `1*DIGIT`, the MUST-reject rule does not trigger. - -**Step 3 -- The forwarding rule does not prohibit it.** - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 - -Since `0200` matches the ABNF, intermediaries are not prohibited from forwarding it. - -### The Critical Ambiguity - -Unlike `005` (where decimal and octal agree), `0200` produces **different values** depending on interpretation: - -- **Decimal:** `0200` = 200 -- **Octal:** `0200` = 2 x 64 + 0 x 8 + 0 = 128 - -This 72-byte difference (200 - 128) is the smuggling payload window. - -### Scored / SHOULD Justification - -This test is scored as **SHOULD** reject (Pass for 400, Warn for 2xx). The value is grammatically valid, so no MUST-level obligation to reject exists. However, `0200` is the most dangerous leading-zero case because the decimal and octal interpretations diverge by 72 bytes -- enough to embed a complete smuggled HTTP request. The SHOULD scoring reflects the severe security risk despite the grammar being technically satisfied. - -### Real-World Smuggling Scenario - -This is a textbook CL-based smuggling attack. The request carries 200 bytes of body. A front-end proxy that interprets `0200` as decimal 200 forwards all 200 bytes. A back-end that interprets `0200` as octal 128 consumes only 128 bytes, leaving 72 bytes unconsumed on the connection. Those 72 bytes spill into the TCP stream and are parsed as the beginning of the next HTTP request. An attacker crafts those 72 bytes as: - -``` -GET /admin HTTP/1.1\r\nHost: internal\r\n\r\n -``` - -The back-end processes this as a legitimate request from the front-end's trusted connection, bypassing authentication and access controls. This is why leading zeros in Content-Length are dangerous even though the ABNF permits them. - -## Sources - -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL Leading Zeros Octal — Request Smuggling" +description: "Content-Length with a leading-zero value that differs between decimal and octal interpretation: Content-Length: 0200 with 200 bytes of body (A repeated). Tested against RFC 9110 §8.6." +weight: 49 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-LEADING-ZEROS-OCTAL` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | SHOULD | +| **Expected** | `400` or `2xx` | + +## What it sends + +Content-Length with a leading-zero value that differs between decimal and octal interpretation: `Content-Length: 0200` with 200 bytes of body (`A` repeated). + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 0200\r\n +\r\n +AAAAAAAAAA... (200 bytes) +``` + + +## What the RFC says + +RFC 9110 §8.6 defines the Content-Length grammar: + +> "Content-Length = 1*DIGIT" + +The value `0200` matches the `1*DIGIT` grammar (four ASCII digits), so it is technically valid. However, `0200` can be parsed as decimal 200 or octal 128 depending on the parser implementation. This is the critical ambiguity that leading zeros create. + +RFC 9112 §6.3 only mandates rejection for *invalid* Content-Length: + +> "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." + +Since `0200` is grammatically valid, the MUST-reject rule does not strictly apply. However, rejecting it is the safer behavior. + +## Pass / Warn + +The value `0200` matches the `1*DIGIT` grammar, so it is technically valid per the RFC. The RFC does not mandate rejection of grammatically valid Content-Length values with leading zeros. Both `400` (strict rejection) and `2xx` (accepting the valid grammar and parsing as decimal 200) are defensible. However, rejecting leading zeros is strongly recommended because of the octal ambiguity risk. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (RFC-valid but dangerous in proxy chains). + +## Why it matters + +This is a classic smuggling vector. If a front-end proxy reads `0200` as decimal 200, it forwards all 200 bytes as the body. If the back-end reads `0200` as octal 128, it only consumes 128 bytes — the remaining 72 bytes "spill" into the connection and are interpreted as the start of the next request. An attacker can craft those 72 bytes to be a complete malicious request, achieving request smuggling through parser disagreement on a single Content-Length value. + +## Deep Analysis + +### ABNF Analysis + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`). The value `0200` consists of four ASCII digits: `0`, `2`, `0`, `0`. All four are valid DIGITs, so `0200` **does match** the grammar. It is syntactically valid per the ABNF. + +### RFC Evidence Chain + +**Step 1 -- The value is grammatically valid.** + +`0200` satisfies `1*DIGIT` (four DIGITs). The ABNF does not distinguish between `200` and `0200`. There is no explicit RFC rule prohibiting leading zeros in Content-Length. + +**Step 2 -- No MUST-reject rule applies.** + +RFC 9112 §6.3 mandates rejection only for *invalid* Content-Length: + +> "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." + +Since `0200` is valid per `1*DIGIT`, the MUST-reject rule does not trigger. + +**Step 3 -- The forwarding rule does not prohibit it.** + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 + +Since `0200` matches the ABNF, intermediaries are not prohibited from forwarding it. + +### The Critical Ambiguity + +Unlike `005` (where decimal and octal agree), `0200` produces **different values** depending on interpretation: + +- **Decimal:** `0200` = 200 +- **Octal:** `0200` = 2 x 64 + 0 x 8 + 0 = 128 + +This 72-byte difference (200 - 128) is the smuggling payload window. + +### Scored / SHOULD Justification + +This test is scored as **SHOULD** reject (Pass for 400, Warn for 2xx). The value is grammatically valid, so no MUST-level obligation to reject exists. However, `0200` is the most dangerous leading-zero case because the decimal and octal interpretations diverge by 72 bytes -- enough to embed a complete smuggled HTTP request. The SHOULD scoring reflects the severe security risk despite the grammar being technically satisfied. + +### Real-World Smuggling Scenario + +This is a textbook CL-based smuggling attack. The request carries 200 bytes of body. A front-end proxy that interprets `0200` as decimal 200 forwards all 200 bytes. A back-end that interprets `0200` as octal 128 consumes only 128 bytes, leaving 72 bytes unconsumed on the connection. Those 72 bytes spill into the TCP stream and are parsed as the beginning of the next HTTP request. An attacker crafts those 72 bytes as: + +``` +GET /admin HTTP/1.1\r\nHost: internal\r\n\r\n +``` + +The back-end processes this as a legitimate request from the front-end's trusted connection, bypassing authentication and access controls. This is why leading zeros in Content-Length are dangerous even though the ABNF permits them. + +## Sources + +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/cl-leading-zeros.md b/docs/content/docs/smuggling/cl-leading-zeros.md index 7f0b66a..a225575 100644 --- a/docs/content/docs/smuggling/cl-leading-zeros.md +++ b/docs/content/docs/smuggling/cl-leading-zeros.md @@ -1,94 +1,94 @@ ---- -title: "CL-LEADING-ZEROS" -description: "CL-LEADING-ZEROS test documentation" -weight: 3 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-LEADING-ZEROS` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **Requirement** | SHOULD | -| **Expected** | `400` or `2xx` | - -## What it sends - -Content-Length with leading zeros: `Content-Length: 005`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 005\r\n -\r\n -hello -``` - - -## What the RFC says - -RFC 9110 §8.6 defines the Content-Length grammar: - -> "Content-Length = 1*DIGIT" - -Since `005` matches the `1*DIGIT` grammar (three ASCII digits), it is technically valid per the RFC. However, leading zeros create ambiguity — some parsers may interpret the value as octal (base-8), while others treat it as decimal. - -RFC 9112 §6.3 only mandates rejection for *invalid* Content-Length: - -> "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." - -Since `005` is grammatically valid, the MUST-reject rule does not strictly apply. However, rejecting it is the safer behavior. - -## Pass / Warn - -The value `005` matches the `1*DIGIT` grammar, so it is technically valid. The RFC does not mandate rejection of grammatically valid Content-Length values. Both `400` (strict rejection of leading zeros) and `2xx` (accepting the valid grammar and parsing as decimal 5) are defensible. - -## Why it matters - -This is a **security vs. strict RFC compliance** tension. The value `005` is grammatically valid, so a server that accepts it and parses it as decimal 5 is not violating the RFC. However, if a front-end and back-end disagree on whether `005` means 5 (decimal) or 5 (octal), they agree by coincidence. For values like `010` (decimal 10 vs. octal 8), disagreement causes body boundary misalignment — a smuggling vector. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (RFC-valid but potentially risky in proxy chains). - -## Deep Analysis - -### ABNF Analysis - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`). The value `005` consists of three ASCII digits: `0`, `0`, `5`. All three are valid DIGITs, so `005` **does match** the grammar. It is syntactically valid per the ABNF. - -### RFC Evidence Chain - -**Step 1 -- The value is grammatically valid.** - -`005` satisfies `1*DIGIT` (three DIGITs). The ABNF makes no distinction between `5`, `05`, and `005`. There is no rule in RFC 9110 or RFC 9112 that prohibits leading zeros in Content-Length values. - -**Step 2 -- No MUST-reject rule applies.** - -RFC 9112 §6.3 mandates rejection only for *invalid* Content-Length: - -> "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." - -Since `005` is valid per `1*DIGIT`, the MUST-reject rule does not trigger. - -**Step 3 -- The forwarding rule does not prohibit it.** - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 - -Since `005` matches the ABNF, intermediaries are not prohibited from forwarding it. - -### Scored / SHOULD Justification - -This test is scored as **SHOULD** reject (Pass for 400, Warn for 2xx). While `005` is grammatically valid and a server accepting it does not violate any MUST-level requirement, rejecting leading zeros is the safer behavior. The test cannot be scored as MUST because the ABNF explicitly permits it, but the security implications justify a SHOULD-level expectation. - -### Real-World Smuggling Scenario - -The value `005` happens to be unambiguous (decimal 5, octal 5) because all digits are below 8. However, accepting `005` reveals that the server's parser tolerates leading zeros, which means it likely also accepts `010`. In a proxy chain, if the front-end interprets `010` as decimal 10 and the back-end interprets it as octal 8, they disagree on the body length by 2 bytes. The front-end forwards 10 bytes, but the back-end only consumes 8 -- the remaining 2 bytes are treated as the start of the next request. An attacker uses `005` as a harmless probe to confirm leading-zero tolerance before escalating to `010` or `0200` for the actual attack. - -## Sources - -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL Leading Zeros — Request Smuggling" +description: "Content-Length with leading zeros: Content-Length: 005. Tested against RFC 9110 §8.6." +weight: 3 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-LEADING-ZEROS` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | SHOULD | +| **Expected** | `400` or `2xx` | + +## What it sends + +Content-Length with leading zeros: `Content-Length: 005`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 005\r\n +\r\n +hello +``` + + +## What the RFC says + +RFC 9110 §8.6 defines the Content-Length grammar: + +> "Content-Length = 1*DIGIT" + +Since `005` matches the `1*DIGIT` grammar (three ASCII digits), it is technically valid per the RFC. However, leading zeros create ambiguity — some parsers may interpret the value as octal (base-8), while others treat it as decimal. + +RFC 9112 §6.3 only mandates rejection for *invalid* Content-Length: + +> "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." + +Since `005` is grammatically valid, the MUST-reject rule does not strictly apply. However, rejecting it is the safer behavior. + +## Pass / Warn + +The value `005` matches the `1*DIGIT` grammar, so it is technically valid. The RFC does not mandate rejection of grammatically valid Content-Length values. Both `400` (strict rejection of leading zeros) and `2xx` (accepting the valid grammar and parsing as decimal 5) are defensible. + +## Why it matters + +This is a **security vs. strict RFC compliance** tension. The value `005` is grammatically valid, so a server that accepts it and parses it as decimal 5 is not violating the RFC. However, if a front-end and back-end disagree on whether `005` means 5 (decimal) or 5 (octal), they agree by coincidence. For values like `010` (decimal 10 vs. octal 8), disagreement causes body boundary misalignment — a smuggling vector. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (RFC-valid but potentially risky in proxy chains). + +## Deep Analysis + +### ABNF Analysis + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`). The value `005` consists of three ASCII digits: `0`, `0`, `5`. All three are valid DIGITs, so `005` **does match** the grammar. It is syntactically valid per the ABNF. + +### RFC Evidence Chain + +**Step 1 -- The value is grammatically valid.** + +`005` satisfies `1*DIGIT` (three DIGITs). The ABNF makes no distinction between `5`, `05`, and `005`. There is no rule in RFC 9110 or RFC 9112 that prohibits leading zeros in Content-Length values. + +**Step 2 -- No MUST-reject rule applies.** + +RFC 9112 §6.3 mandates rejection only for *invalid* Content-Length: + +> "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." + +Since `005` is valid per `1*DIGIT`, the MUST-reject rule does not trigger. + +**Step 3 -- The forwarding rule does not prohibit it.** + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 + +Since `005` matches the ABNF, intermediaries are not prohibited from forwarding it. + +### Scored / SHOULD Justification + +This test is scored as **SHOULD** reject (Pass for 400, Warn for 2xx). While `005` is grammatically valid and a server accepting it does not violate any MUST-level requirement, rejecting leading zeros is the safer behavior. The test cannot be scored as MUST because the ABNF explicitly permits it, but the security implications justify a SHOULD-level expectation. + +### Real-World Smuggling Scenario + +The value `005` happens to be unambiguous (decimal 5, octal 5) because all digits are below 8. However, accepting `005` reveals that the server's parser tolerates leading zeros, which means it likely also accepts `010`. In a proxy chain, if the front-end interprets `010` as decimal 10 and the back-end interprets it as octal 8, they disagree on the body length by 2 bytes. The front-end forwards 10 bytes, but the back-end only consumes 8 -- the remaining 2 bytes are treated as the start of the next request. An attacker uses `005` as a harmless probe to confirm leading-zero tolerance before escalating to `010` or `0200` for the actual attack. + +## Sources + +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/cl-negative-zero.md b/docs/content/docs/smuggling/cl-negative-zero.md index 7af52d5..a4eae4d 100644 --- a/docs/content/docs/smuggling/cl-negative-zero.md +++ b/docs/content/docs/smuggling/cl-negative-zero.md @@ -1,86 +1,86 @@ ---- -title: "CL-NEGATIVE-ZERO" -description: "CL-NEGATIVE-ZERO test documentation" -weight: 47 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-NEGATIVE-ZERO` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -Content-Length with a negative zero value: `Content-Length: -0`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: -0\r\n -\r\n -``` - - -## What the RFC says - -RFC 9110 §8.6 defines the Content-Length grammar: - -> "Content-Length = 1*DIGIT" - -The `1*DIGIT` grammar means only one or more ASCII digits (0-9) are permitted. The minus sign (`-`) is not a digit, so `-0` is invalid regardless of the fact that -0 equals 0 mathematically. RFC 9110 §8.6 further requires: - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" - -RFC 9112 §6.3 mandates rejection: - -> "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." - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." - -## Why it matters - -Some parsers apply numeric conversion first and check validity second. If a parser converts `-0` to the integer `0` and accepts it, it silently consumes an invalid format. A stricter front-end might reject the request or see no body at all, while a lenient back-end accepts it — creating framing disagreement. The `-` character is especially dangerous because it could allow negative body lengths through similar parser shortcuts. - -## Deep Analysis - -### ABNF Violation - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) exclusively. The value `-0` begins with a minus sign (`-`, 0x2D), which is not a DIGIT. Therefore `-0` fails the `1*DIGIT` grammar at the very first character. The fact that `-0` equals `0` mathematically is irrelevant -- the ABNF is a syntactic rule, not a semantic one. - -### RFC Evidence Chain - -**Step 1 -- The value is invalid per the grammar.** - -The minus sign fails the DIGIT check regardless of what follows it. `-0` is syntactically identical to `-1` or `-999` from the grammar's perspective: all begin with a non-DIGIT character. - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 - -**Step 2 -- The comma-separated list exception does not apply.** - -RFC 9112 §6.3 provides an exception only when the value "can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." The value `-0` has no commas. As a single-element list, `-0` must be valid `1*DIGIT` -- and it is not. The exception does not apply. - -**Step 3 -- The server must reject with 400.** - -> "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." -- RFC 9112 §6.3 - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 - -### Scored / Unscored Justification - -This test is **scored** (MUST reject). The minus sign is not a DIGIT, making `-0` unambiguously invalid. No mathematical equivalence to `0` changes the syntactic analysis. The RFC mandates 400 and connection close. A `2xx` response is a compliance failure. - -### Real-World Smuggling Scenario - -The danger of `-0` is parser shortcutting. Some parsers apply numeric conversion first (`atoi("-0")` returns `0`) and then check if the result is non-negative. Since `0` passes the non-negative check, the parser accepts the value without ever validating the syntax. This creates a differential: a strict front-end rejects the request (or treats it as having no body), while a lenient back-end accepts `Content-Length: 0` and reads no body. If the front-end rejects but the connection is reused (a misconfiguration), the back-end may process subsequent bytes on the connection as a new request. More importantly, accepting `-0` signals that the parser tolerates the `-` character, meaning `-1` or other negative values may also slip through to trigger integer underflow attacks. - -## Sources - -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL Negative Zero — Request Smuggling" +description: "Content-Length with a negative zero value: Content-Length: -0. Tested against RFC 9110 §8.6." +weight: 47 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-NEGATIVE-ZERO` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +Content-Length with a negative zero value: `Content-Length: -0`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: -0\r\n +\r\n +``` + + +## What the RFC says + +RFC 9110 §8.6 defines the Content-Length grammar: + +> "Content-Length = 1*DIGIT" + +The `1*DIGIT` grammar means only one or more ASCII digits (0-9) are permitted. The minus sign (`-`) is not a digit, so `-0` is invalid regardless of the fact that -0 equals 0 mathematically. RFC 9110 §8.6 further requires: + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" + +RFC 9112 §6.3 mandates rejection: + +> "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." + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." + +## Why it matters + +Some parsers apply numeric conversion first and check validity second. If a parser converts `-0` to the integer `0` and accepts it, it silently consumes an invalid format. A stricter front-end might reject the request or see no body at all, while a lenient back-end accepts it — creating framing disagreement. The `-` character is especially dangerous because it could allow negative body lengths through similar parser shortcuts. + +## Deep Analysis + +### ABNF Violation + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) exclusively. The value `-0` begins with a minus sign (`-`, 0x2D), which is not a DIGIT. Therefore `-0` fails the `1*DIGIT` grammar at the very first character. The fact that `-0` equals `0` mathematically is irrelevant -- the ABNF is a syntactic rule, not a semantic one. + +### RFC Evidence Chain + +**Step 1 -- The value is invalid per the grammar.** + +The minus sign fails the DIGIT check regardless of what follows it. `-0` is syntactically identical to `-1` or `-999` from the grammar's perspective: all begin with a non-DIGIT character. + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 + +**Step 2 -- The comma-separated list exception does not apply.** + +RFC 9112 §6.3 provides an exception only when the value "can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." The value `-0` has no commas. As a single-element list, `-0` must be valid `1*DIGIT` -- and it is not. The exception does not apply. + +**Step 3 -- The server must reject with 400.** + +> "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." -- RFC 9112 §6.3 + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 + +### Scored / Unscored Justification + +This test is **scored** (MUST reject). The minus sign is not a DIGIT, making `-0` unambiguously invalid. No mathematical equivalence to `0` changes the syntactic analysis. The RFC mandates 400 and connection close. A `2xx` response is a compliance failure. + +### Real-World Smuggling Scenario + +The danger of `-0` is parser shortcutting. Some parsers apply numeric conversion first (`atoi("-0")` returns `0`) and then check if the result is non-negative. Since `0` passes the non-negative check, the parser accepts the value without ever validating the syntax. This creates a differential: a strict front-end rejects the request (or treats it as having no body), while a lenient back-end accepts `Content-Length: 0` and reads no body. If the front-end rejects but the connection is reused (a misconfiguration), the back-end may process subsequent bytes on the connection as a new request. More importantly, accepting `-0` signals that the parser tolerates the `-` character, meaning `-1` or other negative values may also slip through to trigger integer underflow attacks. + +## Sources + +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/cl-negative.md b/docs/content/docs/smuggling/cl-negative.md index e477c8a..5ac5600 100644 --- a/docs/content/docs/smuggling/cl-negative.md +++ b/docs/content/docs/smuggling/cl-negative.md @@ -1,86 +1,86 @@ ---- -title: "CL-NEGATIVE" -description: "CL-NEGATIVE test documentation" -weight: 4 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-NEGATIVE` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -Negative Content-Length: `Content-Length: -1`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: -1\r\n -\r\n -``` - - -## What the RFC says - -RFC 9110 §8.6 defines the Content-Length grammar: - -> "Content-Length = 1*DIGIT" - -The `1*DIGIT` grammar permits only ASCII digits 0-9. The minus sign (`-`) is not a digit, so `-1` does not match the grammar and is invalid. RFC 9110 §8.6 further requires: - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" - -RFC 9112 §6.3 mandates rejection: - -> "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." - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." - -## Why it matters - -A negative Content-Length could cause integer underflow in parsers that convert the value to a signed integer before validation. If a server interprets `-1` as a very large unsigned value (e.g., 4294967295 on 32-bit), it could read far beyond the intended body — a severe security vulnerability. - -## Deep Analysis - -### ABNF Violation - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) exclusively. The value `-1` begins with a minus sign (`-`, 0x2D), which is not a DIGIT (0x30-0x39). Therefore `-1` fails the `1*DIGIT` grammar at the very first character and is unambiguously invalid. - -### RFC Evidence Chain - -**Step 1 -- The value is invalid per the grammar.** - -The minus sign is not in the DIGIT range. The ABNF `1*DIGIT` requires the first character to be a digit; `-` immediately disqualifies the value. - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 - -**Step 2 -- The comma-separated list exception does not apply.** - -RFC 9112 §6.3 provides an exception only when the value "can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." The value `-1` has no commas. As a single-element list, `-1` must be valid `1*DIGIT` -- and it is not. The exception does not apply. - -**Step 3 -- The server must reject with 400.** - -> "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." -- RFC 9112 §6.3 - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 - -### Scored / Unscored Justification - -This test is **scored** (MUST reject). The minus sign is not a DIGIT, so the value is invalid with no exception. The RFC chain from grammar violation to unrecoverable error to mandatory 400 is unambiguous. A `2xx` response is a compliance failure. - -### Real-World Smuggling Scenario - -A negative Content-Length is one of the most dangerous malformed values because of how programming languages handle signed-to-unsigned integer conversion. If a server parses `-1` as a signed 32-bit integer and then casts it to an unsigned type, it becomes `4294967295` (2^32 - 1). The server would attempt to read ~4 GB of body data from the connection, consuming not just the current request's body but potentially hundreds of subsequent requests from other clients on a shared connection. Even on 64-bit systems, `-1` as unsigned is `18446744073709551615`. Beyond smuggling, this is a denial-of-service vector: the server hangs waiting for billions of bytes that will never arrive, tying up the connection indefinitely. - -## Sources - -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL Negative — Request Smuggling" +description: "Negative Content-Length: Content-Length: -1. Tested against RFC 9110 Section 8.6." +weight: 4 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-NEGATIVE` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +Negative Content-Length: `Content-Length: -1`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: -1\r\n +\r\n +``` + + +## What the RFC says + +RFC 9110 §8.6 defines the Content-Length grammar: + +> "Content-Length = 1*DIGIT" + +The `1*DIGIT` grammar permits only ASCII digits 0-9. The minus sign (`-`) is not a digit, so `-1` does not match the grammar and is invalid. RFC 9110 §8.6 further requires: + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" + +RFC 9112 §6.3 mandates rejection: + +> "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." + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." + +## Why it matters + +A negative Content-Length could cause integer underflow in parsers that convert the value to a signed integer before validation. If a server interprets `-1` as a very large unsigned value (e.g., 4294967295 on 32-bit), it could read far beyond the intended body — a severe security vulnerability. + +## Deep Analysis + +### ABNF Violation + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) exclusively. The value `-1` begins with a minus sign (`-`, 0x2D), which is not a DIGIT (0x30-0x39). Therefore `-1` fails the `1*DIGIT` grammar at the very first character and is unambiguously invalid. + +### RFC Evidence Chain + +**Step 1 -- The value is invalid per the grammar.** + +The minus sign is not in the DIGIT range. The ABNF `1*DIGIT` requires the first character to be a digit; `-` immediately disqualifies the value. + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 + +**Step 2 -- The comma-separated list exception does not apply.** + +RFC 9112 §6.3 provides an exception only when the value "can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." The value `-1` has no commas. As a single-element list, `-1` must be valid `1*DIGIT` -- and it is not. The exception does not apply. + +**Step 3 -- The server must reject with 400.** + +> "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." -- RFC 9112 §6.3 + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 + +### Scored / Unscored Justification + +This test is **scored** (MUST reject). The minus sign is not a DIGIT, so the value is invalid with no exception. The RFC chain from grammar violation to unrecoverable error to mandatory 400 is unambiguous. A `2xx` response is a compliance failure. + +### Real-World Smuggling Scenario + +A negative Content-Length is one of the most dangerous malformed values because of how programming languages handle signed-to-unsigned integer conversion. If a server parses `-1` as a signed 32-bit integer and then casts it to an unsigned type, it becomes `4294967295` (2^32 - 1). The server would attempt to read ~4 GB of body data from the connection, consuming not just the current request's body but potentially hundreds of subsequent requests from other clients on a shared connection. Even on 64-bit systems, `-1` as unsigned is `18446744073709551615`. Beyond smuggling, this is a denial-of-service vector: the server hangs waiting for billions of bytes that will never arrive, tying up the connection indefinitely. + +## Sources + +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/cl-octal.md b/docs/content/docs/smuggling/cl-octal.md index afaa322..862a348 100644 --- a/docs/content/docs/smuggling/cl-octal.md +++ b/docs/content/docs/smuggling/cl-octal.md @@ -1,87 +1,87 @@ ---- -title: "CL-OCTAL" -description: "CL-OCTAL test documentation" -weight: 20 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-OCTAL` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -`Content-Length: 0o5` — CL with octal prefix. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 0o5\r\n -\r\n -hello -``` - - -## What the RFC says - -RFC 9110 §8.6 defines the Content-Length grammar: - -> "Content-Length = 1*DIGIT" - -The `1*DIGIT` grammar permits only ASCII digits 0-9. The value `0o5` contains `o`, which is not a digit, making this an invalid Content-Length. RFC 9110 §8.6 further requires: - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" - -RFC 9112 §6.3 mandates rejection: - -> "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." - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." - -## Why it matters - -Some programming languages (Python, Rust, Ruby) parse `0o5` as an octal literal for the value 5. If a server uses a language-level parser that accepts this notation, it would read a body of 5 bytes. A stricter front-end would reject the request, creating a parser differential that enables smuggling. - -## Deep Analysis - -### ABNF Violation - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) exclusively. The value `0o5` contains the character `o` (0x6F), which is not a DIGIT (0x30-0x39). Therefore `0o5` fails the `1*DIGIT` grammar and is unambiguously invalid. - -### RFC Evidence Chain - -**Step 1 -- The value is invalid per the grammar.** - -The character `o` at position 2 breaks the digit-only requirement. Even though `0` and `5` are digits, the intervening `o` makes the complete value non-conformant. - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 - -**Step 2 -- The comma-separated list exception does not apply.** - -RFC 9112 §6.3 provides an exception only when the value "can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." The value `0o5` contains no commas. As a single-element list, `0o5` must be valid `1*DIGIT` -- and it is not. The exception does not apply. - -**Step 3 -- The server must reject with 400.** - -> "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." -- RFC 9112 §6.3 - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 - -### Scored / Unscored Justification - -This test is **scored** (MUST reject). The `o` character is not a DIGIT, making the value unambiguously invalid. No exception applies. The RFC mandates 400 and connection close. A `2xx` response is a compliance failure. - -### Real-World Smuggling Scenario - -The `0o` prefix is the standard octal literal notation in Python 3 (`0o5` = 5), Rust (`0o5` = 5), Ruby (`0o5` = 5), and ECMAScript 2015+ (`0o5` = 5). If a server's Content-Length parser delegates to a language-level integer parser that accepts `0o` notation, `0o5` would be interpreted as the integer 5, and the server would read 5 bytes of body. A front-end that correctly rejects this value (or a parser that stops at the `o` and reads 0 bytes) would disagree on the body boundary. The attacker's 5 body bytes would spill forward as the start of the next request. This is particularly dangerous with values like `0o12` (octal 10) vs. a truncation-to-`0` parser, creating a 10-byte smuggling window. - -## Sources - -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL Octal — Request Smuggling" +description: "Content-Length: 0o5 — CL with octal prefix. Tested against RFC 9110 Section 8.6." +weight: 20 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-OCTAL` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +`Content-Length: 0o5` — CL with octal prefix. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 0o5\r\n +\r\n +hello +``` + + +## What the RFC says + +RFC 9110 §8.6 defines the Content-Length grammar: + +> "Content-Length = 1*DIGIT" + +The `1*DIGIT` grammar permits only ASCII digits 0-9. The value `0o5` contains `o`, which is not a digit, making this an invalid Content-Length. RFC 9110 §8.6 further requires: + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" + +RFC 9112 §6.3 mandates rejection: + +> "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." + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." + +## Why it matters + +Some programming languages (Python, Rust, Ruby) parse `0o5` as an octal literal for the value 5. If a server uses a language-level parser that accepts this notation, it would read a body of 5 bytes. A stricter front-end would reject the request, creating a parser differential that enables smuggling. + +## Deep Analysis + +### ABNF Violation + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) exclusively. The value `0o5` contains the character `o` (0x6F), which is not a DIGIT (0x30-0x39). Therefore `0o5` fails the `1*DIGIT` grammar and is unambiguously invalid. + +### RFC Evidence Chain + +**Step 1 -- The value is invalid per the grammar.** + +The character `o` at position 2 breaks the digit-only requirement. Even though `0` and `5` are digits, the intervening `o` makes the complete value non-conformant. + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 + +**Step 2 -- The comma-separated list exception does not apply.** + +RFC 9112 §6.3 provides an exception only when the value "can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." The value `0o5` contains no commas. As a single-element list, `0o5` must be valid `1*DIGIT` -- and it is not. The exception does not apply. + +**Step 3 -- The server must reject with 400.** + +> "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." -- RFC 9112 §6.3 + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 + +### Scored / Unscored Justification + +This test is **scored** (MUST reject). The `o` character is not a DIGIT, making the value unambiguously invalid. No exception applies. The RFC mandates 400 and connection close. A `2xx` response is a compliance failure. + +### Real-World Smuggling Scenario + +The `0o` prefix is the standard octal literal notation in Python 3 (`0o5` = 5), Rust (`0o5` = 5), Ruby (`0o5` = 5), and ECMAScript 2015+ (`0o5` = 5). If a server's Content-Length parser delegates to a language-level integer parser that accepts `0o` notation, `0o5` would be interpreted as the integer 5, and the server would read 5 bytes of body. A front-end that correctly rejects this value (or a parser that stops at the `o` and reads 0 bytes) would disagree on the body boundary. The attacker's 5 body bytes would spill forward as the start of the next request. This is particularly dangerous with values like `0o12` (octal 10) vs. a truncation-to-`0` parser, creating a 10-byte smuggling window. + +## Sources + +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/cl-te-both.md b/docs/content/docs/smuggling/cl-te-both.md index bb709c3..d8e97ae 100644 --- a/docs/content/docs/smuggling/cl-te-both.md +++ b/docs/content/docs/smuggling/cl-te-both.md @@ -1,93 +1,93 @@ ---- -title: "CL-TE-BOTH" -description: "CL-TE-BOTH test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-TE-BOTH` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **Requirement** | "ought to" handle as error | -| **Expected** | `400` or `2xx` | - -## What it sends - -A request with both `Content-Length` and `Transfer-Encoding` headers present. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 23\r\n -Transfer-Encoding: chunked\r\n -\r\n -D\r\n -hello-bananas\r\n -0\r\n -\r\n -``` - - -## What the RFC says - -RFC 9112 §6.3 states: - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling (Section 11.2) or response splitting (Section 11.1) and ought to be handled as an error." - -RFC 9112 §6.1 provides the server's options: - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." - -The "ought to" language is between SHOULD and MAY. A server MAY reject the message or process it using Transfer-Encoding alone -- both are RFC-compliant. However, the server MUST close the connection afterward. - -## Pass / Warn - -The RFC uses "ought to" language (between SHOULD and MAY) for handling this as an error, and explicitly allows the server to either reject or process with TE alone. Both `400` and `2xx` are RFC-compliant, but rejection is the safer choice. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (RFC-valid, using TE to determine body length). - -## Why it matters - -This is **the** classic request smuggling setup. If the front-end uses Content-Length and the back-end uses Transfer-Encoding (or vice versa), they disagree on body boundaries. - -## Deep Analysis - -### ABNF Analysis - -This test is not about an ABNF grammar violation. Both `Content-Length: 23` and `Transfer-Encoding: chunked` are individually valid headers. The issue is their simultaneous presence in the same message, which the RFC treats as a conflicting-framing condition. - -### RFC Evidence Chain - -**Step 1 -- The sender is prohibited from including both.** - -> "A sender MUST NOT send a Content-Length header field in any message that contains a Transfer-Encoding header field." -- RFC 9112 §6.2 - -The request violates this MUST NOT at the sender level. When a server receives such a message, it knows the sender has already violated the protocol. - -**Step 2 -- The RFC flags this as a potential attack.** - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling (Section 11.2) or response splitting (Section 11.1) and ought to be handled as an error." -- RFC 9112 §6.3 - -The "ought to" language recommends treating this as an error but does not mandate it. - -**Step 3 -- The server has explicit discretion.** - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 - -The server has two compliant options: reject (400) or process using Transfer-Encoding only. In either case, it MUST close the connection afterward. - -### Scored / Unscored Justification - -This test is scored as **"ought to" handle as error** (Pass for 400, Warn for 2xx). The RFC uses "ought to" language -- stronger than MAY but weaker than MUST. The explicit MAY in RFC 9112 §6.1 permits both behaviors. However, the mandatory connection close after any response means a 2xx without connection close would be a separate violation. - -### Real-World Smuggling Scenario - -This is the classic CL.TE / TE.CL smuggling setup described by Watchfire (2005) and popularized by PortSwigger (2019). If different components disagree on whether to use `Content-Length` or `Transfer-Encoding`, they can disagree on request body boundaries or request body content. In real-world smuggling exploits, the attacker typically chooses a conflicting `Content-Length` such that one parser stops early and the remaining bytes are interpreted as the start of the next request on a persistent connection. This test uses a non-empty chunked body (`hello-bananas`) to make it obvious whether an echo-capable server decoded chunked framing (TE) or treated the raw chunked bytes as the body (CL). - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [RFC 9112 §6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL TE Both — Request Smuggling" +description: "A request with both Content-Length and Transfer-Encoding headers present. Tested against RFC 9112 §6.1." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-TE-BOTH` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **Requirement** | "ought to" handle as error | +| **Expected** | `400` or `2xx` | + +## What it sends + +A request with both `Content-Length` and `Transfer-Encoding` headers present. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 23\r\n +Transfer-Encoding: chunked\r\n +\r\n +D\r\n +hello-bananas\r\n +0\r\n +\r\n +``` + + +## What the RFC says + +RFC 9112 §6.3 states: + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling (Section 11.2) or response splitting (Section 11.1) and ought to be handled as an error." + +RFC 9112 §6.1 provides the server's options: + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." + +The "ought to" language is between SHOULD and MAY. A server MAY reject the message or process it using Transfer-Encoding alone -- both are RFC-compliant. However, the server MUST close the connection afterward. + +## Pass / Warn + +The RFC uses "ought to" language (between SHOULD and MAY) for handling this as an error, and explicitly allows the server to either reject or process with TE alone. Both `400` and `2xx` are RFC-compliant, but rejection is the safer choice. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (RFC-valid, using TE to determine body length). + +## Why it matters + +This is **the** classic request smuggling setup. If the front-end uses Content-Length and the back-end uses Transfer-Encoding (or vice versa), they disagree on body boundaries. + +## Deep Analysis + +### ABNF Analysis + +This test is not about an ABNF grammar violation. Both `Content-Length: 23` and `Transfer-Encoding: chunked` are individually valid headers. The issue is their simultaneous presence in the same message, which the RFC treats as a conflicting-framing condition. + +### RFC Evidence Chain + +**Step 1 -- The sender is prohibited from including both.** + +> "A sender MUST NOT send a Content-Length header field in any message that contains a Transfer-Encoding header field." -- RFC 9112 §6.2 + +The request violates this MUST NOT at the sender level. When a server receives such a message, it knows the sender has already violated the protocol. + +**Step 2 -- The RFC flags this as a potential attack.** + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling (Section 11.2) or response splitting (Section 11.1) and ought to be handled as an error." -- RFC 9112 §6.3 + +The "ought to" language recommends treating this as an error but does not mandate it. + +**Step 3 -- The server has explicit discretion.** + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 + +The server has two compliant options: reject (400) or process using Transfer-Encoding only. In either case, it MUST close the connection afterward. + +### Scored / Unscored Justification + +This test is scored as **"ought to" handle as error** (Pass for 400, Warn for 2xx). The RFC uses "ought to" language -- stronger than MAY but weaker than MUST. The explicit MAY in RFC 9112 §6.1 permits both behaviors. However, the mandatory connection close after any response means a 2xx without connection close would be a separate violation. + +### Real-World Smuggling Scenario + +This is the classic CL.TE / TE.CL smuggling setup described by Watchfire (2005) and popularized by PortSwigger (2019). If different components disagree on whether to use `Content-Length` or `Transfer-Encoding`, they can disagree on request body boundaries or request body content. In real-world smuggling exploits, the attacker typically chooses a conflicting `Content-Length` such that one parser stops early and the remaining bytes are interpreted as the start of the next request on a persistent connection. This test uses a non-empty chunked body (`hello-bananas`) to make it obvious whether an echo-capable server decoded chunked framing (TE) or treated the raw chunked bytes as the body (CL). + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [RFC 9112 §6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/cl-trailing-space.md b/docs/content/docs/smuggling/cl-trailing-space.md index 650d432..99977a4 100644 --- a/docs/content/docs/smuggling/cl-trailing-space.md +++ b/docs/content/docs/smuggling/cl-trailing-space.md @@ -1,96 +1,96 @@ ---- -title: "CL-TRAILING-SPACE" -description: "CL-TRAILING-SPACE test documentation" -weight: 10 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-TRAILING-SPACE` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | -| **Requirement** | SHOULD | -| **Expected** | `400` or `2xx` | - -## What it sends - -`Content-Length: 5 ` — trailing space after the value. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5 \r\n -\r\n -hello -``` - -Note the trailing space after `5` in the Content-Length value. - -## What the RFC says - -RFC 9112 §5 defines the field-line syntax: - -> "field-line = field-name ":" OWS field-value OWS" - -The specification explicitly requires parsers to strip trailing whitespace: - -> "A field line value might be preceded and/or followed by optional whitespace (OWS); a single SP preceding the field line value is preferred for consistent readability by humans. The field line value does not include that leading or trailing whitespace: OWS occurring before the first non-whitespace octet of the field line value, or after the last non-whitespace octet of the field line value, is excluded by parsers when extracting the field line value from a field line." - -After OWS stripping, the remaining value `5` is a valid `1*DIGIT` Content-Length. - -## Pass / Warn - -OWS (optional whitespace) after the field value is explicitly permitted by RFC 9112 §5. Trimming it and processing normally is valid behavior. However, `400` is the stricter/safer choice. Both responses are RFC-compliant, but rejection is preferred. - -## Why it matters - -While trailing OWS is permitted, some parsers may include the trailing space in the field value, causing the Content-Length to be parsed as `5 ` rather than `5`. This could lead to parser disagreements or numeric conversion failures in proxy chains. - -## Deep Analysis - -### ABNF Analysis - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -The raw field-line value (before OWS processing) is `5 ` (digit followed by a space). However, RFC 9112 §5 defines the field-line syntax: - -> `field-line = field-name ":" OWS field-value OWS` - -The trailing OWS after the field value is explicitly permitted and must be stripped: - -> "A field line value might be preceded and/or followed by optional whitespace (OWS); a single SP preceding the field line value is preferred for consistent readability by humans. The field line value does not include that leading or trailing whitespace: OWS occurring before the first non-whitespace octet of the field line value, or after the last non-whitespace octet of the field line value, is excluded by parsers when extracting the field line value from a field line." - -After stripping the trailing OWS, the extracted field value is `5`, which matches `1*DIGIT` and is valid. - -### RFC Evidence Chain - -**Step 1 -- Trailing OWS is explicitly permitted.** - -The space after `5` falls after the last non-whitespace octet of the field value. Per RFC 9112 §5, parsers must exclude it. The resulting field value is `5`. - -**Step 2 -- The extracted value is valid.** - -After OWS stripping, `5` matches `1*DIGIT`. No invalid Content-Length rule is triggered. RFC 9112 §6.3 only applies to invalid Content-Length values: - -> "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." - -Since the extracted value `5` is valid, no MUST-reject obligation arises. - -**Step 3 -- Both responses are acceptable.** - -A server that correctly strips trailing OWS and processes the request normally (2xx) is following RFC 9112 §5 precisely. A server that rejects with 400 is being stricter than required but not violating any rule. - -### Scored / SHOULD Justification - -This test is scored as **SHOULD** reject. Trailing OWS is explicitly allowed by RFC 9112 §5. Both `400` (strict rejection) and `2xx` (standard OWS trimming) are compliant behaviors, but rejection is the safer choice in proxy chains where OWS handling inconsistencies may arise. - -### Real-World Smuggling Scenario - -If a front-end proxy includes the trailing space in the field value and passes `5 ` (with space) to its integer parser, the behavior depends on the parser: some return 5 (ignoring trailing non-digits), some return 0 (parse failure), and some throw an error. Meanwhile the back-end correctly strips OWS and reads 5 bytes of body. A parser that returns 0 reads no body, causing the 5 body bytes to be interpreted as the next request. A parser that throws an error might close the connection or forward unpredictably. The trailing space is a subtle probe for parser differential behavior. - -## Sources - -- [RFC 9112 §5](https://www.rfc-editor.org/rfc/rfc9112#section-5) -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +--- +title: "CL Trailing Space — Request Smuggling" +description: "Content-Length: 5 — trailing space after the value. Tested against RFC 9112 §5." +weight: 10 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-TRAILING-SPACE` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §5](https://www.rfc-editor.org/rfc/rfc9112#section-5) | +| **Requirement** | SHOULD | +| **Expected** | `400` or `2xx` | + +## What it sends + +`Content-Length: 5 ` — trailing space after the value. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5 \r\n +\r\n +hello +``` + +Note the trailing space after `5` in the Content-Length value. + +## What the RFC says + +RFC 9112 §5 defines the field-line syntax: + +> "field-line = field-name ":" OWS field-value OWS" + +The specification explicitly requires parsers to strip trailing whitespace: + +> "A field line value might be preceded and/or followed by optional whitespace (OWS); a single SP preceding the field line value is preferred for consistent readability by humans. The field line value does not include that leading or trailing whitespace: OWS occurring before the first non-whitespace octet of the field line value, or after the last non-whitespace octet of the field line value, is excluded by parsers when extracting the field line value from a field line." + +After OWS stripping, the remaining value `5` is a valid `1*DIGIT` Content-Length. + +## Pass / Warn + +OWS (optional whitespace) after the field value is explicitly permitted by RFC 9112 §5. Trimming it and processing normally is valid behavior. However, `400` is the stricter/safer choice. Both responses are RFC-compliant, but rejection is preferred. + +## Why it matters + +While trailing OWS is permitted, some parsers may include the trailing space in the field value, causing the Content-Length to be parsed as `5 ` rather than `5`. This could lead to parser disagreements or numeric conversion failures in proxy chains. + +## Deep Analysis + +### ABNF Analysis + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +The raw field-line value (before OWS processing) is `5 ` (digit followed by a space). However, RFC 9112 §5 defines the field-line syntax: + +> `field-line = field-name ":" OWS field-value OWS` + +The trailing OWS after the field value is explicitly permitted and must be stripped: + +> "A field line value might be preceded and/or followed by optional whitespace (OWS); a single SP preceding the field line value is preferred for consistent readability by humans. The field line value does not include that leading or trailing whitespace: OWS occurring before the first non-whitespace octet of the field line value, or after the last non-whitespace octet of the field line value, is excluded by parsers when extracting the field line value from a field line." + +After stripping the trailing OWS, the extracted field value is `5`, which matches `1*DIGIT` and is valid. + +### RFC Evidence Chain + +**Step 1 -- Trailing OWS is explicitly permitted.** + +The space after `5` falls after the last non-whitespace octet of the field value. Per RFC 9112 §5, parsers must exclude it. The resulting field value is `5`. + +**Step 2 -- The extracted value is valid.** + +After OWS stripping, `5` matches `1*DIGIT`. No invalid Content-Length rule is triggered. RFC 9112 §6.3 only applies to invalid Content-Length values: + +> "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." + +Since the extracted value `5` is valid, no MUST-reject obligation arises. + +**Step 3 -- Both responses are acceptable.** + +A server that correctly strips trailing OWS and processes the request normally (2xx) is following RFC 9112 §5 precisely. A server that rejects with 400 is being stricter than required but not violating any rule. + +### Scored / SHOULD Justification + +This test is scored as **SHOULD** reject. Trailing OWS is explicitly allowed by RFC 9112 §5. Both `400` (strict rejection) and `2xx` (standard OWS trimming) are compliant behaviors, but rejection is the safer choice in proxy chains where OWS handling inconsistencies may arise. + +### Real-World Smuggling Scenario + +If a front-end proxy includes the trailing space in the field value and passes `5 ` (with space) to its integer parser, the behavior depends on the parser: some return 5 (ignoring trailing non-digits), some return 0 (parse failure), and some throw an error. Meanwhile the back-end correctly strips OWS and reads 5 bytes of body. A parser that returns 0 reads no body, causing the 5 body bytes to be interpreted as the next request. A parser that throws an error might close the connection or forward unpredictably. The trailing space is a subtle probe for parser differential behavior. + +## Sources + +- [RFC 9112 §5](https://www.rfc-editor.org/rfc/rfc9112#section-5) +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) diff --git a/docs/content/docs/smuggling/cl-underscore.md b/docs/content/docs/smuggling/cl-underscore.md index 935a0c7..b431b91 100644 --- a/docs/content/docs/smuggling/cl-underscore.md +++ b/docs/content/docs/smuggling/cl-underscore.md @@ -1,87 +1,87 @@ ---- -title: "CL-UNDERSCORE" -description: "CL-UNDERSCORE test documentation" -weight: 46 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL-UNDERSCORE` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -Content-Length with an underscore digit separator: `Content-Length: 1_0` with 10 bytes of body. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 1_0\r\n -\r\n -helloworld -``` - - -## What the RFC says - -RFC 9110 §8.6 defines the Content-Length grammar: - -> "Content-Length = 1*DIGIT" - -The underscore character is not a digit. The `1*DIGIT` grammar only permits ASCII digits 0-9, so `1_0` is not a valid Content-Length value. RFC 9110 §8.6 further requires: - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" - -RFC 9112 §6.3 mandates rejection: - -> "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." - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." - -## Why it matters - -Several programming languages (Python, Rust, Java, Ruby, Kotlin) accept underscores as numeric separators in source code (e.g., `1_000_000`). If a server's parser uses a language-level integer-parsing function that accepts underscores, it would read `1_0` as `10`. A stricter front-end proxy would reject the request or misparse the value, creating a parser differential that enables request smuggling. - -## Deep Analysis - -### ABNF Violation - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) exclusively. The value `1_0` contains the underscore character (`_`, 0x5F), which is not a DIGIT (0x30-0x39). Therefore `1_0` fails the `1*DIGIT` grammar and is unambiguously invalid. The underscore breaks the contiguous digit sequence at position 2. - -### RFC Evidence Chain - -**Step 1 -- The value is invalid per the grammar.** - -The underscore is not in the ASCII digit range. Even though `1` and `0` are valid digits, the intervening `_` makes the complete value non-conformant with `1*DIGIT`. - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 - -**Step 2 -- The comma-separated list exception does not apply.** - -RFC 9112 §6.3 provides an exception only when the value "can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." The value `1_0` contains no commas. As a single-element list, `1_0` must be valid `1*DIGIT` -- and it is not. The exception does not apply. - -**Step 3 -- The server must reject with 400.** - -> "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." -- RFC 9112 §6.3 - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 - -### Scored / Unscored Justification - -This test is **scored** (MUST reject). The underscore is not a DIGIT, making the value unambiguously invalid. No exception applies. The RFC mandates 400 and connection close. A `2xx` response is a compliance failure. - -### Real-World Smuggling Scenario - -Many modern programming languages accept underscores as numeric separators in integer literals: Python (`1_000`), Rust (`1_000`), Java (`1_000`), Ruby (`1_000`), Kotlin (`1_000`), Swift (`1_000`), and C# 7.0+ (`1_000`). If a server parses Content-Length by passing the raw string to a language-level integer parser (e.g., Python's `int("1_0")` returns `10`), it would read 10 bytes of body. A front-end that correctly rejects the value sees no body at all, and the attacker's 10 body bytes spill forward as the next request. Alternatively, a parser that stops at the underscore reads only 1 byte, creating a 9-byte smuggling window vs. the 10-byte back-end interpretation. The underscore is specifically dangerous because it is invisible in many code review contexts and widely supported across language ecosystems. - -## Sources - -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL Underscore — Request Smuggling" +description: "Content-Length with an underscore digit separator: Content-Length: 1_0 with 10 bytes of body. Tested against RFC 9110 §8.6." +weight: 46 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL-UNDERSCORE` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +Content-Length with an underscore digit separator: `Content-Length: 1_0` with 10 bytes of body. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 1_0\r\n +\r\n +helloworld +``` + + +## What the RFC says + +RFC 9110 §8.6 defines the Content-Length grammar: + +> "Content-Length = 1*DIGIT" + +The underscore character is not a digit. The `1*DIGIT` grammar only permits ASCII digits 0-9, so `1_0` is not a valid Content-Length value. RFC 9110 §8.6 further requires: + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" + +RFC 9112 §6.3 mandates rejection: + +> "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." + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." + +## Why it matters + +Several programming languages (Python, Rust, Java, Ruby, Kotlin) accept underscores as numeric separators in source code (e.g., `1_000_000`). If a server's parser uses a language-level integer-parsing function that accepts underscores, it would read `1_0` as `10`. A stricter front-end proxy would reject the request or misparse the value, creating a parser differential that enables request smuggling. + +## Deep Analysis + +### ABNF Violation + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +The `1*DIGIT` production requires one or more ASCII digits (`0`-`9`) exclusively. The value `1_0` contains the underscore character (`_`, 0x5F), which is not a DIGIT (0x30-0x39). Therefore `1_0` fails the `1*DIGIT` grammar and is unambiguously invalid. The underscore breaks the contiguous digit sequence at position 2. + +### RFC Evidence Chain + +**Step 1 -- The value is invalid per the grammar.** + +The underscore is not in the ASCII digit range. Even though `1` and `0` are valid digits, the intervening `_` makes the complete value non-conformant with `1*DIGIT`. + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 + +**Step 2 -- The comma-separated list exception does not apply.** + +RFC 9112 §6.3 provides an exception only when the value "can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." The value `1_0` contains no commas. As a single-element list, `1_0` must be valid `1*DIGIT` -- and it is not. The exception does not apply. + +**Step 3 -- The server must reject with 400.** + +> "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." -- RFC 9112 §6.3 + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 + +### Scored / Unscored Justification + +This test is **scored** (MUST reject). The underscore is not a DIGIT, making the value unambiguously invalid. No exception applies. The RFC mandates 400 and connection close. A `2xx` response is a compliance failure. + +### Real-World Smuggling Scenario + +Many modern programming languages accept underscores as numeric separators in integer literals: Python (`1_000`), Rust (`1_000`), Java (`1_000`), Ruby (`1_000`), Kotlin (`1_000`), Swift (`1_000`), and C# 7.0+ (`1_000`). If a server parses Content-Length by passing the raw string to a language-level integer parser (e.g., Python's `int("1_0")` returns `10`), it would read 10 bytes of body. A front-end that correctly rejects the value sees no body at all, and the attacker's 10 body bytes spill forward as the next request. Alternatively, a parser that stops at the underscore reads only 1 byte, creating a 9-byte smuggling window vs. the 10-byte back-end interpretation. The underscore is specifically dangerous because it is invisible in many code review contexts and widely supported across language ecosystems. + +## Sources + +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/cl0-body-poison.md b/docs/content/docs/smuggling/cl0-body-poison.md index 05ed74b..f5a5587 100644 --- a/docs/content/docs/smuggling/cl0-body-poison.md +++ b/docs/content/docs/smuggling/cl0-body-poison.md @@ -1,43 +1,43 @@ ---- -title: "CL0-BODY-POISON" -description: "SMUG-CL0-BODY-POISON test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CL0-BODY-POISON` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) | -| **Requirement** | Unscored | -| **Expected** | `400`/close preferred; poisoned follow-up = warn | - -## What it sends - -A two-step sequence: first a `POST` with `Content-Length: 0` plus one extra byte, then a clean `GET` on the same connection. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 0\r\n -\r\n -X - -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "If a valid Content-Length header field is present without Transfer-Encoding, its decimal value defines the expected message body length in octets." -- RFC 9112 Section 6.2 - -`Content-Length: 0` means no body bytes are part of the first request. This test checks whether trailing bytes can poison parsing of the next request on a keep-alive connection. - -## Why it matters - -`0.CL`-style desync chains rely on parser disagreement about where the first request ends. This sequence test surfaces that behavior directly. - -## Sources - -- [RFC 9112 §6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) +--- +title: "CL0 Body Poison — Request Smuggling" +description: "A two-step sequence: first a POST with Content-Length: 0 plus one extra byte, then a clean GET on the same connection. Tested against RFC 9112 §6.2." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CL0-BODY-POISON` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) | +| **Requirement** | Unscored | +| **Expected** | `400`/close preferred; poisoned follow-up = warn | + +## What it sends + +A two-step sequence: first a `POST` with `Content-Length: 0` plus one extra byte, then a clean `GET` on the same connection. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 0\r\n +\r\n +X + +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "If a valid Content-Length header field is present without Transfer-Encoding, its decimal value defines the expected message body length in octets." -- RFC 9112 Section 6.2 + +`Content-Length: 0` means no body bytes are part of the first request. This test checks whether trailing bytes can poison parsing of the next request on a keep-alive connection. + +## Why it matters + +`0.CL`-style desync chains rely on parser disagreement about where the first request ends. This sequence test surfaces that behavior directly. + +## Sources + +- [RFC 9112 §6.2](https://www.rfc-editor.org/rfc/rfc9112#section-6.2) diff --git a/docs/content/docs/smuggling/clte-conn-close.md b/docs/content/docs/smuggling/clte-conn-close.md index 308178d..c6566a2 100644 --- a/docs/content/docs/smuggling/clte-conn-close.md +++ b/docs/content/docs/smuggling/clte-conn-close.md @@ -1,63 +1,63 @@ ---- -title: "CLTE-CONN-CLOSE" -description: "CLTE-CONN-CLOSE sequence test documentation" -weight: 10 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CLTE-CONN-CLOSE` | -| **Category** | Smuggling | -| **Type** | Sequence (2 steps) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or `2xx` + connection close | - -## What it does - -This is a **sequence test** — it sends multiple requests on the same TCP connection to verify server behavior across the full exchange. - -### Step 1: Ambiguous POST (CL+TE) - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5\r\n -Transfer-Encoding: chunked\r\n -\r\n -0\r\n -\r\n -``` - -A POST with both `Content-Length: 5` and `Transfer-Encoding: chunked`. The chunked body is the `0` terminator (5 bytes), which happens to match the CL value. - -### Step 2: Follow-up GET - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -A normal GET sent on the same connection. This step only executes if the connection is still open after step 1. - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. **Regardless, the server MUST close the connection after responding to such a request** to avoid the potential attacks." — RFC 9112 §6.1 - -The key word is "regardless" — even if the server correctly processes the request via TE, it **must** close the connection afterward. - -## Why it matters - -The MUST-close requirement exists because keeping the connection open after a dual CL+TE request creates a window for request smuggling. If the connection stays alive, any leftover bytes (or a pipelined request) could be misinterpreted. This sequence test verifies the close actually happens. - -## Verdicts - -- **Pass** — Server returns `400` (rejected outright), OR returns `2xx` and closes the connection (step 2 never executes) -- **Fail** — Server returns `2xx` and keeps the connection open (step 2 executes and gets a response) - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "CL/TE Conn Close — Request Smuggling" +description: "This is a sequence test — it sends multiple requests on the same TCP connection to verify server behavior across the full exchange. Tested against RFC 9112 §6.1." +weight: 10 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CLTE-CONN-CLOSE` | +| **Category** | Smuggling | +| **Type** | Sequence (2 steps) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or `2xx` + connection close | + +## What it does + +This is a **sequence test** — it sends multiple requests on the same TCP connection to verify server behavior across the full exchange. + +### Step 1: Ambiguous POST (CL+TE) + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +Transfer-Encoding: chunked\r\n +\r\n +0\r\n +\r\n +``` + +A POST with both `Content-Length: 5` and `Transfer-Encoding: chunked`. The chunked body is the `0` terminator (5 bytes), which happens to match the CL value. + +### Step 2: Follow-up GET + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +A normal GET sent on the same connection. This step only executes if the connection is still open after step 1. + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. **Regardless, the server MUST close the connection after responding to such a request** to avoid the potential attacks." — RFC 9112 §6.1 + +The key word is "regardless" — even if the server correctly processes the request via TE, it **must** close the connection afterward. + +## Why it matters + +The MUST-close requirement exists because keeping the connection open after a dual CL+TE request creates a window for request smuggling. If the connection stays alive, any leftover bytes (or a pipelined request) could be misinterpreted. This sequence test verifies the close actually happens. + +## Verdicts + +- **Pass** — Server returns `400` (rejected outright), OR returns `2xx` and closes the connection (step 2 never executes) +- **Fail** — Server returns `2xx` and keeps the connection open (step 2 executes and gets a response) + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/clte-desync.md b/docs/content/docs/smuggling/clte-desync.md index 192f4a1..a709dd1 100644 --- a/docs/content/docs/smuggling/clte-desync.md +++ b/docs/content/docs/smuggling/clte-desync.md @@ -1,70 +1,70 @@ ---- -title: "CLTE-DESYNC" -description: "CLTE-DESYNC sequence test documentation" -weight: 13 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CLTE-DESYNC` | -| **Category** | Smuggling | -| **Type** | Sequence (2 steps) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or connection close | - -## What it does - -This is a **sequence test** that detects actual CL.TE request boundary desynchronization — the classic request smuggling attack. - -### Step 1: Poison POST (CL=6, TE=chunked, extra byte) - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 6\r\n -Transfer-Encoding: chunked\r\n -\r\n -0\r\n -\r\n -X -``` - -The chunked body terminates at `0\r\n\r\n` (5 bytes), but `Content-Length` claims 6 bytes. The extra `X` byte sits right after the chunked terminator. - -- If the server uses **TE**: reads the chunked terminator (5 bytes), body done. `X` is leftover on the wire. -- If the server uses **CL**: reads 6 bytes (`0\r\n\r\nX`), body done. - -Either way, `X` may poison the connection. - -### Step 2: Follow-up GET - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -Sent immediately after step 1. If `X` is still on the wire, the server sees `XGET / HTTP/1.1` — a malformed request line that triggers a 400. - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 - -The only safe outcomes are rejection (400) or closing the connection. Any other behavior risks desynchronization. - -## Why it matters - -This test detects request **desynchronization on a single server**, not a full proxy-chain exploit. If the poison byte `X` merges with the follow-up GET, the server's request boundary parsing is broken. In a real proxy chain, this class of bug is what enables request smuggling. - -## Verdicts - -- **Pass** — Server returns `400` (rejected outright), OR closes the connection (step 2 never executes) -- **Fail** — Step 2 executes and returns `400` (desync confirmed — poison byte merged with GET) -- **Fail** — Step 2 executes and returns `2xx` (MUST-close violated, connection stayed open) - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [RFC 9112 §11.2](https://www.rfc-editor.org/rfc/rfc9112#section-11.2) +--- +title: "CL/TE Desync — Request Smuggling" +description: "This is a sequence test that detects actual CL.TE request boundary desynchronization — the classic request smuggling attack. Tested against RFC 9112 §6.1." +weight: 13 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CLTE-DESYNC` | +| **Category** | Smuggling | +| **Type** | Sequence (2 steps) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or connection close | + +## What it does + +This is a **sequence test** that detects actual CL.TE request boundary desynchronization — the classic request smuggling attack. + +### Step 1: Poison POST (CL=6, TE=chunked, extra byte) + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 6\r\n +Transfer-Encoding: chunked\r\n +\r\n +0\r\n +\r\n +X +``` + +The chunked body terminates at `0\r\n\r\n` (5 bytes), but `Content-Length` claims 6 bytes. The extra `X` byte sits right after the chunked terminator. + +- If the server uses **TE**: reads the chunked terminator (5 bytes), body done. `X` is leftover on the wire. +- If the server uses **CL**: reads 6 bytes (`0\r\n\r\nX`), body done. + +Either way, `X` may poison the connection. + +### Step 2: Follow-up GET + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +Sent immediately after step 1. If `X` is still on the wire, the server sees `XGET / HTTP/1.1` — a malformed request line that triggers a 400. + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 + +The only safe outcomes are rejection (400) or closing the connection. Any other behavior risks desynchronization. + +## Why it matters + +This test detects request **desynchronization on a single server**, not a full proxy-chain exploit. If the poison byte `X` merges with the follow-up GET, the server's request boundary parsing is broken. In a real proxy chain, this class of bug is what enables request smuggling. + +## Verdicts + +- **Pass** — Server returns `400` (rejected outright), OR closes the connection (step 2 never executes) +- **Fail** — Step 2 executes and returns `400` (desync confirmed — poison byte merged with GET) +- **Fail** — Step 2 executes and returns `2xx` (MUST-close violated, connection stayed open) + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [RFC 9112 §11.2](https://www.rfc-editor.org/rfc/rfc9112#section-11.2) diff --git a/docs/content/docs/smuggling/clte-pipeline.md b/docs/content/docs/smuggling/clte-pipeline.md index b0c6613..0dda982 100644 --- a/docs/content/docs/smuggling/clte-pipeline.md +++ b/docs/content/docs/smuggling/clte-pipeline.md @@ -1,56 +1,56 @@ ---- -title: "CLTE-PIPELINE" -description: "CLTE-PIPELINE test documentation" -weight: 8 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CLTE-PIPELINE` | -| **Category** | Smuggling | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MAY | -| **Expected** | `400` or close preferred; `2xx` acceptable | - -## What it sends - -A request with both `Content-Length` and `Transfer-Encoding: chunked` — the classic CL.TE conflict pattern. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 4\r\n -Transfer-Encoding: chunked\r\n -\r\n -0\r\n -\r\n -``` - -A CL-only parser reads 4 bytes (`0\r\n\r`) as the body. A TE parser sees the `0` chunk as end-of-body. The ambiguity is what makes this a smuggling vector in proxy chains. - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." — RFC 9112 §6.1 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling (Section 11.2) or response splitting (Section 11.1) and ought to be handled as an error." — RFC 9112 §6.3 - -## Why it matters - -When both framing headers are present, different parsers in a proxy chain may disagree on where the body ends. A server that rejects the ambiguous request with `400` eliminates the risk entirely. A server that accepts it (processing via TE alone) is RFC-compliant but relies on connection closure to prevent exploitation. - -## Verdicts - -- **Pass** — Server rejects with `400` or closes the connection (safest behavior) -- **Warn** — Server responds with `2xx` (RFC-compliant if it processes via TE and closes the connection, but the lenient path) -- **Fail** — Any other response - -## Scored / Unscored Justification - -This test is **scored**. Although the RFC uses MAY language, there is a clear preferred outcome: rejecting the ambiguous request is safer than accepting it. A `2xx` response counts as a warning rather than a pass, reflecting the security trade-off. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) -- [PortSwigger — HTTP Request Smuggling](https://portswigger.net/web-security/request-smuggling) +--- +title: "CL/TE Pipeline — Request Smuggling" +description: "A request with both Content-Length and Transfer-Encoding: chunked — the classic CL.TE conflict pattern. Tested against RFC 9112 §6.1." +weight: 8 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CLTE-PIPELINE` | +| **Category** | Smuggling | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MAY | +| **Expected** | `400` or close preferred; `2xx` acceptable | + +## What it sends + +A request with both `Content-Length` and `Transfer-Encoding: chunked` — the classic CL.TE conflict pattern. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 4\r\n +Transfer-Encoding: chunked\r\n +\r\n +0\r\n +\r\n +``` + +A CL-only parser reads 4 bytes (`0\r\n\r`) as the body. A TE parser sees the `0` chunk as end-of-body. The ambiguity is what makes this a smuggling vector in proxy chains. + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." — RFC 9112 §6.1 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling (Section 11.2) or response splitting (Section 11.1) and ought to be handled as an error." — RFC 9112 §6.3 + +## Why it matters + +When both framing headers are present, different parsers in a proxy chain may disagree on where the body ends. A server that rejects the ambiguous request with `400` eliminates the risk entirely. A server that accepts it (processing via TE alone) is RFC-compliant but relies on connection closure to prevent exploitation. + +## Verdicts + +- **Pass** — Server rejects with `400` or closes the connection (safest behavior) +- **Warn** — Server responds with `2xx` (RFC-compliant if it processes via TE and closes the connection, but the lenient path) +- **Fail** — Any other response + +## Scored / Unscored Justification + +This test is **scored**. Although the RFC uses MAY language, there is a clear preferred outcome: rejecting the ambiguous request is safer than accepting it. A `2xx` response counts as a warning rather than a pass, reflecting the security trade-off. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +- [PortSwigger — HTTP Request Smuggling](https://portswigger.net/web-security/request-smuggling) diff --git a/docs/content/docs/smuggling/clte-smuggled-get-cl-non-numeric.md b/docs/content/docs/smuggling/clte-smuggled-get-cl-non-numeric.md index 9f51d3f..c4e2f65 100644 --- a/docs/content/docs/smuggling/clte-smuggled-get-cl-non-numeric.md +++ b/docs/content/docs/smuggling/clte-smuggled-get-cl-non-numeric.md @@ -1,60 +1,60 @@ ---- -title: "CLTE-SMUGGLED-GET-CL-NON-NUMERIC" -description: "CLTE-SMUGGLED-GET-CL-NON-NUMERIC sequence test documentation" -weight: 16 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CLTE-SMUGGLED-GET-CL-NON-NUMERIC` | -| **Category** | Smuggling | -| **Type** | Sequence (single send) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or connection close | - -## What it does - -This is a variant of `SMUG-CLTE-SMUGGLED-GET` that uses a non-numeric `Content-Length` value (`N`) while also sending `Transfer-Encoding: chunked`. - -Some HTTP stacks reject non-numeric Content-Length outright; others parse a numeric prefix and ignore the trailing junk. In a proxy chain, this can create framing disagreements that enable request smuggling. - -## What it sends - -The request body begins with a valid chunked terminator (`0\r\n\r\n`) and then immediately contains an entire `GET /` request. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 45x\r\n -Transfer-Encoding: chunked\r\n -\r\n -0\r\n -\r\n -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 - -Closing the connection after responding prevents any leftover bytes (including an embedded request) from being interpreted as a second request on the same persistent connection. - -## Why it matters - -Numeric-prefix parsing (`45x` parsed as `45`) is a frequent hardening gap. If one hop reads 45 bytes while another treats the value as invalid, their views of the byte stream diverge and the embedded `GET /` can be executed out of band. - -This test checks for smuggling by looking for **multiple HTTP status lines** (multiple responses) after a single client send. - -## Verdicts - -- **Pass**: The server rejects with `400`, or closes the connection. -- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). -- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "CL/TE Smuggled GET CL Non Numeric — Request Smuggling" +description: "This is a variant of SMUG-CLTE-SMUGGLED-GET that uses a non-numeric Content-Length value (N) while also sending Transfer-Encoding: chunked. Some HTTP stacks reject non-numeric…" +weight: 16 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CLTE-SMUGGLED-GET-CL-NON-NUMERIC` | +| **Category** | Smuggling | +| **Type** | Sequence (single send) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or connection close | + +## What it does + +This is a variant of `SMUG-CLTE-SMUGGLED-GET` that uses a non-numeric `Content-Length` value (`N`) while also sending `Transfer-Encoding: chunked`. + +Some HTTP stacks reject non-numeric Content-Length outright; others parse a numeric prefix and ignore the trailing junk. In a proxy chain, this can create framing disagreements that enable request smuggling. + +## What it sends + +The request body begins with a valid chunked terminator (`0\r\n\r\n`) and then immediately contains an entire `GET /` request. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 45x\r\n +Transfer-Encoding: chunked\r\n +\r\n +0\r\n +\r\n +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 + +Closing the connection after responding prevents any leftover bytes (including an embedded request) from being interpreted as a second request on the same persistent connection. + +## Why it matters + +Numeric-prefix parsing (`45x` parsed as `45`) is a frequent hardening gap. If one hop reads 45 bytes while another treats the value as invalid, their views of the byte stream diverge and the embedded `GET /` can be executed out of band. + +This test checks for smuggling by looking for **multiple HTTP status lines** (multiple responses) after a single client send. + +## Verdicts + +- **Pass**: The server rejects with `400`, or closes the connection. +- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). +- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/clte-smuggled-get-cl-plus.md b/docs/content/docs/smuggling/clte-smuggled-get-cl-plus.md index 8ef0818..00b3922 100644 --- a/docs/content/docs/smuggling/clte-smuggled-get-cl-plus.md +++ b/docs/content/docs/smuggling/clte-smuggled-get-cl-plus.md @@ -1,60 +1,60 @@ ---- -title: "CLTE-SMUGGLED-GET-CL-PLUS" -description: "CLTE-SMUGGLED-GET-CL-PLUS sequence test documentation" -weight: 15 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CLTE-SMUGGLED-GET-CL-PLUS` | -| **Category** | Smuggling | -| **Type** | Sequence (single send) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or connection close | - -## What it does - -This is a variant of `SMUG-CLTE-SMUGGLED-GET` that uses a malformed `Content-Length` header (`+N`) while also sending `Transfer-Encoding: chunked`. - -Some HTTP stacks reject `Content-Length: +N` as invalid; others parse it leniently. In a proxy chain, these disagreements can reintroduce classic CL.TE smuggling. - -## What it sends - -The request body begins with a valid chunked terminator (`0\r\n\r\n`) and then immediately contains an entire `GET /` request. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: +45\r\n -Transfer-Encoding: chunked\r\n -\r\n -0\r\n -\r\n -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 - -Even if a server chooses to accept and process the message, it must close the connection afterward to prevent request boundary confusion and smuggling. - -## Why it matters - -Malformed framing headers are a common source of front-end/back-end parsing disagreements. If one hop accepts `Content-Length: +N` while another rejects it (or ignores it in favor of chunked framing), the embedded `GET /` can be interpreted as a separate request. - -This test looks for concrete evidence of request-boundary confusion by checking whether the server emits **multiple HTTP status lines** (multiple responses) after a single client send. In a proxy chain, this same behavior is what enables request smuggling. - -## Verdicts - -- **Pass**: The server rejects with `400`, or closes the connection. -- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). -- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "CL/TE Smuggled GET CL Plus — Request Smuggling" +description: "This is a variant of SMUG-CLTE-SMUGGLED-GET that uses a malformed Content-Length header (+N) while also sending Transfer-Encoding: chunked. Some HTTP stacks reject Content-Length: +N as…" +weight: 15 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CLTE-SMUGGLED-GET-CL-PLUS` | +| **Category** | Smuggling | +| **Type** | Sequence (single send) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or connection close | + +## What it does + +This is a variant of `SMUG-CLTE-SMUGGLED-GET` that uses a malformed `Content-Length` header (`+N`) while also sending `Transfer-Encoding: chunked`. + +Some HTTP stacks reject `Content-Length: +N` as invalid; others parse it leniently. In a proxy chain, these disagreements can reintroduce classic CL.TE smuggling. + +## What it sends + +The request body begins with a valid chunked terminator (`0\r\n\r\n`) and then immediately contains an entire `GET /` request. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: +45\r\n +Transfer-Encoding: chunked\r\n +\r\n +0\r\n +\r\n +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 + +Even if a server chooses to accept and process the message, it must close the connection afterward to prevent request boundary confusion and smuggling. + +## Why it matters + +Malformed framing headers are a common source of front-end/back-end parsing disagreements. If one hop accepts `Content-Length: +N` while another rejects it (or ignores it in favor of chunked framing), the embedded `GET /` can be interpreted as a separate request. + +This test looks for concrete evidence of request-boundary confusion by checking whether the server emits **multiple HTTP status lines** (multiple responses) after a single client send. In a proxy chain, this same behavior is what enables request smuggling. + +## Verdicts + +- **Pass**: The server rejects with `400`, or closes the connection. +- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). +- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/clte-smuggled-get-te-case-mismatch.md b/docs/content/docs/smuggling/clte-smuggled-get-te-case-mismatch.md index 1f4af53..611fbfa 100644 --- a/docs/content/docs/smuggling/clte-smuggled-get-te-case-mismatch.md +++ b/docs/content/docs/smuggling/clte-smuggled-get-te-case-mismatch.md @@ -1,54 +1,54 @@ ---- -title: "CLTE-SMUGGLED-GET-TE-CASE-MISMATCH" -description: "CLTE-SMUGGLED-GET-TE-CASE-MISMATCH sequence test documentation" -weight: 21 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CLTE-SMUGGLED-GET-TE-CASE-MISMATCH` | -| **Category** | Smuggling | -| **Type** | Sequence (single send) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or connection close | - -## What it does - -This is a variant of `SMUG-CLTE-SMUGGLED-GET` where the `Transfer-Encoding` token is written as `Chunked` (case mismatch). - -Some components are case-insensitive as required by the HTTP token rules; others are not. Any case-sensitivity bug in a proxy chain can reintroduce CL.TE framing disagreement. - -## What it sends - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 45\r\n -Transfer-Encoding: Chunked\r\n -\r\n -0\r\n -\r\n -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 - -## Why it matters - -Transfer-coding tokens are a classic source of normalization differences. If one hop treats `Chunked` as `chunked` and another treats it as unknown, message framing can diverge and the embedded `GET` can be processed as a separate request. - -## Verdicts - -- **Pass**: The server rejects with `400`, or closes the connection. -- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). -- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "CL/TE Smuggled GET TE Case Mismatch — Request Smuggling" +description: "This is a variant of SMUG-CLTE-SMUGGLED-GET where the Transfer-Encoding token is written as Chunked (case mismatch). Some components are case-insensitive as required by the HTTP token rules…" +weight: 21 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CLTE-SMUGGLED-GET-TE-CASE-MISMATCH` | +| **Category** | Smuggling | +| **Type** | Sequence (single send) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or connection close | + +## What it does + +This is a variant of `SMUG-CLTE-SMUGGLED-GET` where the `Transfer-Encoding` token is written as `Chunked` (case mismatch). + +Some components are case-insensitive as required by the HTTP token rules; others are not. Any case-sensitivity bug in a proxy chain can reintroduce CL.TE framing disagreement. + +## What it sends + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 45\r\n +Transfer-Encoding: Chunked\r\n +\r\n +0\r\n +\r\n +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 + +## Why it matters + +Transfer-coding tokens are a classic source of normalization differences. If one hop treats `Chunked` as `chunked` and another treats it as unknown, message framing can diverge and the embedded `GET` can be processed as a separate request. + +## Verdicts + +- **Pass**: The server rejects with `400`, or closes the connection. +- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). +- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/clte-smuggled-get-te-leading-comma.md b/docs/content/docs/smuggling/clte-smuggled-get-te-leading-comma.md index 5446456..4d8a766 100644 --- a/docs/content/docs/smuggling/clte-smuggled-get-te-leading-comma.md +++ b/docs/content/docs/smuggling/clte-smuggled-get-te-leading-comma.md @@ -1,54 +1,54 @@ ---- -title: "CLTE-SMUGGLED-GET-TE-LEADING-COMMA" -description: "CLTE-SMUGGLED-GET-TE-LEADING-COMMA sequence test documentation" -weight: 20 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CLTE-SMUGGLED-GET-TE-LEADING-COMMA` | -| **Category** | Smuggling | -| **Type** | Sequence (single send) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or connection close | - -## What it does - -This is a variant of `SMUG-CLTE-SMUGGLED-GET` where the `Transfer-Encoding` field value contains a **leading comma** (`", chunked"`). - -Some parsers ignore empty list elements and treat this as equivalent to `chunked`; others reject it or ignore the header. That discrepancy can enable request smuggling in multi-hop deployments. - -## What it sends - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 45\r\n -Transfer-Encoding: , chunked\r\n -\r\n -0\r\n -\r\n -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 - -## Why it matters - -Comma-list parsing differences are a recurring source of TE normalization bugs. If one hop sees TE as valid and another does not, the embedded `GET` can be interpreted as a separate request by one side. - -## Verdicts - -- **Pass**: The server rejects with `400`, or closes the connection. -- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). -- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "CL/TE Smuggled GET TE Leading Comma — Request Smuggling" +description: "This is a variant of SMUG-CLTE-SMUGGLED-GET where the Transfer-Encoding field value contains a leading comma (\", chunked\"). Some parsers ignore empty list elements and treat this as equivalent…" +weight: 20 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CLTE-SMUGGLED-GET-TE-LEADING-COMMA` | +| **Category** | Smuggling | +| **Type** | Sequence (single send) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or connection close | + +## What it does + +This is a variant of `SMUG-CLTE-SMUGGLED-GET` where the `Transfer-Encoding` field value contains a **leading comma** (`", chunked"`). + +Some parsers ignore empty list elements and treat this as equivalent to `chunked`; others reject it or ignore the header. That discrepancy can enable request smuggling in multi-hop deployments. + +## What it sends + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 45\r\n +Transfer-Encoding: , chunked\r\n +\r\n +0\r\n +\r\n +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 + +## Why it matters + +Comma-list parsing differences are a recurring source of TE normalization bugs. If one hop sees TE as valid and another does not, the embedded `GET` can be interpreted as a separate request by one side. + +## Verdicts + +- **Pass**: The server rejects with `400`, or closes the connection. +- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). +- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/clte-smuggled-get-te-obs-fold.md b/docs/content/docs/smuggling/clte-smuggled-get-te-obs-fold.md index e935258..d6dfb4f 100644 --- a/docs/content/docs/smuggling/clte-smuggled-get-te-obs-fold.md +++ b/docs/content/docs/smuggling/clte-smuggled-get-te-obs-fold.md @@ -1,64 +1,64 @@ ---- -title: "CLTE-SMUGGLED-GET-TE-OBS-FOLD" -description: "CLTE-SMUGGLED-GET-TE-OBS-FOLD sequence test documentation" -weight: 17 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CLTE-SMUGGLED-GET-TE-OBS-FOLD` | -| **Category** | Smuggling | -| **Type** | Sequence (single send) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §5.2](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) · [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or connection close | - -## What it does - -This is a variant of `SMUG-CLTE-SMUGGLED-GET` that uses **obs-fold** (obsolete line folding) on the `Transfer-Encoding` header while also sending `Content-Length`. - -If a server unfolds obs-fold into `Transfer-Encoding: chunked` and then fails to close the connection, the embedded `GET /` can be interpreted as a second request and the server may emit multiple HTTP responses. - -## What it sends - -Transfer-Encoding is split across two lines using obs-fold: - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding:\r\n - chunked\r\n -Content-Length: 45\r\n -\r\n -0\r\n -\r\n -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "A server that receives an obs-fold in a request message... **MUST** either reject the message by sending a 400 (Bad Request)... or replace each received obs-fold with one or more SP octets prior to interpreting the field value..." — RFC 9112 §5.2 - -If unfolded, the message still contains both `Transfer-Encoding` and `Content-Length`, triggering connection safety requirements: - -> "**Regardless, the server MUST close the connection after responding** to such a request." — RFC 9112 §6.1 - -## Why it matters - -Obs-fold is a well-known parsing differential: some components unfold it, others treat it as malformed. When it is applied to `Transfer-Encoding` with `Content-Length` present, that disagreement is directly usable as a CL.TE smuggling vector. - -This test checks for concrete evidence of request-boundary confusion by looking for **multiple HTTP status lines** (multiple responses) after a single client send. In a proxy chain, this same behavior is what enables request smuggling. - -## Verdicts - -- **Pass**: The server rejects with `400`, or closes the connection. -- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). -- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. - -## Sources - -- [RFC 9112 §5.2](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "CL/TE Smuggled GET TE Obs Fold — Request Smuggling" +description: "This is a variant of SMUG-CLTE-SMUGGLED-GET that uses obs-fold (obsolete line folding) on the Transfer-Encoding header while also sending Content-Length. If a server unfolds obs-fold into…" +weight: 17 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CLTE-SMUGGLED-GET-TE-OBS-FOLD` | +| **Category** | Smuggling | +| **Type** | Sequence (single send) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §5.2](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) · [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or connection close | + +## What it does + +This is a variant of `SMUG-CLTE-SMUGGLED-GET` that uses **obs-fold** (obsolete line folding) on the `Transfer-Encoding` header while also sending `Content-Length`. + +If a server unfolds obs-fold into `Transfer-Encoding: chunked` and then fails to close the connection, the embedded `GET /` can be interpreted as a second request and the server may emit multiple HTTP responses. + +## What it sends + +Transfer-Encoding is split across two lines using obs-fold: + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding:\r\n + chunked\r\n +Content-Length: 45\r\n +\r\n +0\r\n +\r\n +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "A server that receives an obs-fold in a request message... **MUST** either reject the message by sending a 400 (Bad Request)... or replace each received obs-fold with one or more SP octets prior to interpreting the field value..." — RFC 9112 §5.2 + +If unfolded, the message still contains both `Transfer-Encoding` and `Content-Length`, triggering connection safety requirements: + +> "**Regardless, the server MUST close the connection after responding** to such a request." — RFC 9112 §6.1 + +## Why it matters + +Obs-fold is a well-known parsing differential: some components unfold it, others treat it as malformed. When it is applied to `Transfer-Encoding` with `Content-Length` present, that disagreement is directly usable as a CL.TE smuggling vector. + +This test checks for concrete evidence of request-boundary confusion by looking for **multiple HTTP status lines** (multiple responses) after a single client send. In a proxy chain, this same behavior is what enables request smuggling. + +## Verdicts + +- **Pass**: The server rejects with `400`, or closes the connection. +- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). +- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. + +## Sources + +- [RFC 9112 §5.2](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/clte-smuggled-get-te-trailing-space.md b/docs/content/docs/smuggling/clte-smuggled-get-te-trailing-space.md index 3129048..d8f5fa7 100644 --- a/docs/content/docs/smuggling/clte-smuggled-get-te-trailing-space.md +++ b/docs/content/docs/smuggling/clte-smuggled-get-te-trailing-space.md @@ -1,54 +1,54 @@ ---- -title: "CLTE-SMUGGLED-GET-TE-TRAILING-SPACE" -description: "CLTE-SMUGGLED-GET-TE-TRAILING-SPACE sequence test documentation" -weight: 19 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CLTE-SMUGGLED-GET-TE-TRAILING-SPACE` | -| **Category** | Smuggling | -| **Type** | Sequence (single send) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or connection close | - -## What it does - -This is a variant of `SMUG-CLTE-SMUGGLED-GET` where the `Transfer-Encoding` value includes a **trailing space** (`chunked␠`). - -Some components treat this as invalid and fall back to `Content-Length`; others trim and treat it as `chunked`. In a multi-hop chain, that parsing differential can enable CL.TE request smuggling. - -## What it sends - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 45\r\n -Transfer-Encoding: chunked \r\n -\r\n -0\r\n -\r\n -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 - -## Why it matters - -TE value normalization is a common hardening gap. If one hop trims and another does not, the chain can disagree about message framing and interpret the embedded `GET` as a second request. - -## Verdicts - -- **Pass**: The server rejects with `400`, or closes the connection. -- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). -- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "CL/TE Smuggled GET TE Trailing Space — Request Smuggling" +description: "This is a variant of SMUG-CLTE-SMUGGLED-GET where the Transfer-Encoding value includes a trailing space (chunked␠). Some components treat this as invalid and fall back to Content-Length; others…" +weight: 19 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CLTE-SMUGGLED-GET-TE-TRAILING-SPACE` | +| **Category** | Smuggling | +| **Type** | Sequence (single send) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or connection close | + +## What it does + +This is a variant of `SMUG-CLTE-SMUGGLED-GET` where the `Transfer-Encoding` value includes a **trailing space** (`chunked␠`). + +Some components treat this as invalid and fall back to `Content-Length`; others trim and treat it as `chunked`. In a multi-hop chain, that parsing differential can enable CL.TE request smuggling. + +## What it sends + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 45\r\n +Transfer-Encoding: chunked \r\n +\r\n +0\r\n +\r\n +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 + +## Why it matters + +TE value normalization is a common hardening gap. If one hop trims and another does not, the chain can disagree about message framing and interpret the embedded `GET` as a second request. + +## Verdicts + +- **Pass**: The server rejects with `400`, or closes the connection. +- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). +- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/clte-smuggled-get.md b/docs/content/docs/smuggling/clte-smuggled-get.md index 48abf3c..4488980 100644 --- a/docs/content/docs/smuggling/clte-smuggled-get.md +++ b/docs/content/docs/smuggling/clte-smuggled-get.md @@ -1,61 +1,61 @@ ---- -title: "CLTE-SMUGGLED-GET" -description: "CLTE-SMUGGLED-GET sequence test documentation" -weight: 14 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CLTE-SMUGGLED-GET` | -| **Category** | Smuggling | -| **Type** | Sequence (single send) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or connection close | - -## What it does - -This test is the "real" version of `SMUG-CLTE-DESYNC`: instead of smuggling a single poison byte (`X`), it smuggles a complete `GET /` request into the ambiguous body. - -If a server accepts an ambiguous `Content-Length` + `Transfer-Encoding: chunked` request and keeps the connection open, it risks parsing the embedded `GET /` as a second request and sending **two HTTP responses** on one connection. - -## What it sends - -The request body begins with a valid chunked terminator (`0\r\n\r\n`) and then immediately contains an entire `GET /` request. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 45\r\n -Transfer-Encoding: chunked\r\n -\r\n -0\r\n -\r\n -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 - -This rule exists specifically to prevent request smuggling and desynchronization when different HTTP processors disagree about message boundaries. - -## Why it matters - -In a real proxy chain, if a front-end uses `Content-Length` while a back-end uses `Transfer-Encoding: chunked`, the embedded `GET /` can be treated as a separate request by the back-end and "smuggled" past the front-end's routing and security checks. - -This test looks for concrete evidence of request-boundary confusion by checking whether the server emits **multiple HTTP status lines** (multiple responses) after a single client send. In a proxy chain, this same behavior is what enables request smuggling. - -## Verdicts - -- **Pass**: The server rejects with `400`, or closes the connection. -- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). -- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [RFC 9112 §11.2](https://www.rfc-editor.org/rfc/rfc9112#section-11.2) +--- +title: "CL/TE Smuggled GET — Request Smuggling" +description: "This test is the \"real\" version of SMUG-CLTE-DESYNC: instead of smuggling a single poison byte (X), it smuggles a complete GET / request into the ambiguous body. If a server accepts an…" +weight: 14 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CLTE-SMUGGLED-GET` | +| **Category** | Smuggling | +| **Type** | Sequence (single send) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or connection close | + +## What it does + +This test is the "real" version of `SMUG-CLTE-DESYNC`: instead of smuggling a single poison byte (`X`), it smuggles a complete `GET /` request into the ambiguous body. + +If a server accepts an ambiguous `Content-Length` + `Transfer-Encoding: chunked` request and keeps the connection open, it risks parsing the embedded `GET /` as a second request and sending **two HTTP responses** on one connection. + +## What it sends + +The request body begins with a valid chunked terminator (`0\r\n\r\n`) and then immediately contains an entire `GET /` request. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 45\r\n +Transfer-Encoding: chunked\r\n +\r\n +0\r\n +\r\n +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 + +This rule exists specifically to prevent request smuggling and desynchronization when different HTTP processors disagree about message boundaries. + +## Why it matters + +In a real proxy chain, if a front-end uses `Content-Length` while a back-end uses `Transfer-Encoding: chunked`, the embedded `GET /` can be treated as a separate request by the back-end and "smuggled" past the front-end's routing and security checks. + +This test looks for concrete evidence of request-boundary confusion by checking whether the server emits **multiple HTTP status lines** (multiple responses) after a single client send. In a proxy chain, this same behavior is what enables request smuggling. + +## Verdicts + +- **Pass**: The server rejects with `400`, or closes the connection. +- **Fail**: Multiple HTTP responses are observed (embedded GET likely executed). +- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [RFC 9112 §11.2](https://www.rfc-editor.org/rfc/rfc9112#section-11.2) diff --git a/docs/content/docs/smuggling/clte-smuggled-head.md b/docs/content/docs/smuggling/clte-smuggled-head.md index f3d6415..d2cb180 100644 --- a/docs/content/docs/smuggling/clte-smuggled-head.md +++ b/docs/content/docs/smuggling/clte-smuggled-head.md @@ -1,54 +1,54 @@ ---- -title: "CLTE-SMUGGLED-HEAD" -description: "CLTE-SMUGGLED-HEAD sequence test documentation" -weight: 18 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-CLTE-SMUGGLED-HEAD` | -| **Category** | Smuggling | -| **Type** | Sequence (single send) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or connection close | - -## What it does - -This is a `HEAD`-based confirmation variant of `SMUG-CLTE-SMUGGLED-GET`. - -It sends an ambiguous `Content-Length` + `Transfer-Encoding: chunked` request whose body contains a complete smuggled `HEAD /` request. If the server parses the body bytes as a second request, it may emit **multiple HTTP responses** after a single client send. - -## What it sends - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 46\r\n -Transfer-Encoding: chunked\r\n -\r\n -0\r\n -\r\n -HEAD / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 - -## Why it matters - -The ambiguity is the same as classic CL.TE smuggling. Using `HEAD` as the embedded request helps confirm tunneling/smuggling behavior in stacks where response bodies are suppressed or buffered differently. - -## Verdicts - -- **Pass**: The server rejects with `400`, or closes the connection. -- **Fail**: Multiple HTTP responses are observed (embedded `HEAD` likely executed). -- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "CL/TE Smuggled HEAD — Request Smuggling" +description: "This is a HEAD-based confirmation variant of SMUG-CLTE-SMUGGLED-GET. It sends an ambiguous Content-Length + Transfer-Encoding: chunked request whose body contains a complete smuggled HEAD /…" +weight: 18 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-CLTE-SMUGGLED-HEAD` | +| **Category** | Smuggling | +| **Type** | Sequence (single send) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or connection close | + +## What it does + +This is a `HEAD`-based confirmation variant of `SMUG-CLTE-SMUGGLED-GET`. + +It sends an ambiguous `Content-Length` + `Transfer-Encoding: chunked` request whose body contains a complete smuggled `HEAD /` request. If the server parses the body bytes as a second request, it may emit **multiple HTTP responses** after a single client send. + +## What it sends + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 46\r\n +Transfer-Encoding: chunked\r\n +\r\n +0\r\n +\r\n +HEAD / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... **Regardless, the server MUST close the connection after responding to such a request.**" — RFC 9112 §6.1 + +## Why it matters + +The ambiguity is the same as classic CL.TE smuggling. Using `HEAD` as the embedded request helps confirm tunneling/smuggling behavior in stacks where response bodies are suppressed or buffered differently. + +## Verdicts + +- **Pass**: The server rejects with `400`, or closes the connection. +- **Fail**: Multiple HTTP responses are observed (embedded `HEAD` likely executed). +- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/duplicate-cl-smuggled-get.md b/docs/content/docs/smuggling/duplicate-cl-smuggled-get.md index 40205cc..ad49044 100644 --- a/docs/content/docs/smuggling/duplicate-cl-smuggled-get.md +++ b/docs/content/docs/smuggling/duplicate-cl-smuggled-get.md @@ -1,59 +1,59 @@ ---- -title: "DUPLICATE-CL-SMUGGLED-GET" -description: "DUPLICATE-CL-SMUGGLED-GET sequence test documentation" -weight: 24 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-DUPLICATE-CL-SMUGGLED-GET` | -| **Category** | Smuggling | -| **Type** | Sequence (single send) | -| **Scored** | Yes | -| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **RFC Level** | MUST | -| **Expected** | `400`, or connection close | - -## What it does - -This is a CL.CL smuggling confirmation variant of `SMUG-DUPLICATE-CL`. - -It sends two different `Content-Length` header fields and includes an embedded `GET /` request immediately after the shorter body's boundary. If a server selects the shorter Content-Length value, the embedded `GET` can be interpreted as the next request on the connection and cause multiple responses to be emitted. - -## What it sends - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 4\r\n -Content-Length: 49\r\n -\r\n -PINGGET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -(Actual `Content-Length: 49` is computed to match the body bytes.) - -## What the RFC says - -RFC 9110 defines `Content-Length` as a single decimal length. Conflicting Content-Length header fields make framing invalid: - -> "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." -- RFC 9110 §8.6 - -## Why it matters - -CL.CL disagreement is a classic request smuggling vector when two HTTP processors choose different Content-Length values (for example, first vs last header). If the connection stays open, the leftover bytes can become a smuggled request. - -This test looks for concrete evidence of request-boundary confusion by detecting multiple HTTP status lines in the response to a single client send. In a proxy chain, this same behavior is what enables request smuggling. - -## Verdicts - -- **Pass**: The server rejects with `400`, or closes the connection. -- **Fail**: Multiple HTTP status lines are observed (embedded `GET` likely executed). -- **Fail**: The server accepts and keeps the connection open (duplicate CL not rejected). - -## Sources - -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [HTTP Request Smuggling (PortSwigger)](https://portswigger.net/web-security/request-smuggling) +--- +title: "Duplicate CL Smuggled GET — Request Smuggling" +description: "This is a CL.CL smuggling confirmation variant of SMUG-DUPLICATE-CL. It sends two different Content-Length header fields and includes an embedded GET / request immediately after the shorter…" +weight: 24 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-DUPLICATE-CL-SMUGGLED-GET` | +| **Category** | Smuggling | +| **Type** | Sequence (single send) | +| **Scored** | Yes | +| **RFC** | [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **RFC Level** | MUST | +| **Expected** | `400`, or connection close | + +## What it does + +This is a CL.CL smuggling confirmation variant of `SMUG-DUPLICATE-CL`. + +It sends two different `Content-Length` header fields and includes an embedded `GET /` request immediately after the shorter body's boundary. If a server selects the shorter Content-Length value, the embedded `GET` can be interpreted as the next request on the connection and cause multiple responses to be emitted. + +## What it sends + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 4\r\n +Content-Length: 49\r\n +\r\n +PINGGET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +(Actual `Content-Length: 49` is computed to match the body bytes.) + +## What the RFC says + +RFC 9110 defines `Content-Length` as a single decimal length. Conflicting Content-Length header fields make framing invalid: + +> "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." -- RFC 9110 §8.6 + +## Why it matters + +CL.CL disagreement is a classic request smuggling vector when two HTTP processors choose different Content-Length values (for example, first vs last header). If the connection stays open, the leftover bytes can become a smuggled request. + +This test looks for concrete evidence of request-boundary confusion by detecting multiple HTTP status lines in the response to a single client send. In a proxy chain, this same behavior is what enables request smuggling. + +## Verdicts + +- **Pass**: The server rejects with `400`, or closes the connection. +- **Fail**: Multiple HTTP status lines are observed (embedded `GET` likely executed). +- **Fail**: The server accepts and keeps the connection open (duplicate CL not rejected). + +## Sources + +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [HTTP Request Smuggling (PortSwigger)](https://portswigger.net/web-security/request-smuggling) diff --git a/docs/content/docs/smuggling/duplicate-cl.md b/docs/content/docs/smuggling/duplicate-cl.md index 211352b..18fef87 100644 --- a/docs/content/docs/smuggling/duplicate-cl.md +++ b/docs/content/docs/smuggling/duplicate-cl.md @@ -1,91 +1,91 @@ ---- -title: "DUPLICATE-CL" -description: "DUPLICATE-CL test documentation" -weight: 2 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-DUPLICATE-CL` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -Two `Content-Length` headers with different values. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5\r\n -Content-Length: 10\r\n -\r\n -hello -``` - - -## What the RFC says - -RFC 9110 §5.2 establishes that multiple header fields with the same name can be combined: - -> "A recipient MAY combine multiple header fields with the same field name into one field with a comma-separated list, in the order in which the header fields were received" - -This means `Content-Length: 5` and `Content-Length: 10` as separate headers is equivalent to `Content-Length: 5, 10`. RFC 9112 §6.3 mandates rejection when the values differ: - -> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." - -Since `5` and `10` differ, the exception does not apply. This is an unrecoverable error: - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." - -## Why it matters - -If parser A uses the first CL (5 bytes) and parser B uses the second (10 bytes), they disagree on body length. The extra 5 bytes that parser B expects can contain an attacker-crafted request -- classic smuggling. - -## Deep Analysis - -### ABNF Analysis - -RFC 9110 §8.6 defines the Content-Length grammar as: - -> `Content-Length = 1*DIGIT` - -Each individual `Content-Length` header has a valid value: `5` and `10` both match `1*DIGIT`. The violation is not in the grammar of either individual value, but in the presence of two `Content-Length` headers with differing values. - -### RFC Evidence Chain - -**Step 1 -- Multiple headers with the same name are combinable.** - -RFC 9110 §5.2 establishes that multiple header fields with the same name can be combined into a comma-separated list. Therefore `Content-Length: 5` and `Content-Length: 10` as separate headers is semantically equivalent to `Content-Length: 5, 10`. - -**Step 2 -- The combined value is invalid.** - -The combined value `5, 10` does not match `1*DIGIT`. RFC 9112 §6.3 evaluates the comma-separated list exception: - -> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." - -Parsing `5, 10` yields `5` and `10`. Both are individually valid, but they differ. The exception requires "all values in the list are the same" -- which fails. This is an unrecoverable error. - -**Step 3 -- The server must reject with 400.** - -> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 - -Additionally, RFC 9110 §8.6 prohibits forwarding: - -> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 - -### Scored / Unscored Justification - -This test is **scored** (MUST reject). Two Content-Length headers with different values produce an invalid combined value where the comma-separated list exception explicitly fails (values are not the same). The RFC mandates 400 and connection close with no discretion. A `2xx` response is a compliance failure. - -### Real-World Smuggling Scenario - -Duplicate Content-Length headers are the most straightforward smuggling vector. Many HTTP implementations select either the first or last header when duplicates exist -- a behavior that is implementation-specific and undocumented. If the front-end proxy uses the first `Content-Length: 5` and forwards 5 bytes of body, but the back-end uses the last `Content-Length: 10` and expects 10 bytes, the back-end waits for 5 more bytes. It consumes the first 5 bytes of the next legitimate request as body data for the current request, then interprets the remainder of that next request as a new (truncated or malformed) request. Conversely, if the front-end uses 10 and the back-end uses 5, the extra 5 bytes spill forward. This is why the RFC is absolute: differing Content-Length values MUST cause rejection. - -## Sources - -- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) -- [RFC 9110 §5.2](https://www.rfc-editor.org/rfc/rfc9110#section-5.2) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "Duplicate CL — Request Smuggling" +description: "Two Content-Length headers with different values. Tested against RFC 9110 Section 8.6." +weight: 2 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-DUPLICATE-CL` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 Section 8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +Two `Content-Length` headers with different values. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +Content-Length: 10\r\n +\r\n +hello +``` + + +## What the RFC says + +RFC 9110 §5.2 establishes that multiple header fields with the same name can be combined: + +> "A recipient MAY combine multiple header fields with the same field name into one field with a comma-separated list, in the order in which the header fields were received" + +This means `Content-Length: 5` and `Content-Length: 10` as separate headers is equivalent to `Content-Length: 5, 10`. RFC 9112 §6.3 mandates rejection when the values differ: + +> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." + +Since `5` and `10` differ, the exception does not apply. This is an unrecoverable error: + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." + +## Why it matters + +If parser A uses the first CL (5 bytes) and parser B uses the second (10 bytes), they disagree on body length. The extra 5 bytes that parser B expects can contain an attacker-crafted request -- classic smuggling. + +## Deep Analysis + +### ABNF Analysis + +RFC 9110 §8.6 defines the Content-Length grammar as: + +> `Content-Length = 1*DIGIT` + +Each individual `Content-Length` header has a valid value: `5` and `10` both match `1*DIGIT`. The violation is not in the grammar of either individual value, but in the presence of two `Content-Length` headers with differing values. + +### RFC Evidence Chain + +**Step 1 -- Multiple headers with the same name are combinable.** + +RFC 9110 §5.2 establishes that multiple header fields with the same name can be combined into a comma-separated list. Therefore `Content-Length: 5` and `Content-Length: 10` as separate headers is semantically equivalent to `Content-Length: 5, 10`. + +**Step 2 -- The combined value is invalid.** + +The combined value `5, 10` does not match `1*DIGIT`. RFC 9112 §6.3 evaluates the comma-separated list exception: + +> "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, unless the field value can be successfully parsed as a comma-separated list, all values in the list are valid, and all values in the list are the same." + +Parsing `5, 10` yields `5` and `10`. Both are individually valid, but they differ. The exception requires "all values in the list are the same" -- which fails. This is an unrecoverable error. + +**Step 3 -- The server must reject with 400.** + +> "If the unrecoverable error is in a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 + +Additionally, RFC 9110 §8.6 prohibits forwarding: + +> "a sender MUST NOT forward a message with a Content-Length header field value that does not match the ABNF above" -- RFC 9110 §8.6 + +### Scored / Unscored Justification + +This test is **scored** (MUST reject). Two Content-Length headers with different values produce an invalid combined value where the comma-separated list exception explicitly fails (values are not the same). The RFC mandates 400 and connection close with no discretion. A `2xx` response is a compliance failure. + +### Real-World Smuggling Scenario + +Duplicate Content-Length headers are the most straightforward smuggling vector. Many HTTP implementations select either the first or last header when duplicates exist -- a behavior that is implementation-specific and undocumented. If the front-end proxy uses the first `Content-Length: 5` and forwards 5 bytes of body, but the back-end uses the last `Content-Length: 10` and expects 10 bytes, the back-end waits for 5 more bytes. It consumes the first 5 bytes of the next legitimate request as body data for the current request, then interprets the remainder of that next request as a new (truncated or malformed) request. Conversely, if the front-end uses 10 and the back-end uses 5, the extra 5 bytes spill forward. This is why the RFC is absolute: differing Content-Length values MUST cause rejection. + +## Sources + +- [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) +- [RFC 9110 §5.2](https://www.rfc-editor.org/rfc/rfc9110#section-5.2) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/expect-100-cl-desync.md b/docs/content/docs/smuggling/expect-100-cl-desync.md index fe90572..6b1d6ca 100644 --- a/docs/content/docs/smuggling/expect-100-cl-desync.md +++ b/docs/content/docs/smuggling/expect-100-cl-desync.md @@ -1,48 +1,48 @@ ---- -title: "EXPECT-100-CL-DESYNC" -description: "SMUG-EXPECT-100-CL-DESYNC test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-EXPECT-100-CL-DESYNC` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) | -| **Requirement** | Unscored | -| **Expected** | `417/400/close` preferred; poisoned follow-up = warn | - -## What it sends - -A `POST` with `Expect: 100-continue` and immediate body bytes, followed by a second `GET` on the same connection. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5\r\n -Expect: 100-continue\r\n -\r\n -hello - -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "The 100 (Continue) interim response indicates that the initial part of a request has been received and has not yet been rejected by the server." -- RFC 9110 Section 10.1.1 - -This test checks whether servers that accept this flow keep connection framing safe for the next request. - -## Partial Coverage Note - -Existing test `SMUG-EXPECT-100-CL` checks one request. This desync variant verifies the post-response connection state using a second request. - -## Why it matters - -Desync risk appears when a server issues a final response without fully consuming the declared body. - -## Sources - -- [RFC 9110 §10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) +--- +title: "Expect 100 CL Desync — Request Smuggling" +description: "A POST with Expect: 100-continue and immediate body bytes, followed by a second GET on the same connection. Tested against RFC 9110 §10.1.1." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-EXPECT-100-CL-DESYNC` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) | +| **Requirement** | Unscored | +| **Expected** | `417/400/close` preferred; poisoned follow-up = warn | + +## What it sends + +A `POST` with `Expect: 100-continue` and immediate body bytes, followed by a second `GET` on the same connection. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +Expect: 100-continue\r\n +\r\n +hello + +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "The 100 (Continue) interim response indicates that the initial part of a request has been received and has not yet been rejected by the server." -- RFC 9110 Section 10.1.1 + +This test checks whether servers that accept this flow keep connection framing safe for the next request. + +## Partial Coverage Note + +Existing test `SMUG-EXPECT-100-CL` checks one request. This desync variant verifies the post-response connection state using a second request. + +## Why it matters + +Desync risk appears when a server issues a final response without fully consuming the declared body. + +## Sources + +- [RFC 9110 §10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) diff --git a/docs/content/docs/smuggling/expect-100-cl.md b/docs/content/docs/smuggling/expect-100-cl.md index fc02126..424ca59 100644 --- a/docs/content/docs/smuggling/expect-100-cl.md +++ b/docs/content/docs/smuggling/expect-100-cl.md @@ -1,74 +1,74 @@ ---- -title: "EXPECT-100-CL" -description: "EXPECT-100-CL test documentation" -weight: 33 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-EXPECT-100-CL` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) | -| **Requirement** | Unscored | -| **Expected** | `100`, `400` or `2xx` | - -## What it sends - -POST with `Content-Length: 5` and `Expect: 100-continue`, body included immediately. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5\r\n -Expect: 100-continue\r\n -\r\n -hello -``` - -The body is sent immediately without waiting for a `100 Continue` response. - - -## What the RFC says - -> "Upon receiving an HTTP/1.1 (or later) request that has a method, target URI, and complete header section that contains a 100-continue expectation and an indication that request content will follow, an origin server MUST send either: an immediate response with a final status code, if that status can be determined by examining just the method, target URI, and header fields, or an immediate 100 (Continue) response to encourage the client to send the request content." — RFC 9110 §10.1.1 - -> "A server MAY omit sending a 100 (Continue) response if it has already received some or all of the content for the corresponding request, or if the framing indicates that there is no content." — RFC 9110 §10.1.1 - -## Why this test is unscored - -The RFC requires the server to send either a `100 Continue` interim response or a final status code when it receives `Expect: 100-continue`. However, the client in this test sends the body immediately without waiting. The server may still process the body normally (responding `2xx`), or it may reject the request. Both behaviors are implementation-dependent and valid. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server responds `100 Continue` (valid — sent interim response despite already having the body) or `2xx` (processes body despite Expect header). - -## Why it matters - -This test checks whether the server properly handles the body stream when `Expect: 100-continue` is present but the body arrives immediately. If the server ignores the body because it was waiting to send `100 Continue`, the body bytes remain on the connection and can be misinterpreted as the next request -- a connection desync. - -## Deep Analysis - -### RFC Evidence - -> "Upon receiving an HTTP/1.1 (or later) request that has a method, target URI, and complete header section that contains a 100-continue expectation and an indication that request content will follow, an origin server MUST send either: an immediate response with a final status code, if that status can be determined by examining just the method, target URI, and header fields, or an immediate 100 (Continue) response to encourage the client to send the request content." -- RFC 9110 Section 10.1.1 - -> "A server MAY omit sending a 100 (Continue) response if it has already received some or all of the content for the corresponding request, or if the framing indicates that there is no content." -- RFC 9110 Section 10.1.1 - -> "A server that responds with a final status code before reading the entire request content SHOULD indicate in that response whether it intends to close the connection or continue reading and discarding the request content." -- RFC 9110 Section 10.1.1 - -### Chain of Reasoning - -1. **The Expect mechanism assumes a client-server handshake.** The normal flow is: client sends headers with `Expect: 100-continue`, waits, server sends `100 Continue` or a final status, then the client sends (or does not send) the body. This test breaks that assumption by sending the body immediately alongside the headers, without waiting for any server response. - -2. **The RFC explicitly permits the server to skip the 100 response.** Section 10.1.1 says the server MAY omit the `100 Continue` response if it has already received the content. This means the server is allowed to silently process the body that arrived early. However, the server might also have internal state that expects to "gate" the body read behind the 100-continue handshake. - -3. **The desync occurs when the server does not read the body.** If the server's `Expect: 100-continue` handling causes it to respond with a final status (e.g., `200 OK`) before reading the 5 bytes declared by Content-Length, those 5 bytes (`hello`) remain on the TCP connection. On a persistent connection, the server will attempt to parse `hello` as the start of the next HTTP request -- interpreting `hello` as a malformed request method and potentially entering an undefined state. - -4. **Attack scenario.** An attacker targets a server that gates body reads behind `Expect: 100-continue`. The attacker sends `POST / HTTP/1.1` with `Expect: 100-continue`, `Content-Length: N`, and a crafted body containing a smuggled HTTP request. The server responds with `200 OK` without reading the body. The smuggled request bytes sit on the connection and are parsed as a new request -- one that the server processes with the attacker's chosen method, path, and headers. - -### Scored / Unscored Justification - -This test is **unscored** (`Scored = false`). The RFC permits the server to omit the `100 Continue` response if it has already received the content, and it also permits the server to respond with a final status before reading the body. Both `400` (rejecting the request outright) and `2xx` (processing the body normally) are defensible behaviors. The critical question -- whether the server leaves unread bytes on the connection -- cannot be determined from the status code alone, which is why this test flags `2xx` as a warning rather than a failure. A `2xx` response may indicate correct body consumption, or it may indicate a desync; the test surfaces the behavior for manual investigation. - -## Sources - -- [RFC 9110 §10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) +--- +title: "Expect 100 CL — Request Smuggling" +description: "POST with Content-Length: 5 and Expect: 100-continue, body included immediately. Tested against RFC 9110 §10.1.1." +weight: 33 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-EXPECT-100-CL` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) | +| **Requirement** | Unscored | +| **Expected** | `100`, `400` or `2xx` | + +## What it sends + +POST with `Content-Length: 5` and `Expect: 100-continue`, body included immediately. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +Expect: 100-continue\r\n +\r\n +hello +``` + +The body is sent immediately without waiting for a `100 Continue` response. + + +## What the RFC says + +> "Upon receiving an HTTP/1.1 (or later) request that has a method, target URI, and complete header section that contains a 100-continue expectation and an indication that request content will follow, an origin server MUST send either: an immediate response with a final status code, if that status can be determined by examining just the method, target URI, and header fields, or an immediate 100 (Continue) response to encourage the client to send the request content." — RFC 9110 §10.1.1 + +> "A server MAY omit sending a 100 (Continue) response if it has already received some or all of the content for the corresponding request, or if the framing indicates that there is no content." — RFC 9110 §10.1.1 + +## Why this test is unscored + +The RFC requires the server to send either a `100 Continue` interim response or a final status code when it receives `Expect: 100-continue`. However, the client in this test sends the body immediately without waiting. The server may still process the body normally (responding `2xx`), or it may reject the request. Both behaviors are implementation-dependent and valid. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server responds `100 Continue` (valid — sent interim response despite already having the body) or `2xx` (processes body despite Expect header). + +## Why it matters + +This test checks whether the server properly handles the body stream when `Expect: 100-continue` is present but the body arrives immediately. If the server ignores the body because it was waiting to send `100 Continue`, the body bytes remain on the connection and can be misinterpreted as the next request -- a connection desync. + +## Deep Analysis + +### RFC Evidence + +> "Upon receiving an HTTP/1.1 (or later) request that has a method, target URI, and complete header section that contains a 100-continue expectation and an indication that request content will follow, an origin server MUST send either: an immediate response with a final status code, if that status can be determined by examining just the method, target URI, and header fields, or an immediate 100 (Continue) response to encourage the client to send the request content." -- RFC 9110 Section 10.1.1 + +> "A server MAY omit sending a 100 (Continue) response if it has already received some or all of the content for the corresponding request, or if the framing indicates that there is no content." -- RFC 9110 Section 10.1.1 + +> "A server that responds with a final status code before reading the entire request content SHOULD indicate in that response whether it intends to close the connection or continue reading and discarding the request content." -- RFC 9110 Section 10.1.1 + +### Chain of Reasoning + +1. **The Expect mechanism assumes a client-server handshake.** The normal flow is: client sends headers with `Expect: 100-continue`, waits, server sends `100 Continue` or a final status, then the client sends (or does not send) the body. This test breaks that assumption by sending the body immediately alongside the headers, without waiting for any server response. + +2. **The RFC explicitly permits the server to skip the 100 response.** Section 10.1.1 says the server MAY omit the `100 Continue` response if it has already received the content. This means the server is allowed to silently process the body that arrived early. However, the server might also have internal state that expects to "gate" the body read behind the 100-continue handshake. + +3. **The desync occurs when the server does not read the body.** If the server's `Expect: 100-continue` handling causes it to respond with a final status (e.g., `200 OK`) before reading the 5 bytes declared by Content-Length, those 5 bytes (`hello`) remain on the TCP connection. On a persistent connection, the server will attempt to parse `hello` as the start of the next HTTP request -- interpreting `hello` as a malformed request method and potentially entering an undefined state. + +4. **Attack scenario.** An attacker targets a server that gates body reads behind `Expect: 100-continue`. The attacker sends `POST / HTTP/1.1` with `Expect: 100-continue`, `Content-Length: N`, and a crafted body containing a smuggled HTTP request. The server responds with `200 OK` without reading the body. The smuggled request bytes sit on the connection and are parsed as a new request -- one that the server processes with the attacker's chosen method, path, and headers. + +### Scored / Unscored Justification + +This test is **unscored** (`Scored = false`). The RFC permits the server to omit the `100 Continue` response if it has already received the content, and it also permits the server to respond with a final status before reading the body. Both `400` (rejecting the request outright) and `2xx` (processing the body normally) are defensible behaviors. The critical question -- whether the server leaves unread bytes on the connection -- cannot be determined from the status code alone, which is why this test flags `2xx` as a warning rather than a failure. A `2xx` response may indicate correct body consumption, or it may indicate a desync; the test surfaces the behavior for manual investigation. + +## Sources + +- [RFC 9110 §10.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1) diff --git a/docs/content/docs/smuggling/get-cl-body-desync.md b/docs/content/docs/smuggling/get-cl-body-desync.md index fe7989d..d360a63 100644 --- a/docs/content/docs/smuggling/get-cl-body-desync.md +++ b/docs/content/docs/smuggling/get-cl-body-desync.md @@ -1,47 +1,47 @@ ---- -title: "GET-CL-BODY-DESYNC" -description: "SMUG-GET-CL-BODY-DESYNC test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-GET-CL-BODY-DESYNC` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §9.3.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.1) | -| **Requirement** | Unscored | -| **Expected** | `400`/close/pass-through; poisoned follow-up = warn | - -## What it sends - -A `GET` with `Content-Length: 5` and body `hello`, followed by a second `GET` on the same socket. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5\r\n -\r\n -hello - -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "Content received in a GET request has no generally defined semantics... and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack." -- RFC 9110 Section 9.3.1 - -This test extends the GET-with-body case into a sequence to detect unread-body desynchronization. - -## Partial Coverage Note - -Existing test `COMP-GET-WITH-CL-BODY` already checks single-request behavior. This test adds a follow-up request to detect connection poisoning. - -## Why it matters - -Single-request `2xx` is not enough to prove safety. The second request reveals whether body bytes were consumed or leaked into the next parse. - -## Sources - -- [RFC 9110 §9.3.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.1) +--- +title: "GET CL Body Desync — Request Smuggling" +description: "A GET with Content-Length: 5 and body hello, followed by a second GET on the same socket. Tested against RFC 9110 §9.3.1." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-GET-CL-BODY-DESYNC` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §9.3.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.1) | +| **Requirement** | Unscored | +| **Expected** | `400`/close/pass-through; poisoned follow-up = warn | + +## What it sends + +A `GET` with `Content-Length: 5` and body `hello`, followed by a second `GET` on the same socket. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +\r\n +hello + +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "Content received in a GET request has no generally defined semantics... and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack." -- RFC 9110 Section 9.3.1 + +This test extends the GET-with-body case into a sequence to detect unread-body desynchronization. + +## Partial Coverage Note + +Existing test `COMP-GET-WITH-CL-BODY` already checks single-request behavior. This test adds a follow-up request to detect connection poisoning. + +## Why it matters + +Single-request `2xx` is not enough to prove safety. The second request reveals whether body bytes were consumed or leaked into the next parse. + +## Sources + +- [RFC 9110 §9.3.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.1) diff --git a/docs/content/docs/smuggling/get-cl-prefix-desync.md b/docs/content/docs/smuggling/get-cl-prefix-desync.md index ad29b15..e500ccc 100644 --- a/docs/content/docs/smuggling/get-cl-prefix-desync.md +++ b/docs/content/docs/smuggling/get-cl-prefix-desync.md @@ -1,60 +1,60 @@ ---- -title: "GET-CL-PREFIX-DESYNC" -description: "GET-CL-PREFIX-DESYNC sequence test documentation" -weight: 62 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-GET-CL-PREFIX-DESYNC` | -| **Category** | Smuggling | -| **Type** | Sequence (2 steps) | -| **Scored** | No | -| **RFC** | [RFC 9110 §9.3.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.1) | -| **RFC Level** | MAY | -| **Expected** | `400/close` preferred; extra response on step 2 = warn | - -## What it does - -Step 1 sends a `GET` with a `Content-Length` body containing an **incomplete** request prefix (it intentionally omits the blank line that ends the header section). Step 2 begins with a blank line to complete that prefix and then sends a normal `GET`. - -If the server fails to fully consume the GET body from step 1, the prefix can remain on the connection. Step 2 can then "complete" it, causing the leftover bytes to be interpreted as a real request. - -## What it sends - -Step 1: - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 31\r\n -\r\n -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -``` - -Step 2: - -```http -\r\n -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -(Actual `Content-Length` is computed to match the prefix bytes.) - -## Why it matters - -RFC 9110 notes that content in a GET request "might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack." Even if a server chooses to accept such a request, it must ensure it stays synchronized by consuming or discarding the body bytes. - -This test is unscored because GET-with-body handling is not uniformly defined across deployments, but a desync signal is still valuable telemetry. - -## Verdicts - -- **Pass**: The server rejects step 1 with `400`, or closes the connection. -- **Warn**: Step 2 yields multiple HTTP status lines (leftover prefix likely executed), or other evidence of desynchronization. - -## Sources - -- [RFC 9110 §9.3.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.1) +--- +title: "GET CL Prefix Desync — Request Smuggling" +description: "Step 1 sends a GET with a Content-Length body containing an incomplete request prefix (it intentionally omits the blank line that ends the header section). Step 2 begins with a blank line to…" +weight: 62 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-GET-CL-PREFIX-DESYNC` | +| **Category** | Smuggling | +| **Type** | Sequence (2 steps) | +| **Scored** | No | +| **RFC** | [RFC 9110 §9.3.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.1) | +| **RFC Level** | MAY | +| **Expected** | `400/close` preferred; extra response on step 2 = warn | + +## What it does + +Step 1 sends a `GET` with a `Content-Length` body containing an **incomplete** request prefix (it intentionally omits the blank line that ends the header section). Step 2 begins with a blank line to complete that prefix and then sends a normal `GET`. + +If the server fails to fully consume the GET body from step 1, the prefix can remain on the connection. Step 2 can then "complete" it, causing the leftover bytes to be interpreted as a real request. + +## What it sends + +Step 1: + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 31\r\n +\r\n +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +``` + +Step 2: + +```http +\r\n +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +(Actual `Content-Length` is computed to match the prefix bytes.) + +## Why it matters + +RFC 9110 notes that content in a GET request "might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack." Even if a server chooses to accept such a request, it must ensure it stays synchronized by consuming or discarding the body bytes. + +This test is unscored because GET-with-body handling is not uniformly defined across deployments, but a desync signal is still valuable telemetry. + +## Verdicts + +- **Pass**: The server rejects step 1 with `400`, or closes the connection. +- **Warn**: Step 2 yields multiple HTTP status lines (leftover prefix likely executed), or other evidence of desynchronization. + +## Sources + +- [RFC 9110 §9.3.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.1) diff --git a/docs/content/docs/smuggling/head-cl-body.md b/docs/content/docs/smuggling/head-cl-body.md index 7e12ecb..7ec536a 100644 --- a/docs/content/docs/smuggling/head-cl-body.md +++ b/docs/content/docs/smuggling/head-cl-body.md @@ -1,75 +1,75 @@ ---- -title: "HEAD-CL-BODY" -description: "HEAD-CL-BODY test documentation" -weight: 35 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-HEAD-CL-BODY` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §9.3.2](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.2) | -| **Requirement** | Unscored | -| **Expected** | `400` or `2xx` | - -## What it sends - -`HEAD / HTTP/1.1` with `Content-Length: 5` and body `hello`. HEAD requests are not supposed to have a response body, but this test sends a request body. - -```http -HEAD / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5\r\n -\r\n -hello -``` - - -## What the RFC says - -> "The HEAD method is identical to GET except that the server MUST NOT send content in the response." — RFC 9110 §9.3.2 - -> "content received in a HEAD request has no generally defined semantics, cannot alter the meaning or target of the request, and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack" — RFC 9110 §9.3.2 - -> "A client SHOULD NOT generate content in a HEAD request unless it is made directly to an origin server that has previously indicated, in or out of band, that such a request has a purpose and will be adequately supported." — RFC 9110 §9.3.2 - -The RFC does not prohibit a request body on HEAD, but the server must properly consume or discard any sent body to prevent it from spilling into the next request on the connection. - -## Why this test is unscored - -The RFC does not prohibit sending a body with HEAD requests. Whether the server rejects the request (`400`) or accepts it and properly consumes the body (`2xx`) are both valid behaviors. The critical requirement is that the server must not leave unread body bytes on the connection. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (must consume body bytes). - -## Why it matters - -If a server responds to HEAD without reading the `Content-Length` worth of body bytes, those bytes remain on the connection and are interpreted as the start of the next request. This is a connection desync that an attacker can exploit for smuggling on persistent connections. - -## Deep Analysis - -### RFC Evidence - -> "The HEAD method is identical to GET except that the server MUST NOT send content in the response." -- RFC 9110 Section 9.3.2 - -> "content received in a HEAD request has no generally defined semantics, cannot alter the meaning or target of the request, and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack" -- RFC 9110 Section 9.3.2 - -> "A client SHOULD NOT generate content in a HEAD request unless it is made directly to an origin server that has previously indicated, in or out of band, that such a request has a purpose and will be adequately supported." -- RFC 9110 Section 9.3.2 - -### Chain of Reasoning - -1. **HEAD with a body is unusual but not prohibited.** The RFC uses SHOULD NOT (not MUST NOT) for the client side, meaning sending content with HEAD is discouraged but technically allowed. The server side has no explicit MUST about rejecting body content on HEAD requests. This gray area is exactly what makes the test interesting from a smuggling perspective. - -2. **The server's response framing is the key issue.** HEAD responses MUST NOT include a response body, but they MAY include `Content-Length` indicating what the equivalent GET response would return. This means the server produces a response quickly -- potentially before it reads the 5 bytes of request body declared by `Content-Length: 5`. - -3. **Unconsumed body bytes become a smuggled request.** If the server responds to the HEAD without draining the request body, the 5 bytes (`hello`) remain on the TCP connection. On a keep-alive connection, the server's HTTP parser will attempt to read these bytes as the start-line of the next request. The parser sees `hello` where it expects a method like `GET` or `POST`, which may cause an error -- or, with carefully crafted content, it could be parsed as a valid request. - -4. **Attack scenario.** An attacker sends `HEAD / HTTP/1.1` with `Content-Length: N` where the body contains a complete smuggled HTTP request (e.g., `GET /admin HTTP/1.1\r\nHost: target\r\n\r\n`). The server processes the HEAD, sends back headers without a body, and if it does not consume the `N` bytes of declared content, the smuggled `GET /admin` request is parsed as the next request on the connection. On a shared connection (e.g., behind a load balancer), this smuggled request executes with the next legitimate user's credentials. - -### Scored / Unscored Justification - -This test is **unscored** (`Scored = false`). The RFC explicitly acknowledges that HEAD requests with content have "no generally defined semantics" and that implementations may reject them. Neither rejection (`400`) nor acceptance (`2xx`) violates a MUST-level requirement. The real danger -- whether the server leaves body bytes on the connection -- is a behavioral property that cannot be reliably determined from the status code alone. A `2xx` response could mean the server properly consumed the body (safe) or ignored it (vulnerable). The test flags `2xx` as a warning to prompt manual investigation of the server's connection handling. - -## Sources - -- [RFC 9110 §9.3.2](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.2) +--- +title: "HEAD CL Body — Request Smuggling" +description: "HEAD / HTTP/1.1 with Content-Length: 5 and body hello. HEAD requests are not supposed to have a response body, but this test sends a request body. Tested against RFC 9110 §9.3.2." +weight: 35 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-HEAD-CL-BODY` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §9.3.2](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.2) | +| **Requirement** | Unscored | +| **Expected** | `400` or `2xx` | + +## What it sends + +`HEAD / HTTP/1.1` with `Content-Length: 5` and body `hello`. HEAD requests are not supposed to have a response body, but this test sends a request body. + +```http +HEAD / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +\r\n +hello +``` + + +## What the RFC says + +> "The HEAD method is identical to GET except that the server MUST NOT send content in the response." — RFC 9110 §9.3.2 + +> "content received in a HEAD request has no generally defined semantics, cannot alter the meaning or target of the request, and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack" — RFC 9110 §9.3.2 + +> "A client SHOULD NOT generate content in a HEAD request unless it is made directly to an origin server that has previously indicated, in or out of band, that such a request has a purpose and will be adequately supported." — RFC 9110 §9.3.2 + +The RFC does not prohibit a request body on HEAD, but the server must properly consume or discard any sent body to prevent it from spilling into the next request on the connection. + +## Why this test is unscored + +The RFC does not prohibit sending a body with HEAD requests. Whether the server rejects the request (`400`) or accepts it and properly consumes the body (`2xx`) are both valid behaviors. The critical requirement is that the server must not leave unread body bytes on the connection. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (must consume body bytes). + +## Why it matters + +If a server responds to HEAD without reading the `Content-Length` worth of body bytes, those bytes remain on the connection and are interpreted as the start of the next request. This is a connection desync that an attacker can exploit for smuggling on persistent connections. + +## Deep Analysis + +### RFC Evidence + +> "The HEAD method is identical to GET except that the server MUST NOT send content in the response." -- RFC 9110 Section 9.3.2 + +> "content received in a HEAD request has no generally defined semantics, cannot alter the meaning or target of the request, and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack" -- RFC 9110 Section 9.3.2 + +> "A client SHOULD NOT generate content in a HEAD request unless it is made directly to an origin server that has previously indicated, in or out of band, that such a request has a purpose and will be adequately supported." -- RFC 9110 Section 9.3.2 + +### Chain of Reasoning + +1. **HEAD with a body is unusual but not prohibited.** The RFC uses SHOULD NOT (not MUST NOT) for the client side, meaning sending content with HEAD is discouraged but technically allowed. The server side has no explicit MUST about rejecting body content on HEAD requests. This gray area is exactly what makes the test interesting from a smuggling perspective. + +2. **The server's response framing is the key issue.** HEAD responses MUST NOT include a response body, but they MAY include `Content-Length` indicating what the equivalent GET response would return. This means the server produces a response quickly -- potentially before it reads the 5 bytes of request body declared by `Content-Length: 5`. + +3. **Unconsumed body bytes become a smuggled request.** If the server responds to the HEAD without draining the request body, the 5 bytes (`hello`) remain on the TCP connection. On a keep-alive connection, the server's HTTP parser will attempt to read these bytes as the start-line of the next request. The parser sees `hello` where it expects a method like `GET` or `POST`, which may cause an error -- or, with carefully crafted content, it could be parsed as a valid request. + +4. **Attack scenario.** An attacker sends `HEAD / HTTP/1.1` with `Content-Length: N` where the body contains a complete smuggled HTTP request (e.g., `GET /admin HTTP/1.1\r\nHost: target\r\n\r\n`). The server processes the HEAD, sends back headers without a body, and if it does not consume the `N` bytes of declared content, the smuggled `GET /admin` request is parsed as the next request on the connection. On a shared connection (e.g., behind a load balancer), this smuggled request executes with the next legitimate user's credentials. + +### Scored / Unscored Justification + +This test is **unscored** (`Scored = false`). The RFC explicitly acknowledges that HEAD requests with content have "no generally defined semantics" and that implementations may reject them. Neither rejection (`400`) nor acceptance (`2xx`) violates a MUST-level requirement. The real danger -- whether the server leaves body bytes on the connection -- is a behavioral property that cannot be reliably determined from the status code alone. A `2xx` response could mean the server properly consumed the body (safe) or ignored it (vulnerable). The test flags `2xx` as a warning to prompt manual investigation of the server's connection handling. + +## Sources + +- [RFC 9110 §9.3.2](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.2) diff --git a/docs/content/docs/smuggling/multiple-host-comma.md b/docs/content/docs/smuggling/multiple-host-comma.md index 62649e2..4de294a 100644 --- a/docs/content/docs/smuggling/multiple-host-comma.md +++ b/docs/content/docs/smuggling/multiple-host-comma.md @@ -1,75 +1,75 @@ ---- -title: "MULTIPLE-HOST-COMMA" -description: "MULTIPLE-HOST-COMMA test documentation" -weight: 52 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-MULTIPLE-HOST-COMMA` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §7.2](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -A Host header with two comma-separated hostnames. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080, other.example.com\r\n -\r\n -``` - -The Host header contains `localhost:8080, other.example.com` — two distinct hostnames in a single header value. - - -## What the RFC says - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." — RFC 9112 §3.2 - -The Host header is not a list-based field. A comma in the Host value does not indicate multiple list elements — it means the value itself contains two distinct hostnames, which is an invalid field value. The server MUST reject such a request. - -## Why it matters - -If a front-end proxy extracts the first hostname (`localhost:8080`) for routing but the back-end extracts the second (`other.example.com`), routing confusion occurs. An attacker could use this to bypass virtual host restrictions, access internal services, or poison caches for the wrong host. This is a host-header injection vector that enables cache poisoning and SSRF attacks. - -## Deep Analysis - -### Relevant ABNF - -The Host header has a specific grammar that does not permit list syntax: - -``` -Host = uri-host [ ":" port ] -``` - -Unlike most HTTP headers, Host is a singleton field -- it is not defined as a comma-separated list (`#element`). A comma in the Host value is not a list separator; it is a literal character that makes the entire value invalid. - -### RFC Evidence - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -> "A client MUST send a Host header field in an HTTP/1.1 request even if the request-target is in absolute-form." -- RFC 9112 Section 3.2.2 - -> "If the target URI's authority component is empty, the client MUST send a Host header field with an empty field-value." -- RFC 9110 Section 7.2 - -### Chain of Reasoning - -1. **The ABNF for Host is a single `uri-host` with optional port.** Unlike headers such as `Accept` or `Cache-Control`, which use `#element` (comma-separated list) syntax, the Host header is defined as a single value. The value `localhost:8080, other.example.com` does not match `uri-host [ ":" port ]` because the comma and second hostname make the value syntactically invalid per the grammar. - -2. **RFC 9112 Section 3.2 mandates rejection.** The requirement uses MUST language: the server MUST respond with `400` to any request with "a Host header field with an invalid field value." A comma-separated list of hostnames is an invalid field value for the Host header. There is no MAY or SHOULD qualifier -- this is an absolute requirement. - -3. **Parser disagreement on comma-separated Host enables routing attacks.** Different implementations may extract different hostnames from `localhost:8080, other.example.com`. A proxy that takes the first value routes to `localhost:8080`. A back-end that takes the last value routes to `other.example.com`. A third implementation might use the entire string as-is, failing to match any virtual host. This inconsistency is the foundation of host-header injection attacks. - -4. **Attack scenario.** An attacker sends `Host: legitimate.com, attacker.com` to a CDN. The CDN extracts `legitimate.com` for cache key computation and routes the request to the legitimate origin. The origin extracts `attacker.com` and generates a response with links, redirects, or resource URLs pointing to `attacker.com`. The CDN caches this poisoned response under the `legitimate.com` cache key. Every subsequent visitor to `legitimate.com` receives the poisoned response with attacker-controlled URLs. - -### Scored / Unscored Justification - -This test is **scored**. RFC 9112 Section 3.2 uses unconditional MUST language: the server "MUST respond with a 400 (Bad Request) status code" for requests with an invalid Host field value. A comma-separated Host value is syntactically invalid per the Host ABNF. There is no ambiguity in the requirement and no alternative interpretation that would allow a `2xx` response. A server that accepts this request is in direct violation of a MUST-level requirement and is vulnerable to host-header injection, cache poisoning, and routing confusion attacks. - -## Sources - -- [RFC 9110 §7.2](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) -- [RFC 9112 §3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) +--- +title: "Multiple Host Comma — Request Smuggling" +description: "A Host header with two comma-separated hostnames. Tested against RFC 9110 §7.2." +weight: 52 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-MULTIPLE-HOST-COMMA` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §7.2](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +A Host header with two comma-separated hostnames. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080, other.example.com\r\n +\r\n +``` + +The Host header contains `localhost:8080, other.example.com` — two distinct hostnames in a single header value. + + +## What the RFC says + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." — RFC 9112 §3.2 + +The Host header is not a list-based field. A comma in the Host value does not indicate multiple list elements — it means the value itself contains two distinct hostnames, which is an invalid field value. The server MUST reject such a request. + +## Why it matters + +If a front-end proxy extracts the first hostname (`localhost:8080`) for routing but the back-end extracts the second (`other.example.com`), routing confusion occurs. An attacker could use this to bypass virtual host restrictions, access internal services, or poison caches for the wrong host. This is a host-header injection vector that enables cache poisoning and SSRF attacks. + +## Deep Analysis + +### Relevant ABNF + +The Host header has a specific grammar that does not permit list syntax: + +``` +Host = uri-host [ ":" port ] +``` + +Unlike most HTTP headers, Host is a singleton field -- it is not defined as a comma-separated list (`#element`). A comma in the Host value is not a list separator; it is a literal character that makes the entire value invalid. + +### RFC Evidence + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +> "A client MUST send a Host header field in an HTTP/1.1 request even if the request-target is in absolute-form." -- RFC 9112 Section 3.2.2 + +> "If the target URI's authority component is empty, the client MUST send a Host header field with an empty field-value." -- RFC 9110 Section 7.2 + +### Chain of Reasoning + +1. **The ABNF for Host is a single `uri-host` with optional port.** Unlike headers such as `Accept` or `Cache-Control`, which use `#element` (comma-separated list) syntax, the Host header is defined as a single value. The value `localhost:8080, other.example.com` does not match `uri-host [ ":" port ]` because the comma and second hostname make the value syntactically invalid per the grammar. + +2. **RFC 9112 Section 3.2 mandates rejection.** The requirement uses MUST language: the server MUST respond with `400` to any request with "a Host header field with an invalid field value." A comma-separated list of hostnames is an invalid field value for the Host header. There is no MAY or SHOULD qualifier -- this is an absolute requirement. + +3. **Parser disagreement on comma-separated Host enables routing attacks.** Different implementations may extract different hostnames from `localhost:8080, other.example.com`. A proxy that takes the first value routes to `localhost:8080`. A back-end that takes the last value routes to `other.example.com`. A third implementation might use the entire string as-is, failing to match any virtual host. This inconsistency is the foundation of host-header injection attacks. + +4. **Attack scenario.** An attacker sends `Host: legitimate.com, attacker.com` to a CDN. The CDN extracts `legitimate.com` for cache key computation and routes the request to the legitimate origin. The origin extracts `attacker.com` and generates a response with links, redirects, or resource URLs pointing to `attacker.com`. The CDN caches this poisoned response under the `legitimate.com` cache key. Every subsequent visitor to `legitimate.com` receives the poisoned response with attacker-controlled URLs. + +### Scored / Unscored Justification + +This test is **scored**. RFC 9112 Section 3.2 uses unconditional MUST language: the server "MUST respond with a 400 (Bad Request) status code" for requests with an invalid Host field value. A comma-separated Host value is syntactically invalid per the Host ABNF. There is no ambiguity in the requirement and no alternative interpretation that would allow a `2xx` response. A server that accepts this request is in direct violation of a MUST-level requirement and is vulnerable to host-header injection, cache poisoning, and routing confusion attacks. + +## Sources + +- [RFC 9110 §7.2](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) +- [RFC 9112 §3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) diff --git a/docs/content/docs/smuggling/options-cl-body-desync.md b/docs/content/docs/smuggling/options-cl-body-desync.md index 5a5a136..dc6b959 100644 --- a/docs/content/docs/smuggling/options-cl-body-desync.md +++ b/docs/content/docs/smuggling/options-cl-body-desync.md @@ -1,47 +1,47 @@ ---- -title: "OPTIONS-CL-BODY-DESYNC" -description: "SMUG-OPTIONS-CL-BODY-DESYNC test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-OPTIONS-CL-BODY-DESYNC` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) | -| **Requirement** | Unscored | -| **Expected** | `400`/close/pass-through; poisoned follow-up = warn | - -## What it sends - -An `OPTIONS` request with `Content-Length: 5` and body `hello`, followed by a second `GET` on the same connection. - -```http -OPTIONS / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5\r\n -\r\n -hello - -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "An OPTIONS request containing content must send a valid Content-Type header field describing the representation media type." -- RFC 9110 Section 9.3.7 - -While server behavior differs across frameworks, this sequence checks whether body handling leaves the connection in a desynchronized state. - -## Partial Coverage Note - -Existing test `SMUG-OPTIONS-CL-BODY` checks a single request. This variant adds a follow-up request to detect unread-body poisoning. - -## Why it matters - -OPTIONS is common in CORS workflows. If a server responds before consuming bytes, those bytes can corrupt the next request boundary. - -## Sources - -- [RFC 9110 §9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) +--- +title: "OPTIONS CL Body Desync — Request Smuggling" +description: "An OPTIONS request with Content-Length: 5 and body hello, followed by a second GET on the same connection. Tested against RFC 9110 §9.3.7." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-OPTIONS-CL-BODY-DESYNC` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) | +| **Requirement** | Unscored | +| **Expected** | `400`/close/pass-through; poisoned follow-up = warn | + +## What it sends + +An `OPTIONS` request with `Content-Length: 5` and body `hello`, followed by a second `GET` on the same connection. + +```http +OPTIONS / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +\r\n +hello + +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "An OPTIONS request containing content must send a valid Content-Type header field describing the representation media type." -- RFC 9110 Section 9.3.7 + +While server behavior differs across frameworks, this sequence checks whether body handling leaves the connection in a desynchronized state. + +## Partial Coverage Note + +Existing test `SMUG-OPTIONS-CL-BODY` checks a single request. This variant adds a follow-up request to detect unread-body poisoning. + +## Why it matters + +OPTIONS is common in CORS workflows. If a server responds before consuming bytes, those bytes can corrupt the next request boundary. + +## Sources + +- [RFC 9110 §9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) diff --git a/docs/content/docs/smuggling/options-cl-body.md b/docs/content/docs/smuggling/options-cl-body.md index 84feab0..7245b60 100644 --- a/docs/content/docs/smuggling/options-cl-body.md +++ b/docs/content/docs/smuggling/options-cl-body.md @@ -1,75 +1,75 @@ ---- -title: "OPTIONS-CL-BODY" -description: "OPTIONS-CL-BODY test documentation" -weight: 36 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-OPTIONS-CL-BODY` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) | -| **Requirement** | Unscored | -| **Expected** | `400` or `2xx` | - -## What it sends - -`OPTIONS / HTTP/1.1` with `Content-Length: 5` and body `hello`. - -```http -OPTIONS / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 5\r\n -\r\n -hello -``` - - -## What the RFC says - -> "A client that generates an OPTIONS request containing content MUST send a valid Content-Type header field describing the representation media type. Note that this specification does not define any use for such content." — RFC 9110 §9.3.7 - -OPTIONS requests may have a body, but the server must properly handle it. This test sends a body without a Content-Type header, which violates the client-side MUST. If the body is not consumed, it leaks onto the connection. - -## Why this test is unscored - -The RFC explicitly allows bodies on OPTIONS requests (with proper Content-Type). Whether the server rejects the request (`400`) or accepts it and processes the body (`2xx`) are both valid behaviors. The critical requirement is that the server must not leave unread body bytes on the connection. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (must consume body bytes). - -## Why it matters - -Like HEAD with a body, if the server responds to OPTIONS without reading the declared body bytes, the remaining data is misinterpreted as the next request. This desync can be exploited to smuggle requests, especially when OPTIONS is commonly used for CORS preflight. - -## Deep Analysis - -### RFC Evidence - -> "A client that generates an OPTIONS request containing content MUST send a valid Content-Type header field describing the representation media type. Note that this specification does not define any use for such content." -- RFC 9110 Section 9.3.7 - -> "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 RFC explicitly permits bodies in OPTIONS requests (unlike HEAD responses, which are forbidden). However, the client-side MUST for Content-Type is important: this test omits Content-Type, meaning the sender is already in violation. - -> "Regardless of the method, if a server receives a request with a message body and a Content-Length field, it MUST either read and discard the body bytes or close the connection." -- RFC 9112 Section 6.3 (paraphrased from body length determination rules) - -### Chain of Reasoning - -1. **OPTIONS with a body is explicitly allowed by the RFC.** Section 9.3.7 does not use MUST NOT or SHOULD NOT for the body itself -- it only requires that a client sending content MUST include Content-Type. This test deliberately omits Content-Type to probe whether the server enforces that client-side requirement. Regardless, the body's 5 bytes are declared via `Content-Length: 5`. - -2. **The server must consume or reject the declared body.** The HTTP/1.1 message body length algorithm in RFC 9112 Section 6.3 is clear: when Content-Length is present, it defines the body length. The server must read exactly that many bytes from the connection before attempting to parse the next request. This applies to all methods, including OPTIONS. - -3. **OPTIONS responses are typically small and fast.** Most servers respond to `OPTIONS /` immediately with `Allow` headers and a `200 OK` with zero-length body. This fast-path processing makes it easy for an implementation to skip body consumption -- the server "knows" the answer without reading the body. But skipping the body read leaves 5 bytes on the connection. - -4. **CORS preflight makes OPTIONS ubiquitous.** In modern web applications, browsers send OPTIONS requests for CORS preflight. This means OPTIONS requests are extremely common in proxy-origin chains. If a server mishandles the body on OPTIONS, the desync is exploitable at scale -- every CORS-enabled endpoint becomes a potential smuggling vector. - -5. **Attack scenario.** An attacker sends `OPTIONS / HTTP/1.1` with `Content-Length: N` where the body contains a crafted HTTP request (e.g., `POST /transfer HTTP/1.1\r\nHost: bank.com\r\n...`). The server responds with `200 OK` and the `Allow` header, without consuming the body. The smuggled POST request sits on the connection and is parsed as the next request. On a shared connection behind a reverse proxy, this smuggled request may execute with the next legitimate user's session cookies. - -### Scored / Unscored Justification - -This test is **unscored** (`Scored = false`). The RFC explicitly allows bodies in OPTIONS requests, and the server has discretion in how it handles one: reject it (`400`, perhaps because Content-Type is missing), accept it and process normally (`2xx`), or even ignore the body content while still properly draining the declared bytes. All of these are defensible behaviors. The dangerous case -- where the server responds without consuming the body -- cannot be definitively detected from the status code alone. A `2xx` response might mean the server properly drained the body (safe) or skipped it entirely (vulnerable). The test flags `2xx` as a warning to prompt investigation of the server's connection behavior. - -## Sources - -- [RFC 9110 §9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) +--- +title: "OPTIONS CL Body — Request Smuggling" +description: "OPTIONS / HTTP/1.1 with Content-Length: 5 and body hello. Tested against RFC 9110 §9.3.7." +weight: 36 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-OPTIONS-CL-BODY` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) | +| **Requirement** | Unscored | +| **Expected** | `400` or `2xx` | + +## What it sends + +`OPTIONS / HTTP/1.1` with `Content-Length: 5` and body `hello`. + +```http +OPTIONS / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 5\r\n +\r\n +hello +``` + + +## What the RFC says + +> "A client that generates an OPTIONS request containing content MUST send a valid Content-Type header field describing the representation media type. Note that this specification does not define any use for such content." — RFC 9110 §9.3.7 + +OPTIONS requests may have a body, but the server must properly handle it. This test sends a body without a Content-Type header, which violates the client-side MUST. If the body is not consumed, it leaks onto the connection. + +## Why this test is unscored + +The RFC explicitly allows bodies on OPTIONS requests (with proper Content-Type). Whether the server rejects the request (`400`) or accepts it and processes the body (`2xx`) are both valid behaviors. The critical requirement is that the server must not leave unread body bytes on the connection. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (must consume body bytes). + +## Why it matters + +Like HEAD with a body, if the server responds to OPTIONS without reading the declared body bytes, the remaining data is misinterpreted as the next request. This desync can be exploited to smuggle requests, especially when OPTIONS is commonly used for CORS preflight. + +## Deep Analysis + +### RFC Evidence + +> "A client that generates an OPTIONS request containing content MUST send a valid Content-Type header field describing the representation media type. Note that this specification does not define any use for such content." -- RFC 9110 Section 9.3.7 + +> "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 RFC explicitly permits bodies in OPTIONS requests (unlike HEAD responses, which are forbidden). However, the client-side MUST for Content-Type is important: this test omits Content-Type, meaning the sender is already in violation. + +> "Regardless of the method, if a server receives a request with a message body and a Content-Length field, it MUST either read and discard the body bytes or close the connection." -- RFC 9112 Section 6.3 (paraphrased from body length determination rules) + +### Chain of Reasoning + +1. **OPTIONS with a body is explicitly allowed by the RFC.** Section 9.3.7 does not use MUST NOT or SHOULD NOT for the body itself -- it only requires that a client sending content MUST include Content-Type. This test deliberately omits Content-Type to probe whether the server enforces that client-side requirement. Regardless, the body's 5 bytes are declared via `Content-Length: 5`. + +2. **The server must consume or reject the declared body.** The HTTP/1.1 message body length algorithm in RFC 9112 Section 6.3 is clear: when Content-Length is present, it defines the body length. The server must read exactly that many bytes from the connection before attempting to parse the next request. This applies to all methods, including OPTIONS. + +3. **OPTIONS responses are typically small and fast.** Most servers respond to `OPTIONS /` immediately with `Allow` headers and a `200 OK` with zero-length body. This fast-path processing makes it easy for an implementation to skip body consumption -- the server "knows" the answer without reading the body. But skipping the body read leaves 5 bytes on the connection. + +4. **CORS preflight makes OPTIONS ubiquitous.** In modern web applications, browsers send OPTIONS requests for CORS preflight. This means OPTIONS requests are extremely common in proxy-origin chains. If a server mishandles the body on OPTIONS, the desync is exploitable at scale -- every CORS-enabled endpoint becomes a potential smuggling vector. + +5. **Attack scenario.** An attacker sends `OPTIONS / HTTP/1.1` with `Content-Length: N` where the body contains a crafted HTTP request (e.g., `POST /transfer HTTP/1.1\r\nHost: bank.com\r\n...`). The server responds with `200 OK` and the `Allow` header, without consuming the body. The smuggled POST request sits on the connection and is parsed as the next request. On a shared connection behind a reverse proxy, this smuggled request may execute with the next legitimate user's session cookies. + +### Scored / Unscored Justification + +This test is **unscored** (`Scored = false`). The RFC explicitly allows bodies in OPTIONS requests, and the server has discretion in how it handles one: reject it (`400`, perhaps because Content-Type is missing), accept it and process normally (`2xx`), or even ignore the body content while still properly draining the declared bytes. All of these are defensible behaviors. The dangerous case -- where the server responds without consuming the body -- cannot be definitively detected from the status code alone. A `2xx` response might mean the server properly drained the body (safe) or skipped it entirely (vulnerable). The test flags `2xx` as a warning to prompt investigation of the server's connection behavior. + +## Sources + +- [RFC 9110 §9.3.7](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7) diff --git a/docs/content/docs/smuggling/options-te-obs-fold.md b/docs/content/docs/smuggling/options-te-obs-fold.md index 8453c60..e839a29 100644 --- a/docs/content/docs/smuggling/options-te-obs-fold.md +++ b/docs/content/docs/smuggling/options-te-obs-fold.md @@ -1,49 +1,49 @@ ---- -title: "OPTIONS-TE-OBS-FOLD" -description: "SMUG-OPTIONS-TE-OBS-FOLD test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-OPTIONS-TE-OBS-FOLD` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §5.2](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) | -| **Requirement** | MUST | -| **Expected** | `400` or `2xx` + close | - -## What it sends - -A two-step sequence: `OPTIONS` with folded `Transfer-Encoding` plus `Content-Length`, then a follow-up `GET`. - -```http -OPTIONS / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding:\r\n - chunked\r\n -Content-Length: 5\r\n -\r\n -hello - -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -## What the RFC says - -> "A server that receives an obs-fold in a request message ... MUST either reject the message by sending a 400 (Bad Request) ... or replace each received obs-fold with one or more SP octets." -- RFC 9112 Section 5.2 - -If unfolded to `Transfer-Encoding: chunked` while `Content-Length` is also present, connection safety rules still apply. - -## Partial Coverage Note - -Existing test `SMUG-TE-OBS-FOLD` already covers this grammar issue in a single request. This variant exercises the `OPTIONS` method path and verifies follow-up connection handling. - -## Why it matters - -Method-specific parser branches can bypass generic TE validation. This is the class highlighted by recent OPTIONS+obs-fold smuggling disclosures. - -## Sources - -- [RFC 9112 §5.2](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) +--- +title: "OPTIONS TE Obs Fold — Request Smuggling" +description: "A two-step sequence: OPTIONS with folded Transfer-Encoding plus Content-Length, then a follow-up GET. Tested against RFC 9112 §5.2." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-OPTIONS-TE-OBS-FOLD` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §5.2](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) | +| **Requirement** | MUST | +| **Expected** | `400` or `2xx` + close | + +## What it sends + +A two-step sequence: `OPTIONS` with folded `Transfer-Encoding` plus `Content-Length`, then a follow-up `GET`. + +```http +OPTIONS / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding:\r\n + chunked\r\n +Content-Length: 5\r\n +\r\n +hello + +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +## What the RFC says + +> "A server that receives an obs-fold in a request message ... MUST either reject the message by sending a 400 (Bad Request) ... or replace each received obs-fold with one or more SP octets." -- RFC 9112 Section 5.2 + +If unfolded to `Transfer-Encoding: chunked` while `Content-Length` is also present, connection safety rules still apply. + +## Partial Coverage Note + +Existing test `SMUG-TE-OBS-FOLD` already covers this grammar issue in a single request. This variant exercises the `OPTIONS` method path and verifies follow-up connection handling. + +## Why it matters + +Method-specific parser branches can bypass generic TE validation. This is the class highlighted by recent OPTIONS+obs-fold smuggling disclosures. + +## Sources + +- [RFC 9112 §5.2](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) diff --git a/docs/content/docs/smuggling/pipeline-safe.md b/docs/content/docs/smuggling/pipeline-safe.md index 04fc8c6..801c212 100644 --- a/docs/content/docs/smuggling/pipeline-safe.md +++ b/docs/content/docs/smuggling/pipeline-safe.md @@ -1,55 +1,55 @@ ---- -title: "PIPELINE-SAFE" -description: "PIPELINE-SAFE baseline sequence test documentation" -weight: 15 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-PIPELINE-SAFE` | -| **Category** | Smuggling | -| **Type** | Sequence (2 steps) | -| **Scored** | No | -| **RFC** | [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) | -| **RFC Level** | SHOULD | -| **Expected** | `2xx` + `2xx` | - -## What it does - -This is a **baseline sequence test** — it sends two clean, unambiguous requests on the same keep-alive connection to verify the server supports normal HTTP/1.1 pipelining. - -### Step 1: First GET - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -### Step 2: Second GET - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -Both requests are identical, clean, and unambiguous. No smuggling payload. - -## What the RFC says - -> "A client that supports persistent connections MAY 'pipeline' its requests (i.e., send multiple requests without waiting for each response). A server MAY process a sequence of pipelined requests in parallel if they all have safe methods." — RFC 9112 §9.3 - -## Why it matters - -This test serves as a **control** for the other sequence tests. If a server can't handle two clean GETs on one connection, the results of desync and MUST-close tests are unreliable — failures could be caused by missing pipelining support rather than smuggling vulnerabilities. - -## Verdicts - -- **Pass** — Both steps return `2xx` (pipelining works correctly) -- **Warn** — Step 1 returns `2xx` but server closes connection before step 2 (no pipelining support) -- **Fail** — Step 1 does not return `2xx` - -## Sources - -- [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) +--- +title: "Pipeline Safe — Request Smuggling" +description: "This is a baseline sequence test — it sends two clean, unambiguous requests on the same keep-alive connection to verify the server supports normal HTTP/1.1 pipelining." +weight: 15 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-PIPELINE-SAFE` | +| **Category** | Smuggling | +| **Type** | Sequence (2 steps) | +| **Scored** | No | +| **RFC** | [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) | +| **RFC Level** | SHOULD | +| **Expected** | `2xx` + `2xx` | + +## What it does + +This is a **baseline sequence test** — it sends two clean, unambiguous requests on the same keep-alive connection to verify the server supports normal HTTP/1.1 pipelining. + +### Step 1: First GET + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +### Step 2: Second GET + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +Both requests are identical, clean, and unambiguous. No smuggling payload. + +## What the RFC says + +> "A client that supports persistent connections MAY 'pipeline' its requests (i.e., send multiple requests without waiting for each response). A server MAY process a sequence of pipelined requests in parallel if they all have safe methods." — RFC 9112 §9.3 + +## Why it matters + +This test serves as a **control** for the other sequence tests. If a server can't handle two clean GETs on one connection, the results of desync and MUST-close tests are unreliable — failures could be caused by missing pipelining support rather than smuggling vulnerabilities. + +## Verdicts + +- **Pass** — Both steps return `2xx` (pipelining works correctly) +- **Warn** — Step 1 returns `2xx` but server closes connection before step 2 (no pipelining support) +- **Fail** — Step 1 does not return `2xx` + +## Sources + +- [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) diff --git a/docs/content/docs/smuggling/te-case-mismatch.md b/docs/content/docs/smuggling/te-case-mismatch.md index ab11805..38f7fce 100644 --- a/docs/content/docs/smuggling/te-case-mismatch.md +++ b/docs/content/docs/smuggling/te-case-mismatch.md @@ -1,92 +1,92 @@ ---- -title: "TE-CASE-MISMATCH" -description: "TE-CASE-MISMATCH test documentation" -weight: 14 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TE-CASE-MISMATCH` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **Requirement** | SHOULD | -| **Expected** | `400` or `2xx` | - -## What it sends - -`Transfer-Encoding: Chunked` — capital `C` instead of lowercase. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: Chunked\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -Note `Chunked` with a capital C instead of `chunked`. - - -## What the RFC says - -> "All transfer-coding names are case-insensitive and ought to be registered within the HTTP Transfer Coding registry." — RFC 9112 Section 7 - -> "A recipient MUST be able to parse the chunked transfer coding (Section 7.1) because it plays a crucial role in framing messages when the content size is not known in advance." — RFC 9112 Section 6.1 - -Recognizing `Chunked` as `chunked` is correct, RFC-compliant behavior. A server that treats transfer coding names as case-sensitive may reject or misinterpret the header, creating a potential CL/TE desync when Content-Length is also present. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (RFC-valid case-insensitive matching). - -## Why it matters - -Because transfer coding names are explicitly case-insensitive, both `400` and `2xx` are defensible responses. A server that rejects the request is being overly strict but safe. A server that accepts it is following the RFC. However, rejecting is preferred because case-insensitive matching combined with Content-Length creates a smuggling risk. - -## Deep Analysis - -### ABNF - -The Transfer-Encoding header and token grammar are defined as follows: - -``` -Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 -transfer-coding = token ; RFC 9110 §10.1.4 -token = 1*tchar ; RFC 9110 §5.6.2 -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" - / DIGIT / ALPHA -``` - -### RFC Evidence - -> "All transfer-coding names are case-insensitive and ought to be registered within the HTTP Transfer Coding registry." -- RFC 9112 §7 - -> "A recipient MUST be able to parse the chunked transfer coding (Section 7.1) because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 §6.1 - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 - -### Chain of Reasoning - -1. The test sends `Transfer-Encoding: Chunked` with a capital `C` instead of the lowercase `chunked`. -2. RFC 9112 section 7 explicitly states that all transfer-coding names are **case-insensitive**. This means `Chunked`, `CHUNKED`, and `chunked` are all equivalent per the specification. -3. RFC 9112 section 6.1 requires that a recipient MUST be able to parse the chunked transfer coding. A case-insensitive comparison satisfies this requirement regardless of capitalization. -4. The request also includes `Content-Length: 5`, creating a CL/TE dual-header scenario. Under RFC 9112 section 6.1, the server MAY reject such a request or process it using Transfer-Encoding alone, but MUST close the connection afterward. -5. The smuggling risk arises when a front-end parser performs case-sensitive matching and does not recognize `Chunked` as a valid transfer coding, falling back to Content-Length framing. If a back-end parser correctly performs case-insensitive matching and uses chunked framing, the two parsers disagree on where the message body ends -- enabling request smuggling. - -### Scored / SHOULD Justification - -This test is scored as **SHOULD** reject because, while the RFC explicitly declares transfer-coding names case-insensitive, the combination with Content-Length creates a smuggling risk. A server that rejects `Chunked` with `400` is being strict but safe. A server that accepts it and processes chunked encoding is following the RFC correctly. Neither behavior violates a MUST-level requirement, but rejection is preferred. - -- **Pass (400):** Strict rejection prevents any parser disagreement with intermediaries. -- **Warn (2xx):** RFC-compliant case-insensitive matching; the server correctly recognized the encoding. - -### Smuggling Attack Scenarios - -- **CL/TE Desync via Case Sensitivity:** An attacker sends `Transfer-Encoding: Chunked` with `Content-Length: 5`. A case-sensitive front-end proxy does not recognize `Chunked` and routes based on Content-Length. The back-end server performs case-insensitive matching, switches to chunked framing, and interprets the body differently. The attacker can embed a second request inside the chunked body that the front-end never sees. -- **Reverse Proxy Bypass:** Some WAFs or reverse proxies check for `Transfer-Encoding: chunked` using exact string matching. Sending `Chunked` or `CHUNKED` bypasses the check while the origin server still processes chunked encoding, allowing smuggled payloads to reach the back-end unfiltered. - -## Sources - -- [RFC 9112 §7](https://www.rfc-editor.org/rfc/rfc9112#section-7) -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "TE Case Mismatch — Request Smuggling" +description: "Transfer-Encoding: Chunked — capital C instead of lowercase. Tested against RFC 9112 §6.1." +weight: 14 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-CASE-MISMATCH` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **Requirement** | SHOULD | +| **Expected** | `400` or `2xx` | + +## What it sends + +`Transfer-Encoding: Chunked` — capital `C` instead of lowercase. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: Chunked\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +Note `Chunked` with a capital C instead of `chunked`. + + +## What the RFC says + +> "All transfer-coding names are case-insensitive and ought to be registered within the HTTP Transfer Coding registry." — RFC 9112 Section 7 + +> "A recipient MUST be able to parse the chunked transfer coding (Section 7.1) because it plays a crucial role in framing messages when the content size is not known in advance." — RFC 9112 Section 6.1 + +Recognizing `Chunked` as `chunked` is correct, RFC-compliant behavior. A server that treats transfer coding names as case-sensitive may reject or misinterpret the header, creating a potential CL/TE desync when Content-Length is also present. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (RFC-valid case-insensitive matching). + +## Why it matters + +Because transfer coding names are explicitly case-insensitive, both `400` and `2xx` are defensible responses. A server that rejects the request is being overly strict but safe. A server that accepts it is following the RFC. However, rejecting is preferred because case-insensitive matching combined with Content-Length creates a smuggling risk. + +## Deep Analysis + +### ABNF + +The Transfer-Encoding header and token grammar are defined as follows: + +``` +Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 +transfer-coding = token ; RFC 9110 §10.1.4 +token = 1*tchar ; RFC 9110 §5.6.2 +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + / DIGIT / ALPHA +``` + +### RFC Evidence + +> "All transfer-coding names are case-insensitive and ought to be registered within the HTTP Transfer Coding registry." -- RFC 9112 §7 + +> "A recipient MUST be able to parse the chunked transfer coding (Section 7.1) because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 §6.1 + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 + +### Chain of Reasoning + +1. The test sends `Transfer-Encoding: Chunked` with a capital `C` instead of the lowercase `chunked`. +2. RFC 9112 section 7 explicitly states that all transfer-coding names are **case-insensitive**. This means `Chunked`, `CHUNKED`, and `chunked` are all equivalent per the specification. +3. RFC 9112 section 6.1 requires that a recipient MUST be able to parse the chunked transfer coding. A case-insensitive comparison satisfies this requirement regardless of capitalization. +4. The request also includes `Content-Length: 5`, creating a CL/TE dual-header scenario. Under RFC 9112 section 6.1, the server MAY reject such a request or process it using Transfer-Encoding alone, but MUST close the connection afterward. +5. The smuggling risk arises when a front-end parser performs case-sensitive matching and does not recognize `Chunked` as a valid transfer coding, falling back to Content-Length framing. If a back-end parser correctly performs case-insensitive matching and uses chunked framing, the two parsers disagree on where the message body ends -- enabling request smuggling. + +### Scored / SHOULD Justification + +This test is scored as **SHOULD** reject because, while the RFC explicitly declares transfer-coding names case-insensitive, the combination with Content-Length creates a smuggling risk. A server that rejects `Chunked` with `400` is being strict but safe. A server that accepts it and processes chunked encoding is following the RFC correctly. Neither behavior violates a MUST-level requirement, but rejection is preferred. + +- **Pass (400):** Strict rejection prevents any parser disagreement with intermediaries. +- **Warn (2xx):** RFC-compliant case-insensitive matching; the server correctly recognized the encoding. + +### Smuggling Attack Scenarios + +- **CL/TE Desync via Case Sensitivity:** An attacker sends `Transfer-Encoding: Chunked` with `Content-Length: 5`. A case-sensitive front-end proxy does not recognize `Chunked` and routes based on Content-Length. The back-end server performs case-insensitive matching, switches to chunked framing, and interprets the body differently. The attacker can embed a second request inside the chunked body that the front-end never sees. +- **Reverse Proxy Bypass:** Some WAFs or reverse proxies check for `Transfer-Encoding: chunked` using exact string matching. Sending `Chunked` or `CHUNKED` bypasses the check while the origin server still processes chunked encoding, allowing smuggled payloads to reach the back-end unfiltered. + +## Sources + +- [RFC 9112 §7](https://www.rfc-editor.org/rfc/rfc9112#section-7) +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/te-double-chunked.md b/docs/content/docs/smuggling/te-double-chunked.md index fb6ebc7..9edc060 100644 --- a/docs/content/docs/smuggling/te-double-chunked.md +++ b/docs/content/docs/smuggling/te-double-chunked.md @@ -1,94 +1,94 @@ ---- -title: "TE-DOUBLE-CHUNKED" -description: "TE-DOUBLE-CHUNKED test documentation" -weight: 13 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TE-DOUBLE-CHUNKED` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **Requirement** | SHOULD | -| **Expected** | `400` or `2xx` | - -## What it sends - -`Transfer-Encoding: chunked, chunked` — duplicate `chunked` encoding with a Content-Length header also present. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked, chunked\r\n -Content-Length: 5\r\n -\r\n -hello -``` - - -## What the RFC says - -> "A sender MUST NOT apply the chunked transfer coding more than once to a message body." — RFC 9112 Section 6.1 - -> "If any transfer coding other than chunked is applied to a request's content, the sender MUST apply chunked as the final transfer coding." — RFC 9112 Section 6.1 - -The sender violates the MUST NOT rule by listing `chunked` twice. However, the server's obligation is to parse the Transfer-Encoding it receives, and a server that sees `chunked, chunked` might reasonably process it as a single `chunked` application or reject it. - -## Pass / Warn - -While the sender clearly violates RFC 9112 Section 6.1 by applying chunked twice, the RFC does not specify a mandatory server response for this case. The server may reject with `400` (strict) or deduplicate and process normally (lenient). Both behaviors are defensible since the MUST NOT applies to senders, not to how receivers handle the violation. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (lenient, deduplicates chunked). - -## Deep Analysis - -### ABNF - -The Transfer-Encoding header uses the list syntax (`#rule`): - -``` -Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 -transfer-coding = token ; RFC 9110 §10.1.4 -token = 1*tchar ; RFC 9110 §5.6.2 -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" - / DIGIT / ALPHA -``` - -The `#` construct means a comma-separated list of transfer-coding tokens. The value `chunked, chunked` is a syntactically valid list of two elements that both resolve to the same transfer coding. - -### RFC Evidence - -> "A sender MUST NOT apply the chunked transfer coding more than once to a message body (i.e., chunking an already chunked message is not allowed)." -- RFC 9112 §6.1 - -> "If any transfer coding other than chunked is applied to a request's content, the sender MUST apply chunked as the final transfer coding to ensure that the message is properly framed." -- RFC 9112 §6.1 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 - -### Chain of Reasoning - -1. The test sends `Transfer-Encoding: chunked, chunked` alongside `Content-Length: 5`. -2. RFC 9112 section 6.1 states that a sender MUST NOT apply the chunked transfer coding more than once. The value `chunked, chunked` declares chunked applied twice, directly violating this sender-side requirement. -3. However, this MUST NOT is directed at the **sender**, not the receiver. The RFC does not prescribe a specific receiver behavior when encountering a duplicated chunked coding. -4. A strict server may reject with `400` because the request is malformed per the sender rules. A lenient server may deduplicate and process a single chunked decoding, which is also defensible. -5. The presence of `Content-Length: 5` alongside Transfer-Encoding creates the dual-header smuggling setup described in RFC 9112 section 6.3. -6. Some servers may attempt to apply chunked decoding twice (chunked-within-chunked), leading to unpredictable behavior and potential parser confusion. - -### Scored / SHOULD Justification - -This test is scored at the **SHOULD** level because the MUST NOT in RFC 9112 section 6.1 applies to the sender, not to how receivers should handle the violation. No MUST-level requirement dictates a specific server response when `chunked` appears twice, but rejection is the safer behavior. - -- **Pass (400):** The server rejects the malformed request, which is the safest response. -- **Warn (2xx):** The server deduplicates the encoding and processes normally, which is lenient but not a specification violation. - -### Smuggling Attack Scenarios - -- **Double-Chunked Confusion:** A front-end proxy sees `chunked, chunked` and applies chunked decoding once, then forwards the partially-decoded body. The back-end sees the forwarded data and may attempt to apply chunked decoding again, interpreting attacker-controlled data as chunk boundaries and enabling request smuggling. -- **Deduplication Disagreement:** A front-end deduplicates the list to a single `chunked` and uses chunked framing. A back-end that does not recognize the duplicate falls back to Content-Length. The parsers disagree on body boundaries, allowing the attacker to inject a second request. -- **Parser State Corruption:** Some HTTP libraries track encoding layers in a stack. Pushing `chunked` twice may corrupt internal state, causing the parser to miscount body boundaries or skip the terminating zero-length chunk. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "TE Double Chunked — Request Smuggling" +description: "Transfer-Encoding: chunked, chunked — duplicate chunked encoding with a Content-Length header also present. Tested against RFC 9112 §6.1." +weight: 13 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-DOUBLE-CHUNKED` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **Requirement** | SHOULD | +| **Expected** | `400` or `2xx` | + +## What it sends + +`Transfer-Encoding: chunked, chunked` — duplicate `chunked` encoding with a Content-Length header also present. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked, chunked\r\n +Content-Length: 5\r\n +\r\n +hello +``` + + +## What the RFC says + +> "A sender MUST NOT apply the chunked transfer coding more than once to a message body." — RFC 9112 Section 6.1 + +> "If any transfer coding other than chunked is applied to a request's content, the sender MUST apply chunked as the final transfer coding." — RFC 9112 Section 6.1 + +The sender violates the MUST NOT rule by listing `chunked` twice. However, the server's obligation is to parse the Transfer-Encoding it receives, and a server that sees `chunked, chunked` might reasonably process it as a single `chunked` application or reject it. + +## Pass / Warn + +While the sender clearly violates RFC 9112 Section 6.1 by applying chunked twice, the RFC does not specify a mandatory server response for this case. The server may reject with `400` (strict) or deduplicate and process normally (lenient). Both behaviors are defensible since the MUST NOT applies to senders, not to how receivers handle the violation. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (lenient, deduplicates chunked). + +## Deep Analysis + +### ABNF + +The Transfer-Encoding header uses the list syntax (`#rule`): + +``` +Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 +transfer-coding = token ; RFC 9110 §10.1.4 +token = 1*tchar ; RFC 9110 §5.6.2 +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + / DIGIT / ALPHA +``` + +The `#` construct means a comma-separated list of transfer-coding tokens. The value `chunked, chunked` is a syntactically valid list of two elements that both resolve to the same transfer coding. + +### RFC Evidence + +> "A sender MUST NOT apply the chunked transfer coding more than once to a message body (i.e., chunking an already chunked message is not allowed)." -- RFC 9112 §6.1 + +> "If any transfer coding other than chunked is applied to a request's content, the sender MUST apply chunked as the final transfer coding to ensure that the message is properly framed." -- RFC 9112 §6.1 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 + +### Chain of Reasoning + +1. The test sends `Transfer-Encoding: chunked, chunked` alongside `Content-Length: 5`. +2. RFC 9112 section 6.1 states that a sender MUST NOT apply the chunked transfer coding more than once. The value `chunked, chunked` declares chunked applied twice, directly violating this sender-side requirement. +3. However, this MUST NOT is directed at the **sender**, not the receiver. The RFC does not prescribe a specific receiver behavior when encountering a duplicated chunked coding. +4. A strict server may reject with `400` because the request is malformed per the sender rules. A lenient server may deduplicate and process a single chunked decoding, which is also defensible. +5. The presence of `Content-Length: 5` alongside Transfer-Encoding creates the dual-header smuggling setup described in RFC 9112 section 6.3. +6. Some servers may attempt to apply chunked decoding twice (chunked-within-chunked), leading to unpredictable behavior and potential parser confusion. + +### Scored / SHOULD Justification + +This test is scored at the **SHOULD** level because the MUST NOT in RFC 9112 section 6.1 applies to the sender, not to how receivers should handle the violation. No MUST-level requirement dictates a specific server response when `chunked` appears twice, but rejection is the safer behavior. + +- **Pass (400):** The server rejects the malformed request, which is the safest response. +- **Warn (2xx):** The server deduplicates the encoding and processes normally, which is lenient but not a specification violation. + +### Smuggling Attack Scenarios + +- **Double-Chunked Confusion:** A front-end proxy sees `chunked, chunked` and applies chunked decoding once, then forwards the partially-decoded body. The back-end sees the forwarded data and may attempt to apply chunked decoding again, interpreting attacker-controlled data as chunk boundaries and enabling request smuggling. +- **Deduplication Disagreement:** A front-end deduplicates the list to a single `chunked` and uses chunked framing. A back-end that does not recognize the duplicate falls back to Content-Length. The parsers disagree on body boundaries, allowing the attacker to inject a second request. +- **Parser State Corruption:** Some HTTP libraries track encoding layers in a stack. Pushing `chunked` twice may corrupt internal state, causing the parser to miscount body boundaries or skip the terminating zero-length chunk. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/te-duplicate-headers-smuggled-get.md b/docs/content/docs/smuggling/te-duplicate-headers-smuggled-get.md index 16c0ab5..e0f720b 100644 --- a/docs/content/docs/smuggling/te-duplicate-headers-smuggled-get.md +++ b/docs/content/docs/smuggling/te-duplicate-headers-smuggled-get.md @@ -1,64 +1,64 @@ ---- -title: "TE-DUPLICATE-HEADERS-SMUGGLED-GET" -description: "TE-DUPLICATE-HEADERS-SMUGGLED-GET sequence test documentation" -weight: 22 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TE-DUPLICATE-HEADERS-SMUGGLED-GET` | -| **Category** | Smuggling | -| **Type** | Sequence (single send) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or connection close | - -## What it does - -This is a TE.TE + CL ambiguity variant of `SMUG-CLTE-SMUGGLED-GET`. - -It sends two `Transfer-Encoding` header fields with different values (`chunked` and `identity`) plus a `Content-Length`, and embeds a full `GET /` request after the chunked terminator. If the server keeps the connection reusable and the embedded request is executed, the probe will observe multiple HTTP status lines after a single client send. - -## What it sends - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -Transfer-Encoding: identity\r\n -Content-Length: 45\r\n -\r\n -0\r\n -\r\n -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -(Actual `Content-Length` is computed to match the body bytes.) - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... Regardless, the server MUST close the connection after responding to such a request." -- RFC 9112 §6.1 - -## Why it matters - -Request smuggling often relies on parsing disagreements about: - -- whether duplicate TE header fields are merged, rejected, or one is ignored -- whether CL is honored in the presence of TE -- whether the connection is kept open after an ambiguous request - -This test looks for concrete evidence of request-boundary confusion by detecting multiple HTTP responses produced from a single client send. In a proxy chain, this same behavior is what enables request smuggling. - -## Verdicts - -- **Pass**: The server rejects with `400`, or closes the connection. -- **Fail**: Multiple HTTP status lines are observed (embedded `GET` likely executed). -- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [HTTP Request Smuggling (PortSwigger)](https://portswigger.net/web-security/request-smuggling) +--- +title: "TE Duplicate Headers Smuggled GET — Request Smuggling" +description: "This is a TE.TE + CL ambiguity variant of SMUG-CLTE-SMUGGLED-GET. It sends two Transfer-Encoding header fields with different values (chunked and identity) plus a Content-Length, and embeds a…" +weight: 22 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-DUPLICATE-HEADERS-SMUGGLED-GET` | +| **Category** | Smuggling | +| **Type** | Sequence (single send) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or connection close | + +## What it does + +This is a TE.TE + CL ambiguity variant of `SMUG-CLTE-SMUGGLED-GET`. + +It sends two `Transfer-Encoding` header fields with different values (`chunked` and `identity`) plus a `Content-Length`, and embeds a full `GET /` request after the chunked terminator. If the server keeps the connection reusable and the embedded request is executed, the probe will observe multiple HTTP status lines after a single client send. + +## What it sends + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +Transfer-Encoding: identity\r\n +Content-Length: 45\r\n +\r\n +0\r\n +\r\n +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +(Actual `Content-Length` is computed to match the body bytes.) + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... Regardless, the server MUST close the connection after responding to such a request." -- RFC 9112 §6.1 + +## Why it matters + +Request smuggling often relies on parsing disagreements about: + +- whether duplicate TE header fields are merged, rejected, or one is ignored +- whether CL is honored in the presence of TE +- whether the connection is kept open after an ambiguous request + +This test looks for concrete evidence of request-boundary confusion by detecting multiple HTTP responses produced from a single client send. In a proxy chain, this same behavior is what enables request smuggling. + +## Verdicts + +- **Pass**: The server rejects with `400`, or closes the connection. +- **Fail**: Multiple HTTP status lines are observed (embedded `GET` likely executed). +- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [HTTP Request Smuggling (PortSwigger)](https://portswigger.net/web-security/request-smuggling) diff --git a/docs/content/docs/smuggling/te-duplicate-headers.md b/docs/content/docs/smuggling/te-duplicate-headers.md index 26571fd..0fac69c 100644 --- a/docs/content/docs/smuggling/te-duplicate-headers.md +++ b/docs/content/docs/smuggling/te-duplicate-headers.md @@ -1,92 +1,92 @@ ---- -title: "TE-DUPLICATE-HEADERS" -description: "TE-DUPLICATE-HEADERS test documentation" -weight: 24 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TE-DUPLICATE-HEADERS` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 Section 6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -Two TE headers (`chunked` and `identity`) plus Content-Length. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -Transfer-Encoding: identity\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -Two separate `Transfer-Encoding` headers with different values, plus a `Content-Length`. - - -## What the RFC says - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 Section 6.3 - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 Section 6.1 - -Two separate `Transfer-Encoding` headers with conflicting values (`chunked` and `identity`) create an additional layer of ambiguity -- different servers may pick different TE header values to determine framing. Combined with a Content-Length header, this is a textbook smuggling setup. - -## Why it matters - -When two TE headers carry different values, one parser may use `chunked` framing while another falls through to `identity` (and then to Content-Length). This disagreement on body length is the core of CL/TE request smuggling. - -## Deep Analysis - -### ABNF - -``` -Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 -transfer-coding = token ; RFC 9110 §10.1.4 -token = 1*tchar ; RFC 9110 §5.6.2 -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" - / DIGIT / ALPHA -``` - -The `#` rule means Transfer-Encoding is a comma-separated list. When multiple header lines share the same field name, they are semantically equivalent to a single line with values joined by commas. Two separate `Transfer-Encoding` lines with `chunked` and `identity` are equivalent to `Transfer-Encoding: chunked, identity`. - -### RFC Evidence - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 - -> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 §6.1 - -### Chain of Reasoning - -1. The test sends two separate `Transfer-Encoding` header lines: one with `chunked` and one with `identity`. A `Content-Length: 5` header is also present. -2. Per HTTP semantics, multiple header lines with the same field name are combined as a comma-separated list. The effective value is `Transfer-Encoding: chunked, identity`. -3. The `identity` transfer coding was removed from the registry in RFC 7230 and is absent from RFC 9112. It is an unrecognized transfer coding. -4. With `chunked` not as the final encoding (it appears before `identity`), RFC 9112 section 6.3 requires: "the server MUST respond with the 400 (Bad Request) status code and then close the connection." -5. Additionally, the presence of both Transfer-Encoding and Content-Length triggers the smuggling warning in RFC 9112 section 6.3, which states such a message "ought to be handled as an error." -6. The combination of an unrecognized coding, chunked not being final, and a conflicting Content-Length makes this a triple-layered ambiguity. - -### Scored / Unscored Justification - -This test is **scored** (MUST reject). Two separate Transfer-Encoding headers with conflicting values create a scenario where `chunked` is not the final encoding. RFC 9112 section 6.3 uses MUST-level language requiring a `400` response when chunked is not final. The additional presence of Content-Length reinforces that this ought to be treated as an error. - -- **Pass (400 or close):** The server correctly rejects the ambiguous framing. -- **Fail (2xx):** The server accepted a request with irreconcilable framing signals, violating MUST-level requirements. - -### Smuggling Attack Scenarios - -- **Header Precedence Disagreement:** A front-end proxy may process only the first `Transfer-Encoding: chunked` header, using chunked framing. A back-end may combine both headers and see `chunked, identity`, treating it as an unrecognized encoding and falling back to Content-Length. This framing disagreement enables CL/TE desync. -- **Last-Header-Wins vs. First-Header-Wins:** Different HTTP implementations follow different strategies when encountering duplicate headers. A front-end using "first wins" sees `chunked`; a back-end using "last wins" sees `identity` (an unrecognized coding) and may fall back to Content-Length. The attacker controls which framing each parser uses. -- **Selective Header Forwarding:** Some proxies forward only the first instance of a repeated header. If the front-end forwards only `Transfer-Encoding: chunked` but the back-end originally received both, the proxy's normalization creates a mismatch with the original Content-Length, enabling body-boundary manipulation. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "TE Duplicate Headers — Request Smuggling" +description: "Two TE headers (chunked and identity) plus Content-Length. Tested against RFC 9112 Section 6.1." +weight: 24 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-DUPLICATE-HEADERS` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 Section 6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +Two TE headers (`chunked` and `identity`) plus Content-Length. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +Transfer-Encoding: identity\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +Two separate `Transfer-Encoding` headers with different values, plus a `Content-Length`. + + +## What the RFC says + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 Section 6.3 + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 Section 6.1 + +Two separate `Transfer-Encoding` headers with conflicting values (`chunked` and `identity`) create an additional layer of ambiguity -- different servers may pick different TE header values to determine framing. Combined with a Content-Length header, this is a textbook smuggling setup. + +## Why it matters + +When two TE headers carry different values, one parser may use `chunked` framing while another falls through to `identity` (and then to Content-Length). This disagreement on body length is the core of CL/TE request smuggling. + +## Deep Analysis + +### ABNF + +``` +Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 +transfer-coding = token ; RFC 9110 §10.1.4 +token = 1*tchar ; RFC 9110 §5.6.2 +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + / DIGIT / ALPHA +``` + +The `#` rule means Transfer-Encoding is a comma-separated list. When multiple header lines share the same field name, they are semantically equivalent to a single line with values joined by commas. Two separate `Transfer-Encoding` lines with `chunked` and `identity` are equivalent to `Transfer-Encoding: chunked, identity`. + +### RFC Evidence + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 + +> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 §6.1 + +### Chain of Reasoning + +1. The test sends two separate `Transfer-Encoding` header lines: one with `chunked` and one with `identity`. A `Content-Length: 5` header is also present. +2. Per HTTP semantics, multiple header lines with the same field name are combined as a comma-separated list. The effective value is `Transfer-Encoding: chunked, identity`. +3. The `identity` transfer coding was removed from the registry in RFC 7230 and is absent from RFC 9112. It is an unrecognized transfer coding. +4. With `chunked` not as the final encoding (it appears before `identity`), RFC 9112 section 6.3 requires: "the server MUST respond with the 400 (Bad Request) status code and then close the connection." +5. Additionally, the presence of both Transfer-Encoding and Content-Length triggers the smuggling warning in RFC 9112 section 6.3, which states such a message "ought to be handled as an error." +6. The combination of an unrecognized coding, chunked not being final, and a conflicting Content-Length makes this a triple-layered ambiguity. + +### Scored / Unscored Justification + +This test is **scored** (MUST reject). Two separate Transfer-Encoding headers with conflicting values create a scenario where `chunked` is not the final encoding. RFC 9112 section 6.3 uses MUST-level language requiring a `400` response when chunked is not final. The additional presence of Content-Length reinforces that this ought to be treated as an error. + +- **Pass (400 or close):** The server correctly rejects the ambiguous framing. +- **Fail (2xx):** The server accepted a request with irreconcilable framing signals, violating MUST-level requirements. + +### Smuggling Attack Scenarios + +- **Header Precedence Disagreement:** A front-end proxy may process only the first `Transfer-Encoding: chunked` header, using chunked framing. A back-end may combine both headers and see `chunked, identity`, treating it as an unrecognized encoding and falling back to Content-Length. This framing disagreement enables CL/TE desync. +- **Last-Header-Wins vs. First-Header-Wins:** Different HTTP implementations follow different strategies when encountering duplicate headers. A front-end using "first wins" sees `chunked`; a back-end using "last wins" sees `identity` (an unrecognized coding) and may fall back to Content-Length. The attacker controls which framing each parser uses. +- **Selective Header Forwarding:** Some proxies forward only the first instance of a repeated header. If the front-end forwards only `Transfer-Encoding: chunked` but the back-end originally received both, the proxy's normalization creates a mismatch with the original Content-Length, enabling body-boundary manipulation. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/te-empty-value.md b/docs/content/docs/smuggling/te-empty-value.md index 5753e7e..8396f96 100644 --- a/docs/content/docs/smuggling/te-empty-value.md +++ b/docs/content/docs/smuggling/te-empty-value.md @@ -1,89 +1,89 @@ ---- -title: "TE-EMPTY-VALUE" -description: "TE-EMPTY-VALUE test documentation" -weight: 22 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TE-EMPTY-VALUE` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 Section 6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -`Transfer-Encoding: ` (empty value) with `Content-Length: 5`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: \r\n -Content-Length: 5\r\n -\r\n -hello -``` - -The `Transfer-Encoding` header has an empty value. - - -## What the RFC says - -> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 Section 6.1 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 Section 6.3 - -An empty Transfer-Encoding value contains no valid transfer coding name. The header is syntactically present but semantically empty, creating a framing ambiguity: should the server treat Transfer-Encoding as present (overriding Content-Length) or absent (falling back to Content-Length)? - -## Why it matters - -If a front-end sees Transfer-Encoding as present and ignores Content-Length, but a back-end sees an empty value and falls back to Content-Length, they will disagree on body framing -- a direct smuggling vector. - -## Deep Analysis - -### ABNF - -``` -Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 -transfer-coding = token ; RFC 9110 §10.1.4 -token = 1*tchar ; RFC 9110 §5.6.2 -field-value = *field-content ; RFC 9110 §5.5 -``` - -The `token` rule requires **at least one** `tchar` character (`1*tchar`). An empty string contains zero `tchar` characters and therefore does not match the `token` production. The `#` list rule (`#transfer-coding`) permits empty list elements (which should be ignored), but an entirely empty value contains no valid elements at all. - -### RFC Evidence - -> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 §6.1 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 - -### Chain of Reasoning - -1. The test sends `Transfer-Encoding: ` (empty value) alongside `Content-Length: 5`. -2. An empty Transfer-Encoding value contains no valid `token` per the ABNF. The header is syntactically present but semantically void -- it declares a transfer coding without naming one. -3. The critical ambiguity: is Transfer-Encoding "present" or "absent"? If present, RFC 9112 section 6.3 says it overrides Content-Length. If absent (because the value is empty), Content-Length governs framing. -4. RFC 9112 section 6.3 states that receiving both Transfer-Encoding and Content-Length "ought to be handled as an error." The Transfer-Encoding header is syntactically present regardless of whether its value is empty. -5. RFC 9112 section 6.1 further requires the server to close the connection after responding to a request with both headers, regardless of whether it rejects or processes the request. -6. The empty value is not a recognized transfer coding, so the SHOULD-level guidance to respond with `501 (Not Implemented)` also applies. - -### Scored / Unscored Justification - -This test is **scored** (MUST reject). The Transfer-Encoding header is syntactically present, triggering the CL/TE dual-header rules of RFC 9112 section 6.1 and 6.3. The server MUST at minimum close the connection. The empty value provides no valid framing mechanism, making the message body length indeterminate -- a condition that demands rejection for safety. - -- **Pass (400 or close):** The server correctly rejects the ambiguous request. -- **Fail (2xx):** The server silently accepted a request with indeterminate framing, creating a smuggling-exploitable condition. - -### Smuggling Attack Scenarios - -- **Presence vs. Value Disagreement:** A front-end proxy checks for the Transfer-Encoding header's **presence** and, finding it, ignores Content-Length per RFC 9112 section 6.3. But with an empty value, it cannot apply any transfer decoding and may stall or error. A back-end that evaluates the **value** finds it empty, treats Transfer-Encoding as absent, and uses Content-Length framing. The desync between presence-based and value-based logic allows body boundary manipulation. -- **Header Stripping Bypass:** Some proxies strip empty-valued headers during normalization. If the front-end strips `Transfer-Encoding: ` and forwards only `Content-Length: 5`, but the back-end receives the original request (e.g., via connection reuse), the back-end sees Transfer-Encoding as present and ignores Content-Length. The attacker can embed a smuggled request in the body that only the back-end parses. -- **Fallback Framing Exploit:** A parser that sees an empty Transfer-Encoding may fall through to Content-Length framing. Another parser that treats the header as present but unrecognizable may respond with an error or close the connection. This inconsistency in fallback behavior can be exploited to desynchronize request boundaries across a proxy chain. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "TE Empty Value — Request Smuggling" +description: "Transfer-Encoding: (empty value) with Content-Length: 5. Tested against RFC 9112 Section 6.1." +weight: 22 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-EMPTY-VALUE` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 Section 6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +`Transfer-Encoding: ` (empty value) with `Content-Length: 5`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: \r\n +Content-Length: 5\r\n +\r\n +hello +``` + +The `Transfer-Encoding` header has an empty value. + + +## What the RFC says + +> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 Section 6.1 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 Section 6.3 + +An empty Transfer-Encoding value contains no valid transfer coding name. The header is syntactically present but semantically empty, creating a framing ambiguity: should the server treat Transfer-Encoding as present (overriding Content-Length) or absent (falling back to Content-Length)? + +## Why it matters + +If a front-end sees Transfer-Encoding as present and ignores Content-Length, but a back-end sees an empty value and falls back to Content-Length, they will disagree on body framing -- a direct smuggling vector. + +## Deep Analysis + +### ABNF + +``` +Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 +transfer-coding = token ; RFC 9110 §10.1.4 +token = 1*tchar ; RFC 9110 §5.6.2 +field-value = *field-content ; RFC 9110 §5.5 +``` + +The `token` rule requires **at least one** `tchar` character (`1*tchar`). An empty string contains zero `tchar` characters and therefore does not match the `token` production. The `#` list rule (`#transfer-coding`) permits empty list elements (which should be ignored), but an entirely empty value contains no valid elements at all. + +### RFC Evidence + +> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 §6.1 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 + +### Chain of Reasoning + +1. The test sends `Transfer-Encoding: ` (empty value) alongside `Content-Length: 5`. +2. An empty Transfer-Encoding value contains no valid `token` per the ABNF. The header is syntactically present but semantically void -- it declares a transfer coding without naming one. +3. The critical ambiguity: is Transfer-Encoding "present" or "absent"? If present, RFC 9112 section 6.3 says it overrides Content-Length. If absent (because the value is empty), Content-Length governs framing. +4. RFC 9112 section 6.3 states that receiving both Transfer-Encoding and Content-Length "ought to be handled as an error." The Transfer-Encoding header is syntactically present regardless of whether its value is empty. +5. RFC 9112 section 6.1 further requires the server to close the connection after responding to a request with both headers, regardless of whether it rejects or processes the request. +6. The empty value is not a recognized transfer coding, so the SHOULD-level guidance to respond with `501 (Not Implemented)` also applies. + +### Scored / Unscored Justification + +This test is **scored** (MUST reject). The Transfer-Encoding header is syntactically present, triggering the CL/TE dual-header rules of RFC 9112 section 6.1 and 6.3. The server MUST at minimum close the connection. The empty value provides no valid framing mechanism, making the message body length indeterminate -- a condition that demands rejection for safety. + +- **Pass (400 or close):** The server correctly rejects the ambiguous request. +- **Fail (2xx):** The server silently accepted a request with indeterminate framing, creating a smuggling-exploitable condition. + +### Smuggling Attack Scenarios + +- **Presence vs. Value Disagreement:** A front-end proxy checks for the Transfer-Encoding header's **presence** and, finding it, ignores Content-Length per RFC 9112 section 6.3. But with an empty value, it cannot apply any transfer decoding and may stall or error. A back-end that evaluates the **value** finds it empty, treats Transfer-Encoding as absent, and uses Content-Length framing. The desync between presence-based and value-based logic allows body boundary manipulation. +- **Header Stripping Bypass:** Some proxies strip empty-valued headers during normalization. If the front-end strips `Transfer-Encoding: ` and forwards only `Content-Length: 5`, but the back-end receives the original request (e.g., via connection reuse), the back-end sees Transfer-Encoding as present and ignores Content-Length. The attacker can embed a smuggled request in the body that only the back-end parses. +- **Fallback Framing Exploit:** A parser that sees an empty Transfer-Encoding may fall through to Content-Length framing. Another parser that treats the header as present but unrecognizable may respond with an error or close the connection. This inconsistency in fallback behavior can be exploited to desynchronize request boundaries across a proxy chain. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/te-formfeed.md b/docs/content/docs/smuggling/te-formfeed.md index 31621e7..c773aae 100644 --- a/docs/content/docs/smuggling/te-formfeed.md +++ b/docs/content/docs/smuggling/te-formfeed.md @@ -1,35 +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) +--- +title: "TE Form-Feed — Request Smuggling" +description: "Transfer-Encoding: chunked with Content-Length present. Tested against RFC 9110 §5.5." +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-http10.md b/docs/content/docs/smuggling/te-http10.md index d896b17..3952bf5 100644 --- a/docs/content/docs/smuggling/te-http10.md +++ b/docs/content/docs/smuggling/te-http10.md @@ -1,86 +1,86 @@ ---- -title: "TE-HTTP10" -description: "TE-HTTP10 test documentation" -weight: 17 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TE-HTTP10` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 Section 6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -HTTP/1.0 request with `Transfer-Encoding: chunked` and `Content-Length: 5`. - -```http -POST / HTTP/1.0\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -Note the HTTP/1.0 version — Transfer-Encoding is not defined for HTTP/1.0. - - -## What the RFC says - -> "A server MUST NOT send a response containing Transfer-Encoding unless the corresponding request indicates HTTP/1.1 (or later minor revisions)." — RFC 9112 Section 6.1 - -> "A server or client that receives an HTTP/1.0 message containing a Transfer-Encoding header field MUST treat the message as if the framing is faulty, even if a Content-Length is present, and close the connection." — RFC 9112 Section 6.1 - -Transfer-Encoding is not defined in HTTP/1.0. The RFC explicitly requires treating TE in a 1.0 message as faulty framing. - -## Why it matters - -HTTP/1.0 doesn't support chunked encoding. A server that processes TE on a 1.0 request may disagree with proxies that use CL. - -## Deep Analysis - -### ABNF - -``` -Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 -transfer-coding = token ; RFC 9110 §10.1.4 -HTTP-version = HTTP-name "/" DIGIT "." DIGIT ; RFC 9112 §2.6 -``` - -The Transfer-Encoding mechanism is defined exclusively for HTTP/1.1 and later. HTTP/1.0 has no concept of transfer codings; its only body-length mechanism is Content-Length or connection close. - -### RFC Evidence - -> "A server MUST NOT send a response containing Transfer-Encoding unless the corresponding request indicates HTTP/1.1 (or later minor revisions)." -- RFC 9112 §6.1 - -> "A server or client that receives an HTTP/1.0 message containing a Transfer-Encoding header field MUST treat the message as if the framing is faulty, even if a Content-Length is present, and close the connection after processing the message." -- RFC 9112 §6.1 - -> "Transfer-Encoding was added in HTTP/1.1. It is generally assumed that implementations advertising only HTTP/1.0 support will not understand how to process transfer-encoded content, and that an HTTP/1.0 message received with a Transfer-Encoding is likely to have been forwarded without proper handling of the chunked transfer coding in transit." -- RFC 9112 §6.1 - -### Chain of Reasoning - -1. The test sends an `HTTP/1.0` request with `Transfer-Encoding: chunked` and `Content-Length: 5`. -2. RFC 9112 section 6.1 uses **MUST-level** language: a recipient of an HTTP/1.0 message containing Transfer-Encoding "MUST treat the message as if the framing is faulty, even if a Content-Length is present, and close the connection." -3. This is one of the strongest requirements in the specification. The word "faulty" combined with MUST means the server has no discretion -- it cannot process the Transfer-Encoding, it cannot fall back to Content-Length, it must treat the framing as broken. -4. The RFC further explains the rationale: an HTTP/1.0 message with Transfer-Encoding was likely forwarded by an intermediary that did not properly handle chunked encoding in transit. The message may have been corrupted. -5. The combination of `HTTP/1.0` + `Transfer-Encoding` + `Content-Length` creates a scenario where every possible framing interpretation is unreliable. - -### Scored / Unscored Justification - -This test is **scored** (MUST reject). RFC 9112 section 6.1 contains explicit MUST-level language requiring the server to treat an HTTP/1.0 message with Transfer-Encoding as having faulty framing. There is no room for lenient interpretation -- the server must close the connection. - -- **Pass (400 or close):** The server correctly treats the framing as faulty and closes the connection. -- **Fail (2xx):** The server processed Transfer-Encoding on an HTTP/1.0 request, violating a MUST-level requirement. - -### Smuggling Attack Scenarios - -- **Version Downgrade Desync:** An attacker sends an HTTP/1.0 request with both `Transfer-Encoding: chunked` and `Content-Length: 5`. A front-end proxy that supports HTTP/1.1 may upgrade the request or process the Transfer-Encoding header despite the 1.0 version. A back-end that strictly follows RFC 9112 rejects the request. But if the front-end already forwarded the body using chunked framing to the back-end's connection, leftover bytes on the socket become the next "request" -- a classic smuggling scenario. -- **Proxy Chain Confusion:** In a multi-hop proxy chain, an HTTP/1.0 request with Transfer-Encoding may be handled differently at each hop. A first proxy may strip Transfer-Encoding (treating it as invalid for 1.0), while a second proxy may have already processed the body as chunked. The inconsistency between what each hop consumed leaves attacker-controlled data on the wire. -- **Legacy Server Exploitation:** Older HTTP/1.0-only servers have no concept of chunked encoding. If they receive `Transfer-Encoding: chunked`, they ignore the header entirely and use Content-Length. An intermediary that processes the chunked encoding before forwarding may inject additional data that the legacy server interprets as a separate request. - -## Sources - -- [RFC 9112 Section 6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "TE HTTP/1.0 — Request Smuggling" +description: "HTTP/1.0 request with Transfer-Encoding: chunked and Content-Length: 5. Tested against RFC 9112 Section 6.1." +weight: 17 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-HTTP10` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 Section 6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +HTTP/1.0 request with `Transfer-Encoding: chunked` and `Content-Length: 5`. + +```http +POST / HTTP/1.0\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +Note the HTTP/1.0 version — Transfer-Encoding is not defined for HTTP/1.0. + + +## What the RFC says + +> "A server MUST NOT send a response containing Transfer-Encoding unless the corresponding request indicates HTTP/1.1 (or later minor revisions)." — RFC 9112 Section 6.1 + +> "A server or client that receives an HTTP/1.0 message containing a Transfer-Encoding header field MUST treat the message as if the framing is faulty, even if a Content-Length is present, and close the connection." — RFC 9112 Section 6.1 + +Transfer-Encoding is not defined in HTTP/1.0. The RFC explicitly requires treating TE in a 1.0 message as faulty framing. + +## Why it matters + +HTTP/1.0 doesn't support chunked encoding. A server that processes TE on a 1.0 request may disagree with proxies that use CL. + +## Deep Analysis + +### ABNF + +``` +Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 +transfer-coding = token ; RFC 9110 §10.1.4 +HTTP-version = HTTP-name "/" DIGIT "." DIGIT ; RFC 9112 §2.6 +``` + +The Transfer-Encoding mechanism is defined exclusively for HTTP/1.1 and later. HTTP/1.0 has no concept of transfer codings; its only body-length mechanism is Content-Length or connection close. + +### RFC Evidence + +> "A server MUST NOT send a response containing Transfer-Encoding unless the corresponding request indicates HTTP/1.1 (or later minor revisions)." -- RFC 9112 §6.1 + +> "A server or client that receives an HTTP/1.0 message containing a Transfer-Encoding header field MUST treat the message as if the framing is faulty, even if a Content-Length is present, and close the connection after processing the message." -- RFC 9112 §6.1 + +> "Transfer-Encoding was added in HTTP/1.1. It is generally assumed that implementations advertising only HTTP/1.0 support will not understand how to process transfer-encoded content, and that an HTTP/1.0 message received with a Transfer-Encoding is likely to have been forwarded without proper handling of the chunked transfer coding in transit." -- RFC 9112 §6.1 + +### Chain of Reasoning + +1. The test sends an `HTTP/1.0` request with `Transfer-Encoding: chunked` and `Content-Length: 5`. +2. RFC 9112 section 6.1 uses **MUST-level** language: a recipient of an HTTP/1.0 message containing Transfer-Encoding "MUST treat the message as if the framing is faulty, even if a Content-Length is present, and close the connection." +3. This is one of the strongest requirements in the specification. The word "faulty" combined with MUST means the server has no discretion -- it cannot process the Transfer-Encoding, it cannot fall back to Content-Length, it must treat the framing as broken. +4. The RFC further explains the rationale: an HTTP/1.0 message with Transfer-Encoding was likely forwarded by an intermediary that did not properly handle chunked encoding in transit. The message may have been corrupted. +5. The combination of `HTTP/1.0` + `Transfer-Encoding` + `Content-Length` creates a scenario where every possible framing interpretation is unreliable. + +### Scored / Unscored Justification + +This test is **scored** (MUST reject). RFC 9112 section 6.1 contains explicit MUST-level language requiring the server to treat an HTTP/1.0 message with Transfer-Encoding as having faulty framing. There is no room for lenient interpretation -- the server must close the connection. + +- **Pass (400 or close):** The server correctly treats the framing as faulty and closes the connection. +- **Fail (2xx):** The server processed Transfer-Encoding on an HTTP/1.0 request, violating a MUST-level requirement. + +### Smuggling Attack Scenarios + +- **Version Downgrade Desync:** An attacker sends an HTTP/1.0 request with both `Transfer-Encoding: chunked` and `Content-Length: 5`. A front-end proxy that supports HTTP/1.1 may upgrade the request or process the Transfer-Encoding header despite the 1.0 version. A back-end that strictly follows RFC 9112 rejects the request. But if the front-end already forwarded the body using chunked framing to the back-end's connection, leftover bytes on the socket become the next "request" -- a classic smuggling scenario. +- **Proxy Chain Confusion:** In a multi-hop proxy chain, an HTTP/1.0 request with Transfer-Encoding may be handled differently at each hop. A first proxy may strip Transfer-Encoding (treating it as invalid for 1.0), while a second proxy may have already processed the body as chunked. The inconsistency between what each hop consumed leaves attacker-controlled data on the wire. +- **Legacy Server Exploitation:** Older HTTP/1.0-only servers have no concept of chunked encoding. If they receive `Transfer-Encoding: chunked`, they ignore the header entirely and use Content-Length. An intermediary that processes the chunked encoding before forwarding may inject additional data that the legacy server interprets as a separate request. + +## Sources + +- [RFC 9112 Section 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 c8db4ca..acbb9dc 100644 --- a/docs/content/docs/smuggling/te-identity.md +++ b/docs/content/docs/smuggling/te-identity.md @@ -1,92 +1,92 @@ ---- -title: "TE-IDENTITY" -description: "TE-IDENTITY test documentation" -weight: 30 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TE-IDENTITY` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **Requirement** | MUST reject | -| **Expected** | `400`/`501` or close | - -## What it sends - -A request with `Transfer-Encoding: identity` and `Content-Length: 5`. The `identity` encoding was deprecated and removed in HTTP/1.1 (RFC 7230 and later RFC 9112). - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: identity\r\n -Content-Length: 5\r\n -\r\n -hello -``` - - -## What the RFC says - -> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 Section 6.1 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 Section 6.3 - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 Section 6.1 - -The `identity` transfer coding was listed in RFC 2616 (Section 3.6) as a registered transfer coding meaning "no transformation." It was removed from the transfer coding registry in RFC 7230 and is absent from RFC 9112. Since `identity` is no longer a recognized transfer coding, a server receiving `Transfer-Encoding: identity` is receiving an unknown coding. - -## Why it matters - -If a front-end treats `identity` as "no encoding" and uses Content-Length, but a back-end rejects the unknown TE, they disagree on how to parse the body. Conversely, if the back-end ignores the TE header entirely, CL is used — but a front-end that rejects may not forward the request at all. Both scenarios create desync potential. - -## Deep Analysis - -### ABNF - -``` -Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 -transfer-coding = token ; RFC 9110 §10.1.4 -token = 1*tchar ; RFC 9110 §5.6.2 -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" - / DIGIT / ALPHA -``` - -The token `identity` is syntactically valid per the ABNF (it consists entirely of ALPHA characters). However, `identity` is not a registered transfer coding in RFC 9112. It was present in RFC 2616 section 3.6 as a "no transformation" coding but was explicitly removed in RFC 7230 and remains absent from RFC 9112. - -### RFC Evidence - -> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 §6.1 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 - -### Chain of Reasoning - -1. The test sends `Transfer-Encoding: identity` alongside `Content-Length: 5`. -2. The `identity` coding was listed in RFC 2616 (the original HTTP/1.1 spec) as meaning "no transformation." It was removed in RFC 7230 (the revised HTTP/1.1 message syntax) and remains absent from RFC 9112. -3. Since `identity` is not registered in the current HTTP Transfer Coding registry, a compliant server does not understand it. RFC 9112 section 6.1 states that a server receiving an unrecognized transfer coding SHOULD respond with `501 (Not Implemented)`. -4. The presence of both Transfer-Encoding and Content-Length triggers RFC 9112 section 6.3, which states that Transfer-Encoding overrides Content-Length and that such a message "ought to be handled as an error." -5. RFC 9112 section 6.1 additionally requires the server to close the connection after responding to a request with both headers. -6. The `identity` coding is particularly dangerous because legacy servers may still recognize it from their RFC 2616 implementations, treating it as "no transformation" and falling back to Content-Length framing -- while modern servers reject it as unknown. - -### Scored / Unscored Justification - -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/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 - -- **Legacy vs. Modern Desync:** An attacker sends `Transfer-Encoding: identity` with `Content-Length: 5`. A legacy proxy still running RFC 2616 logic recognizes `identity` as "no transformation" and uses Content-Length for framing. A modern back-end treats `identity` as an unknown coding and responds with an error. But if the proxy already forwarded the body, leftover bytes on the connection become a smuggled request. -- **Identity-as-Passthrough Exploit:** Some server implementations treat any unrecognized Transfer-Encoding as a no-op, effectively implementing `identity` behavior. If a front-end rejects the unknown coding but the back-end silently accepts it and uses Content-Length, the attacker controls body-boundary interpretation through the framing disagreement. -- **Encoding Priority Confusion:** When a server sees `Transfer-Encoding: identity`, it must decide whether Transfer-Encoding is "present" (and thus overrides Content-Length per section 6.3) or "unrecognized" (and thus potentially ignorable). This ambiguity in priority logic is exactly what attackers exploit to create desync conditions. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) -- [RFC 2616 §3.6](https://www.rfc-editor.org/rfc/rfc2616#section-3.6) (obsolete -- defined `identity` as a transfer coding) +--- +title: "TE Identity — Request Smuggling" +description: "A request with Transfer-Encoding: identity and Content-Length: 5. The identity encoding was deprecated and removed in HTTP/1.1 (RFC 7230 and later RFC 9112). Tested against RFC 9112 §6.1." +weight: 30 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-IDENTITY` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **Requirement** | MUST reject | +| **Expected** | `400`/`501` or close | + +## What it sends + +A request with `Transfer-Encoding: identity` and `Content-Length: 5`. The `identity` encoding was deprecated and removed in HTTP/1.1 (RFC 7230 and later RFC 9112). + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: identity\r\n +Content-Length: 5\r\n +\r\n +hello +``` + + +## What the RFC says + +> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 Section 6.1 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 Section 6.3 + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 Section 6.1 + +The `identity` transfer coding was listed in RFC 2616 (Section 3.6) as a registered transfer coding meaning "no transformation." It was removed from the transfer coding registry in RFC 7230 and is absent from RFC 9112. Since `identity` is no longer a recognized transfer coding, a server receiving `Transfer-Encoding: identity` is receiving an unknown coding. + +## Why it matters + +If a front-end treats `identity` as "no encoding" and uses Content-Length, but a back-end rejects the unknown TE, they disagree on how to parse the body. Conversely, if the back-end ignores the TE header entirely, CL is used — but a front-end that rejects may not forward the request at all. Both scenarios create desync potential. + +## Deep Analysis + +### ABNF + +``` +Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 +transfer-coding = token ; RFC 9110 §10.1.4 +token = 1*tchar ; RFC 9110 §5.6.2 +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + / DIGIT / ALPHA +``` + +The token `identity` is syntactically valid per the ABNF (it consists entirely of ALPHA characters). However, `identity` is not a registered transfer coding in RFC 9112. It was present in RFC 2616 section 3.6 as a "no transformation" coding but was explicitly removed in RFC 7230 and remains absent from RFC 9112. + +### RFC Evidence + +> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 §6.1 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 + +### Chain of Reasoning + +1. The test sends `Transfer-Encoding: identity` alongside `Content-Length: 5`. +2. The `identity` coding was listed in RFC 2616 (the original HTTP/1.1 spec) as meaning "no transformation." It was removed in RFC 7230 (the revised HTTP/1.1 message syntax) and remains absent from RFC 9112. +3. Since `identity` is not registered in the current HTTP Transfer Coding registry, a compliant server does not understand it. RFC 9112 section 6.1 states that a server receiving an unrecognized transfer coding SHOULD respond with `501 (Not Implemented)`. +4. The presence of both Transfer-Encoding and Content-Length triggers RFC 9112 section 6.3, which states that Transfer-Encoding overrides Content-Length and that such a message "ought to be handled as an error." +5. RFC 9112 section 6.1 additionally requires the server to close the connection after responding to a request with both headers. +6. The `identity` coding is particularly dangerous because legacy servers may still recognize it from their RFC 2616 implementations, treating it as "no transformation" and falling back to Content-Length framing -- while modern servers reject it as unknown. + +### Scored / Unscored Justification + +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/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 + +- **Legacy vs. Modern Desync:** An attacker sends `Transfer-Encoding: identity` with `Content-Length: 5`. A legacy proxy still running RFC 2616 logic recognizes `identity` as "no transformation" and uses Content-Length for framing. A modern back-end treats `identity` as an unknown coding and responds with an error. But if the proxy already forwarded the body, leftover bytes on the connection become a smuggled request. +- **Identity-as-Passthrough Exploit:** Some server implementations treat any unrecognized Transfer-Encoding as a no-op, effectively implementing `identity` behavior. If a front-end rejects the unknown coding but the back-end silently accepts it and uses Content-Length, the attacker controls body-boundary interpretation through the framing disagreement. +- **Encoding Priority Confusion:** When a server sees `Transfer-Encoding: identity`, it must decide whether Transfer-Encoding is "present" (and thus overrides Content-Length per section 6.3) or "unrecognized" (and thus potentially ignorable). This ambiguity in priority logic is exactly what attackers exploit to create desync conditions. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +- [RFC 2616 §3.6](https://www.rfc-editor.org/rfc/rfc2616#section-3.6) (obsolete -- defined `identity` as a transfer coding) diff --git a/docs/content/docs/smuggling/te-leading-comma.md b/docs/content/docs/smuggling/te-leading-comma.md index c90a7f1..8c108a8 100644 --- a/docs/content/docs/smuggling/te-leading-comma.md +++ b/docs/content/docs/smuggling/te-leading-comma.md @@ -1,94 +1,94 @@ ---- -title: "TE-LEADING-COMMA" -description: "TE-LEADING-COMMA test documentation" -weight: 23 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TE-LEADING-COMMA` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §5.6.1](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.1) | -| **Requirement** | SHOULD | -| **Expected** | `400` or `2xx` | - -## What it sends - -`Transfer-Encoding: , chunked` — leading comma before `chunked`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: , chunked\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -The Transfer-Encoding value starts with a leading comma before `chunked`. - - -## What the RFC says - -> "A recipient MUST parse and ignore a reasonable number of empty list elements: enough to handle common mistakes by senders that merge values, but not so much that they could be used as a denial-of-service mechanism." -- RFC 9110 Section 5.6.1 - -The leading comma produces an empty list element before `chunked`. Since RFC 9110 Section 5.6.1 requires recipients to parse and ignore empty list elements, a server that strips the empty element and processes `chunked` normally is RFC-compliant. - -## Pass / Warn - -The RFC requires recipients to ignore empty list elements, so stripping the leading comma and processing `chunked` is RFC-compliant. However, rejecting the value is the safer choice since the leading comma creates ambiguity. Both behaviors are valid, but rejection is preferred. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (RFC-valid per Section 5.6.1 empty-element handling). - -## Why it matters - -Some parsers strip leading commas and see "chunked", while others reject the value entirely. This discrepancy enables smuggling when front-end and back-end parsers disagree on whether Transfer-Encoding is valid. - -## Deep Analysis - -### ABNF - -``` -Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 -transfer-coding = token ; RFC 9110 §10.1.4 -token = 1*tchar ; RFC 9110 §5.6.2 -OWS = *( SP / HTAB ) ; RFC 9110 §5.6.3 -``` - -The `#` rule (list syntax) is defined in RFC 9110 section 5.6.1. A `#` list allows comma-separated elements with optional whitespace. The value `, chunked` consists of an empty element (before the comma), a comma delimiter, and the element `chunked`. The empty element is what the RFC requires recipients to parse and ignore. - -### RFC Evidence - -> "A recipient MUST parse and ignore a reasonable number of empty list elements: enough to handle common mistakes by senders that merge values, but not so much that they could be used as a denial-of-service mechanism." -- RFC 9110 §5.6.1 - -> "A recipient MUST be able to parse the chunked transfer coding (Section 7.1) because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 §6.1 - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 - -### Chain of Reasoning - -1. The test sends `Transfer-Encoding: , chunked` alongside `Content-Length: 5`. -2. Per the `#` list syntax in RFC 9110 section 5.6.1, the leading comma produces an empty list element before the actual `chunked` element. -3. RFC 9110 section 5.6.1 explicitly requires recipients to "parse and ignore a reasonable number of empty list elements." This means a fully compliant server should strip the empty element and see `Transfer-Encoding: chunked`. -4. However, some parsers do not implement empty-element stripping for Transfer-Encoding specifically. They may see `, chunked` as a single unrecognized token (including the comma and space) and reject it or fall back to Content-Length. -5. A server that strips the empty element and processes `chunked` is following RFC 9110 section 5.6.1. A server that rejects with `400` is being strict but safe. -6. The dual presence of Transfer-Encoding and Content-Length triggers the smuggling warning in RFC 9112 section 6.3, regardless of how the leading comma is handled. - -### Scored / SHOULD Justification - -This test is scored as **SHOULD** reject because, while RFC 9110 section 5.6.1 explicitly requires recipients to handle empty list elements, the leading comma creates smuggling risk. A server that ignores the empty element and processes `chunked` is RFC-compliant. A server that rejects the request is being defensive. Neither response violates a MUST-level requirement, but rejection is the safer behavior. - -- **Pass (400):** Strict rejection -- the server refuses to process the syntactically unusual value. -- **Warn (2xx):** RFC-compliant per section 5.6.1 -- the server correctly stripped the empty list element and processed `chunked`. - -### Smuggling Attack Scenarios - -- **Empty-Element Stripping Disagreement:** A front-end proxy does not strip empty list elements and sees `, chunked` as an unrecognized transfer coding, falling back to Content-Length framing. A back-end correctly strips the empty element, sees `chunked`, and uses chunked framing. The desync between Content-Length and chunked framing lets the attacker embed a second request inside the chunked body. -- **Comma Prefix as WAF Bypass:** Web Application Firewalls that check for `Transfer-Encoding: chunked` using exact string matching will not match `, chunked`. The request passes through the WAF unexamined, but the origin server strips the leading comma and processes chunked encoding normally. This allows the attacker to smuggle requests past the WAF. -- **Normalization Cascade:** In a multi-hop proxy chain, the first proxy may strip the leading comma and forward `Transfer-Encoding: chunked`. The second proxy now sees a clean header and processes normally. But if the first proxy used Content-Length for its own framing decision before normalizing, the body it forwarded may not match what the second proxy expects from chunked framing. - -## Sources - -- [RFC 9110 §5.6.1 -- Lists](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.1) -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "TE Leading Comma — Request Smuggling" +description: "Transfer-Encoding: , chunked — leading comma before chunked. Tested against RFC 9110 §5.6.1." +weight: 23 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-LEADING-COMMA` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §5.6.1](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.1) | +| **Requirement** | SHOULD | +| **Expected** | `400` or `2xx` | + +## What it sends + +`Transfer-Encoding: , chunked` — leading comma before `chunked`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: , chunked\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +The Transfer-Encoding value starts with a leading comma before `chunked`. + + +## What the RFC says + +> "A recipient MUST parse and ignore a reasonable number of empty list elements: enough to handle common mistakes by senders that merge values, but not so much that they could be used as a denial-of-service mechanism." -- RFC 9110 Section 5.6.1 + +The leading comma produces an empty list element before `chunked`. Since RFC 9110 Section 5.6.1 requires recipients to parse and ignore empty list elements, a server that strips the empty element and processes `chunked` normally is RFC-compliant. + +## Pass / Warn + +The RFC requires recipients to ignore empty list elements, so stripping the leading comma and processing `chunked` is RFC-compliant. However, rejecting the value is the safer choice since the leading comma creates ambiguity. Both behaviors are valid, but rejection is preferred. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (RFC-valid per Section 5.6.1 empty-element handling). + +## Why it matters + +Some parsers strip leading commas and see "chunked", while others reject the value entirely. This discrepancy enables smuggling when front-end and back-end parsers disagree on whether Transfer-Encoding is valid. + +## Deep Analysis + +### ABNF + +``` +Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 +transfer-coding = token ; RFC 9110 §10.1.4 +token = 1*tchar ; RFC 9110 §5.6.2 +OWS = *( SP / HTAB ) ; RFC 9110 §5.6.3 +``` + +The `#` rule (list syntax) is defined in RFC 9110 section 5.6.1. A `#` list allows comma-separated elements with optional whitespace. The value `, chunked` consists of an empty element (before the comma), a comma delimiter, and the element `chunked`. The empty element is what the RFC requires recipients to parse and ignore. + +### RFC Evidence + +> "A recipient MUST parse and ignore a reasonable number of empty list elements: enough to handle common mistakes by senders that merge values, but not so much that they could be used as a denial-of-service mechanism." -- RFC 9110 §5.6.1 + +> "A recipient MUST be able to parse the chunked transfer coding (Section 7.1) because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 §6.1 + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 + +### Chain of Reasoning + +1. The test sends `Transfer-Encoding: , chunked` alongside `Content-Length: 5`. +2. Per the `#` list syntax in RFC 9110 section 5.6.1, the leading comma produces an empty list element before the actual `chunked` element. +3. RFC 9110 section 5.6.1 explicitly requires recipients to "parse and ignore a reasonable number of empty list elements." This means a fully compliant server should strip the empty element and see `Transfer-Encoding: chunked`. +4. However, some parsers do not implement empty-element stripping for Transfer-Encoding specifically. They may see `, chunked` as a single unrecognized token (including the comma and space) and reject it or fall back to Content-Length. +5. A server that strips the empty element and processes `chunked` is following RFC 9110 section 5.6.1. A server that rejects with `400` is being strict but safe. +6. The dual presence of Transfer-Encoding and Content-Length triggers the smuggling warning in RFC 9112 section 6.3, regardless of how the leading comma is handled. + +### Scored / SHOULD Justification + +This test is scored as **SHOULD** reject because, while RFC 9110 section 5.6.1 explicitly requires recipients to handle empty list elements, the leading comma creates smuggling risk. A server that ignores the empty element and processes `chunked` is RFC-compliant. A server that rejects the request is being defensive. Neither response violates a MUST-level requirement, but rejection is the safer behavior. + +- **Pass (400):** Strict rejection -- the server refuses to process the syntactically unusual value. +- **Warn (2xx):** RFC-compliant per section 5.6.1 -- the server correctly stripped the empty list element and processed `chunked`. + +### Smuggling Attack Scenarios + +- **Empty-Element Stripping Disagreement:** A front-end proxy does not strip empty list elements and sees `, chunked` as an unrecognized transfer coding, falling back to Content-Length framing. A back-end correctly strips the empty element, sees `chunked`, and uses chunked framing. The desync between Content-Length and chunked framing lets the attacker embed a second request inside the chunked body. +- **Comma Prefix as WAF Bypass:** Web Application Firewalls that check for `Transfer-Encoding: chunked` using exact string matching will not match `, chunked`. The request passes through the WAF unexamined, but the origin server strips the leading comma and processes chunked encoding normally. This allows the attacker to smuggle requests past the WAF. +- **Normalization Cascade:** In a multi-hop proxy chain, the first proxy may strip the leading comma and forward `Transfer-Encoding: chunked`. The second proxy now sees a clean header and processes normally. But if the first proxy used Content-Length for its own framing decision before normalizing, the body it forwarded may not match what the second proxy expects from chunked framing. + +## Sources + +- [RFC 9110 §5.6.1 -- Lists](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.1) +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/te-not-final-chunked.md b/docs/content/docs/smuggling/te-not-final-chunked.md index 947d63c..c18fdee 100644 --- a/docs/content/docs/smuggling/te-not-final-chunked.md +++ b/docs/content/docs/smuggling/te-not-final-chunked.md @@ -1,84 +1,84 @@ ---- -title: "TE-NOT-FINAL-CHUNKED" -description: "TE-NOT-FINAL-CHUNKED test documentation" -weight: 16 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TE-NOT-FINAL-CHUNKED` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) | -| **Requirement** | MUST reject | -| **Expected** | `400` or close | - -## What it sends - -`Transfer-Encoding: chunked, gzip` — chunked is not the final encoding. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked, gzip\r\n -\r\n -0\r\n -\r\n -``` - - -## What the RFC says - -> "If a Transfer-Encoding header field is present in a request and the chunked transfer coding is not the final encoding, the message body length cannot be determined reliably; the server MUST respond with the 400 (Bad Request) status code and then close the connection." — RFC 9112 §6.3 - -This is MUST-level language — servers have no discretion here. - -## Why it matters - -If chunked isn't the final encoding, the server cannot determine body boundaries. This can be exploited for smuggling. - -## Deep Analysis - -### ABNF - -``` -Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 -transfer-coding = token ; RFC 9110 §10.1.4 -token = 1*tchar ; RFC 9110 §5.6.2 -``` - -The value `chunked, gzip` is a valid comma-separated list of two transfer-coding tokens. The ordering matters: the last element in the list represents the outermost encoding applied to the message body. In this case, `gzip` is the final (outermost) encoding, not `chunked`. - -### RFC Evidence - -> "If a Transfer-Encoding header field is present in a request and the chunked transfer coding is not the final encoding, the message body length cannot be determined reliably; the server MUST respond with the 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 - -> "If any transfer coding other than chunked is applied to a request's content, the sender MUST apply chunked as the final transfer coding to ensure that the message is properly framed." -- RFC 9112 §6.1 - -> "A recipient MUST be able to parse the chunked transfer coding (Section 7.1) because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 §6.1 - -### Chain of Reasoning - -1. The test sends `Transfer-Encoding: chunked, gzip` with a zero-length chunked body (`0\r\n\r\n`). -2. In Transfer-Encoding, the codings are listed in the order they were applied. The last coding in the list is the outermost encoding. Here, `gzip` is final, meaning the message body is supposed to be gzip-compressed data that, when decompressed, yields chunked-encoded data. -3. RFC 9112 section 6.1 requires the sender to apply chunked as the **final** transfer coding for requests. The sender violated this by placing `gzip` after `chunked`. -4. RFC 9112 section 6.3 uses **MUST-level** language: when chunked is not the final encoding in a request, "the server MUST respond with the 400 (Bad Request) status code and then close the connection." There is no discretion here. -5. The rationale is clear: if chunked is not the outermost encoding, the server cannot determine message body boundaries. Chunked encoding is the framing mechanism -- without it as the outermost layer, the server has no way to know where the message body ends. -6. The test body (`0\r\n\r\n`) is a valid chunked terminator, but the server is supposed to see the body as gzip-compressed first. Since the body is not valid gzip data, the framing is inherently broken. - -### Scored / Unscored Justification - -This test is **scored** (MUST reject). RFC 9112 section 6.3 contains one of the most explicit MUST-level requirements in the specification: when chunked is not the final encoding in a request, the server "MUST respond with the 400 (Bad Request) status code and then close the connection." There is no ambiguity and no room for lenient processing. - -- **Pass (400 or close):** The server correctly rejects the request per the explicit MUST requirement. -- **Fail (2xx):** The server accepted a request with indeterminate body boundaries, violating a MUST-level requirement. - -### Smuggling Attack Scenarios - -- **Encoding Order Confusion:** A front-end proxy may only check whether `chunked` appears anywhere in the Transfer-Encoding list and process chunked framing regardless of position. It reads the body using chunk boundaries. A back-end that correctly validates the encoding order rejects the request with `400`. But if the front-end already consumed the body and is reusing the connection, leftover data on the socket becomes the next request. -- **Gzip Decompression Bypass:** A front-end that tries to apply the encodings in order first attempts gzip decompression. Since the body is not valid gzip, it may error or pass through raw bytes. A back-end that ignores encoding order and processes `chunked` directly reads different body boundaries. The mismatch enables request smuggling. -- **Selective Encoding Processing:** Some implementations only process `chunked` and ignore other transfer codings they do not support. If a front-end ignores `gzip` and processes `chunked`, but a back-end treats the entire Transfer-Encoding as invalid and falls back to reading until connection close, the two parsers consume different amounts of data from the connection. - -## Sources - -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) -- [RFC 9112 §7](https://www.rfc-editor.org/rfc/rfc9112#section-7) +--- +title: "TE Not Final Chunked — Request Smuggling" +description: "Transfer-Encoding: chunked, gzip — chunked is not the final encoding. Tested against RFC 9112 §6.3." +weight: 16 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-NOT-FINAL-CHUNKED` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) | +| **Requirement** | MUST reject | +| **Expected** | `400` or close | + +## What it sends + +`Transfer-Encoding: chunked, gzip` — chunked is not the final encoding. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked, gzip\r\n +\r\n +0\r\n +\r\n +``` + + +## What the RFC says + +> "If a Transfer-Encoding header field is present in a request and the chunked transfer coding is not the final encoding, the message body length cannot be determined reliably; the server MUST respond with the 400 (Bad Request) status code and then close the connection." — RFC 9112 §6.3 + +This is MUST-level language — servers have no discretion here. + +## Why it matters + +If chunked isn't the final encoding, the server cannot determine body boundaries. This can be exploited for smuggling. + +## Deep Analysis + +### ABNF + +``` +Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 +transfer-coding = token ; RFC 9110 §10.1.4 +token = 1*tchar ; RFC 9110 §5.6.2 +``` + +The value `chunked, gzip` is a valid comma-separated list of two transfer-coding tokens. The ordering matters: the last element in the list represents the outermost encoding applied to the message body. In this case, `gzip` is the final (outermost) encoding, not `chunked`. + +### RFC Evidence + +> "If a Transfer-Encoding header field is present in a request and the chunked transfer coding is not the final encoding, the message body length cannot be determined reliably; the server MUST respond with the 400 (Bad Request) status code and then close the connection." -- RFC 9112 §6.3 + +> "If any transfer coding other than chunked is applied to a request's content, the sender MUST apply chunked as the final transfer coding to ensure that the message is properly framed." -- RFC 9112 §6.1 + +> "A recipient MUST be able to parse the chunked transfer coding (Section 7.1) because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 §6.1 + +### Chain of Reasoning + +1. The test sends `Transfer-Encoding: chunked, gzip` with a zero-length chunked body (`0\r\n\r\n`). +2. In Transfer-Encoding, the codings are listed in the order they were applied. The last coding in the list is the outermost encoding. Here, `gzip` is final, meaning the message body is supposed to be gzip-compressed data that, when decompressed, yields chunked-encoded data. +3. RFC 9112 section 6.1 requires the sender to apply chunked as the **final** transfer coding for requests. The sender violated this by placing `gzip` after `chunked`. +4. RFC 9112 section 6.3 uses **MUST-level** language: when chunked is not the final encoding in a request, "the server MUST respond with the 400 (Bad Request) status code and then close the connection." There is no discretion here. +5. The rationale is clear: if chunked is not the outermost encoding, the server cannot determine message body boundaries. Chunked encoding is the framing mechanism -- without it as the outermost layer, the server has no way to know where the message body ends. +6. The test body (`0\r\n\r\n`) is a valid chunked terminator, but the server is supposed to see the body as gzip-compressed first. Since the body is not valid gzip data, the framing is inherently broken. + +### Scored / Unscored Justification + +This test is **scored** (MUST reject). RFC 9112 section 6.3 contains one of the most explicit MUST-level requirements in the specification: when chunked is not the final encoding in a request, the server "MUST respond with the 400 (Bad Request) status code and then close the connection." There is no ambiguity and no room for lenient processing. + +- **Pass (400 or close):** The server correctly rejects the request per the explicit MUST requirement. +- **Fail (2xx):** The server accepted a request with indeterminate body boundaries, violating a MUST-level requirement. + +### Smuggling Attack Scenarios + +- **Encoding Order Confusion:** A front-end proxy may only check whether `chunked` appears anywhere in the Transfer-Encoding list and process chunked framing regardless of position. It reads the body using chunk boundaries. A back-end that correctly validates the encoding order rejects the request with `400`. But if the front-end already consumed the body and is reusing the connection, leftover data on the socket becomes the next request. +- **Gzip Decompression Bypass:** A front-end that tries to apply the encodings in order first attempts gzip decompression. Since the body is not valid gzip, it may error or pass through raw bytes. A back-end that ignores encoding order and processes `chunked` directly reads different body boundaries. The mismatch enables request smuggling. +- **Selective Encoding Processing:** Some implementations only process `chunked` and ignore other transfer codings they do not support. If a front-end ignores `gzip` and processes `chunked`, but a back-end treats the entire Transfer-Encoding as invalid and falls back to reading until connection close, the two parsers consume different amounts of data from the connection. + +## Sources + +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +- [RFC 9112 §7](https://www.rfc-editor.org/rfc/rfc9112#section-7) diff --git a/docs/content/docs/smuggling/te-null.md b/docs/content/docs/smuggling/te-null.md index 97066fa..90ab7d6 100644 --- a/docs/content/docs/smuggling/te-null.md +++ b/docs/content/docs/smuggling/te-null.md @@ -1,35 +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) +--- +title: "TE Null — Request Smuggling" +description: "Transfer-Encoding: chunked with Content-Length present. Tested against RFC 9110 §5.5." +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 a3df41b..d066779 100644 --- a/docs/content/docs/smuggling/te-obs-fold.md +++ b/docs/content/docs/smuggling/te-obs-fold.md @@ -1,89 +1,89 @@ ---- -title: "TE-OBS-FOLD" -description: "TE-OBS-FOLD test documentation" -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 reject or unfold obs-fold | -| **Expected** | `400`, or `2xx` with connection close | - -## What it sends - -Transfer-Encoding header value wrapped using obs-fold (obsolete line folding), with Content-Length also present. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding:\r\n - chunked\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -The Transfer-Encoding value `chunked` is placed on a continuation line (preceded by CRLF and a space), using the obsolete line folding syntax. - - -## What the RFC says - -> "A server that receives an obs-fold in a request message that is not within a 'message/http' container MUST either reject the message by sending a 400 (Bad Request), preferably with a representation explaining that obsolete line folding is unacceptable, or replace each received obs-fold with one or more SP octets prior to interpreting the field value or forwarding the message downstream." -- RFC 9112 Section 5.2 - -When obs-fold is used on the Transfer-Encoding header with Content-Length also present, the risk is acute: a folding-aware parser unfolds the value and sees `Transfer-Encoding: chunked`, while a strict parser that does not recognize the fold sees an empty Transfer-Encoding value and falls back to Content-Length. This creates a direct CL/TE desync. - -## 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, one parser can unfold to `chunked` while another ignores it and falls back to Content-Length. - -## Deep Analysis - -### ABNF - -``` -field-line = field-name ":" OWS field-value OWS ; RFC 9112 §5 -obs-fold = OWS CRLF RWS ; RFC 9112 §5.2 -OWS = *( SP / HTAB ) ; RFC 9110 §5.6.3 -RWS = 1*( SP / HTAB ) ; RFC 9110 §5.6.3 -``` - -The `obs-fold` rule (obsolete line folding) allows a field value to be continued on the next line if that line begins with at least one space or tab (`RWS`). In this test, the Transfer-Encoding value is split: the colon is followed by `\r\n` (CRLF) and then ` chunked` (space + value), matching the `obs-fold` production. - -### RFC Evidence - -> "A server that receives an obs-fold in a request message that is not within a message/http container MUST either reject the message by sending a 400 (Bad Request), preferably with a representation explaining that obsolete line folding is unacceptable, or replace each received obs-fold with one or more SP octets prior to interpreting the field value or forwarding the message downstream." -- RFC 9112 §5.2 - -> "A sender MUST NOT generate a message that includes line folding (i.e., that has any field line value that contains a match to the obs-fold rule) unless the message is intended for packaging within the message/http media type." -- RFC 9112 §5.2 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 - -### Chain of Reasoning - -1. The test sends a Transfer-Encoding header where the value `chunked` is placed on a continuation line using obs-fold syntax: `Transfer-Encoding:\r\n chunked`. A `Content-Length: 5` header is also present. -2. RFC 9112 section 5.2 explicitly states that a sender MUST NOT generate messages with obs-fold. The sender is in violation. -3. For the server, RFC 9112 section 5.2 provides a MUST-level requirement with two options: either reject with `400` **or** replace the obs-fold with spaces and interpret the field value normally. -4. If the server chooses to unfold, it replaces the `\r\n ` with a space and sees `Transfer-Encoding: chunked`. Combined with `Content-Length: 5`, this triggers the dual-header rules of RFC 9112 section 6.3. -5. The critical danger is that a parser **unaware** of obs-fold sees the `Transfer-Encoding:` header as having an empty value (since the value after the colon on that line is empty), followed by what looks like a new header line starting with ` chunked`. The space-prefixed line may be discarded as malformed or interpreted as a separate entity. This parser sees no valid Transfer-Encoding and falls back to Content-Length. -6. Meanwhile, a folding-aware parser unfolds the value and uses chunked framing. This disagreement between folding-aware and folding-unaware parsers is the core of the smuggling vector. - -### Scored / Unscored Justification - -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`. -- **Warn:** `2xx` with connection close (accepted unfold path). -- **Fail:** `2xx` without connection close. - -### Smuggling Attack Scenarios - -- **Fold-Aware vs. Fold-Unaware Desync:** A front-end proxy that does not implement obs-fold parsing sees `Transfer-Encoding:` with an empty value and ignores it, using Content-Length for framing. A back-end that implements obs-fold unfolding sees `Transfer-Encoding: chunked` and uses chunked framing. The attacker embeds a second request inside the chunked body that the front-end never sees. -- **Header Injection via Fold Confusion:** A parser that does not recognize obs-fold may interpret the continuation line ` chunked` as a malformed header line. Some parsers silently discard lines starting with whitespace, while others attempt to parse them as headers. This inconsistency can cause different hops in a proxy chain to see different sets of headers. -- **Selective Obs-Fold Processing:** Even among folding-aware parsers, some may only unfold certain headers. A proxy that unfolds general headers but not Transfer-Encoding specifically would see an empty TE value, while the back-end unfolds all headers and processes chunked encoding. The selective unfolding creates the exact framing disagreement attackers need. - -## Sources - -- [RFC 9112 §5.2](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "TE Obs Fold — Request Smuggling" +description: "Transfer-Encoding header value wrapped using obs-fold (obsolete line folding), with Content-Length also present. Tested against RFC 9112 §5.2." +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 reject or unfold obs-fold | +| **Expected** | `400`, or `2xx` with connection close | + +## What it sends + +Transfer-Encoding header value wrapped using obs-fold (obsolete line folding), with Content-Length also present. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding:\r\n + chunked\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +The Transfer-Encoding value `chunked` is placed on a continuation line (preceded by CRLF and a space), using the obsolete line folding syntax. + + +## What the RFC says + +> "A server that receives an obs-fold in a request message that is not within a 'message/http' container MUST either reject the message by sending a 400 (Bad Request), preferably with a representation explaining that obsolete line folding is unacceptable, or replace each received obs-fold with one or more SP octets prior to interpreting the field value or forwarding the message downstream." -- RFC 9112 Section 5.2 + +When obs-fold is used on the Transfer-Encoding header with Content-Length also present, the risk is acute: a folding-aware parser unfolds the value and sees `Transfer-Encoding: chunked`, while a strict parser that does not recognize the fold sees an empty Transfer-Encoding value and falls back to Content-Length. This creates a direct CL/TE desync. + +## 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, one parser can unfold to `chunked` while another ignores it and falls back to Content-Length. + +## Deep Analysis + +### ABNF + +``` +field-line = field-name ":" OWS field-value OWS ; RFC 9112 §5 +obs-fold = OWS CRLF RWS ; RFC 9112 §5.2 +OWS = *( SP / HTAB ) ; RFC 9110 §5.6.3 +RWS = 1*( SP / HTAB ) ; RFC 9110 §5.6.3 +``` + +The `obs-fold` rule (obsolete line folding) allows a field value to be continued on the next line if that line begins with at least one space or tab (`RWS`). In this test, the Transfer-Encoding value is split: the colon is followed by `\r\n` (CRLF) and then ` chunked` (space + value), matching the `obs-fold` production. + +### RFC Evidence + +> "A server that receives an obs-fold in a request message that is not within a message/http container MUST either reject the message by sending a 400 (Bad Request), preferably with a representation explaining that obsolete line folding is unacceptable, or replace each received obs-fold with one or more SP octets prior to interpreting the field value or forwarding the message downstream." -- RFC 9112 §5.2 + +> "A sender MUST NOT generate a message that includes line folding (i.e., that has any field line value that contains a match to the obs-fold rule) unless the message is intended for packaging within the message/http media type." -- RFC 9112 §5.2 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 + +### Chain of Reasoning + +1. The test sends a Transfer-Encoding header where the value `chunked` is placed on a continuation line using obs-fold syntax: `Transfer-Encoding:\r\n chunked`. A `Content-Length: 5` header is also present. +2. RFC 9112 section 5.2 explicitly states that a sender MUST NOT generate messages with obs-fold. The sender is in violation. +3. For the server, RFC 9112 section 5.2 provides a MUST-level requirement with two options: either reject with `400` **or** replace the obs-fold with spaces and interpret the field value normally. +4. If the server chooses to unfold, it replaces the `\r\n ` with a space and sees `Transfer-Encoding: chunked`. Combined with `Content-Length: 5`, this triggers the dual-header rules of RFC 9112 section 6.3. +5. The critical danger is that a parser **unaware** of obs-fold sees the `Transfer-Encoding:` header as having an empty value (since the value after the colon on that line is empty), followed by what looks like a new header line starting with ` chunked`. The space-prefixed line may be discarded as malformed or interpreted as a separate entity. This parser sees no valid Transfer-Encoding and falls back to Content-Length. +6. Meanwhile, a folding-aware parser unfolds the value and uses chunked framing. This disagreement between folding-aware and folding-unaware parsers is the core of the smuggling vector. + +### Scored / Unscored Justification + +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`. +- **Warn:** `2xx` with connection close (accepted unfold path). +- **Fail:** `2xx` without connection close. + +### Smuggling Attack Scenarios + +- **Fold-Aware vs. Fold-Unaware Desync:** A front-end proxy that does not implement obs-fold parsing sees `Transfer-Encoding:` with an empty value and ignores it, using Content-Length for framing. A back-end that implements obs-fold unfolding sees `Transfer-Encoding: chunked` and uses chunked framing. The attacker embeds a second request inside the chunked body that the front-end never sees. +- **Header Injection via Fold Confusion:** A parser that does not recognize obs-fold may interpret the continuation line ` chunked` as a malformed header line. Some parsers silently discard lines starting with whitespace, while others attempt to parse them as headers. This inconsistency can cause different hops in a proxy chain to see different sets of headers. +- **Selective Obs-Fold Processing:** Even among folding-aware parsers, some may only unfold certain headers. A proxy that unfolds general headers but not Transfer-Encoding specifically would see an empty TE value, while the back-end unfolds all headers and processes chunked encoding. The selective unfolding creates the exact framing disagreement attackers need. + +## Sources + +- [RFC 9112 §5.2](https://www.rfc-editor.org/rfc/rfc9112#section-5.2) +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/te-sp-before-colon.md b/docs/content/docs/smuggling/te-sp-before-colon.md index d64b2ad..ac35234 100644 --- a/docs/content/docs/smuggling/te-sp-before-colon.md +++ b/docs/content/docs/smuggling/te-sp-before-colon.md @@ -1,87 +1,87 @@ ---- -title: "TE-SP-BEFORE-COLON" -description: "TE-SP-BEFORE-COLON test documentation" -weight: 7 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TE-SP-BEFORE-COLON` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §5.1](https://www.rfc-editor.org/rfc/rfc9112#section-5.1) | -| **Requirement** | MUST | -| **Expected** | `400` or close | - -## What it sends - -`Transfer-Encoding : chunked` — space before the colon. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding : chunked\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -Note the space between `Transfer-Encoding` and the colon. - - -## What the RFC says - -> "No whitespace is allowed between the field name and colon. In the past, differences in the handling of such whitespace have led to security vulnerabilities in request routing and response handling." -- RFC 9112 Section 5.1 - -> "A server MUST reject, with a response status code of 400 (Bad Request), any received request message that contains whitespace between a header field name and colon." -- RFC 9112 Section 5.1 - -This is MUST-level language -- servers have no discretion here. - -## Why it matters - -This is the Transfer-Encoding variant of the SP-BEFORE-COLON smuggling technique. If one parser ignores the space and processes chunked encoding while another rejects or ignores the header, they will frame the body differently -- a direct CL/TE desync. - -## Deep Analysis - -### ABNF - -``` -field-line = field-name ":" OWS field-value OWS ; RFC 9112 §5 -field-name = token ; RFC 9110 §5.1 -token = 1*tchar ; RFC 9110 §5.6.2 -``` - -The ABNF for `field-line` shows the colon immediately follows `field-name` with no intervening whitespace. The `OWS` (optional whitespace) only appears **after** the colon, not before it. The space in `Transfer-Encoding : chunked` falls between the field-name and the colon, which is a position where the grammar explicitly permits nothing. - -### RFC Evidence - -> "No whitespace is allowed between the field name and colon. In the past, differences in the handling of such whitespace have led to security vulnerabilities in request routing and response handling." -- RFC 9112 §5.1 - -> "A server MUST reject, with a response status code of 400 (Bad Request), any received request message that contains whitespace between a header field name and colon." -- RFC 9112 §5.1 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 - -### Chain of Reasoning - -1. The test sends `Transfer-Encoding : chunked` (space before the colon) alongside `Content-Length: 5`. -2. RFC 9112 section 5.1 uses **MUST-level** language: the server "MUST reject, with a response status code of 400 (Bad Request), any received request message that contains whitespace between a header field name and colon." -3. The RFC explicitly explains the security rationale: "In the past, differences in the handling of such whitespace have led to security vulnerabilities in request routing and response handling." -4. A lenient parser that strips the space and processes `Transfer-Encoding: chunked` would use chunked framing. A strict parser that sees `Transfer-Encoding ` (with trailing space) as the field-name would not recognize it as `Transfer-Encoding` and would fall back to Content-Length. -5. This is exactly the kind of parser disagreement the RFC warns about. The MUST-level rejection requirement exists precisely because permitting whitespace before the colon creates exploitable ambiguity. -6. The ABNF is unambiguous: `field-line = field-name ":" OWS field-value OWS`. There is no OWS or BWS before the colon. - -### Scored / Unscored Justification - -This test is **scored** (MUST reject with `400`). RFC 9112 section 5.1 contains one of the clearest MUST-level requirements in the specification, explicitly mandating `400 (Bad Request)` for any whitespace between field name and colon. No `AllowConnectionClose` alternative is acceptable because the RFC specifies both the response code (`400`) and the action (reject). The historical security vulnerabilities cited in the RFC underscore why this requirement exists. - -- **Pass (400):** The server correctly rejects the request per the explicit MUST requirement. -- **Fail (2xx or close):** The server failed to issue the required `400` response, either accepting the malformed header or merely closing the connection. - -### Smuggling Attack Scenarios - -- **Field-Name Mismatch Desync:** A lenient front-end strips the space before the colon and sees `Transfer-Encoding: chunked`, using chunked framing. A strict back-end treats `Transfer-Encoding ` (with trailing space) as an unknown header name and falls back to `Content-Length: 5`. The front-end consumes the body as chunked data while the back-end consumes only 5 bytes, leaving attacker-controlled data on the socket as the next request. -- **Proxy Normalization Bypass:** Some proxies normalize headers by removing spaces around colons. The front-end normalizes `Transfer-Encoding : chunked` to `Transfer-Encoding: chunked` before forwarding. The back-end processes the normalized header normally. But if the proxy made its own framing decision based on the **original** malformed header (possibly treating it as unrecognized), the proxy and back-end disagree on body boundaries. -- **WAF Evasion via Space Injection:** Security devices that check for `Transfer-Encoding:` (without a space before the colon) will not match `Transfer-Encoding :` (with the space). The request bypasses the WAF's smuggling detection, but the origin server's lenient parser strips the space and processes chunked encoding, allowing the attacker to smuggle requests past the security layer. - -## Sources - -- [RFC 9112 §5.1](https://www.rfc-editor.org/rfc/rfc9112#section-5.1) +--- +title: "TE Space Before Colon — Request Smuggling" +description: "Transfer-Encoding : chunked — space before the colon. Tested against RFC 9112 §5.1." +weight: 7 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-SP-BEFORE-COLON` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §5.1](https://www.rfc-editor.org/rfc/rfc9112#section-5.1) | +| **Requirement** | MUST | +| **Expected** | `400` or close | + +## What it sends + +`Transfer-Encoding : chunked` — space before the colon. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding : chunked\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +Note the space between `Transfer-Encoding` and the colon. + + +## What the RFC says + +> "No whitespace is allowed between the field name and colon. In the past, differences in the handling of such whitespace have led to security vulnerabilities in request routing and response handling." -- RFC 9112 Section 5.1 + +> "A server MUST reject, with a response status code of 400 (Bad Request), any received request message that contains whitespace between a header field name and colon." -- RFC 9112 Section 5.1 + +This is MUST-level language -- servers have no discretion here. + +## Why it matters + +This is the Transfer-Encoding variant of the SP-BEFORE-COLON smuggling technique. If one parser ignores the space and processes chunked encoding while another rejects or ignores the header, they will frame the body differently -- a direct CL/TE desync. + +## Deep Analysis + +### ABNF + +``` +field-line = field-name ":" OWS field-value OWS ; RFC 9112 §5 +field-name = token ; RFC 9110 §5.1 +token = 1*tchar ; RFC 9110 §5.6.2 +``` + +The ABNF for `field-line` shows the colon immediately follows `field-name` with no intervening whitespace. The `OWS` (optional whitespace) only appears **after** the colon, not before it. The space in `Transfer-Encoding : chunked` falls between the field-name and the colon, which is a position where the grammar explicitly permits nothing. + +### RFC Evidence + +> "No whitespace is allowed between the field name and colon. In the past, differences in the handling of such whitespace have led to security vulnerabilities in request routing and response handling." -- RFC 9112 §5.1 + +> "A server MUST reject, with a response status code of 400 (Bad Request), any received request message that contains whitespace between a header field name and colon." -- RFC 9112 §5.1 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 + +### Chain of Reasoning + +1. The test sends `Transfer-Encoding : chunked` (space before the colon) alongside `Content-Length: 5`. +2. RFC 9112 section 5.1 uses **MUST-level** language: the server "MUST reject, with a response status code of 400 (Bad Request), any received request message that contains whitespace between a header field name and colon." +3. The RFC explicitly explains the security rationale: "In the past, differences in the handling of such whitespace have led to security vulnerabilities in request routing and response handling." +4. A lenient parser that strips the space and processes `Transfer-Encoding: chunked` would use chunked framing. A strict parser that sees `Transfer-Encoding ` (with trailing space) as the field-name would not recognize it as `Transfer-Encoding` and would fall back to Content-Length. +5. This is exactly the kind of parser disagreement the RFC warns about. The MUST-level rejection requirement exists precisely because permitting whitespace before the colon creates exploitable ambiguity. +6. The ABNF is unambiguous: `field-line = field-name ":" OWS field-value OWS`. There is no OWS or BWS before the colon. + +### Scored / Unscored Justification + +This test is **scored** (MUST reject with `400`). RFC 9112 section 5.1 contains one of the clearest MUST-level requirements in the specification, explicitly mandating `400 (Bad Request)` for any whitespace between field name and colon. No `AllowConnectionClose` alternative is acceptable because the RFC specifies both the response code (`400`) and the action (reject). The historical security vulnerabilities cited in the RFC underscore why this requirement exists. + +- **Pass (400):** The server correctly rejects the request per the explicit MUST requirement. +- **Fail (2xx or close):** The server failed to issue the required `400` response, either accepting the malformed header or merely closing the connection. + +### Smuggling Attack Scenarios + +- **Field-Name Mismatch Desync:** A lenient front-end strips the space before the colon and sees `Transfer-Encoding: chunked`, using chunked framing. A strict back-end treats `Transfer-Encoding ` (with trailing space) as an unknown header name and falls back to `Content-Length: 5`. The front-end consumes the body as chunked data while the back-end consumes only 5 bytes, leaving attacker-controlled data on the socket as the next request. +- **Proxy Normalization Bypass:** Some proxies normalize headers by removing spaces around colons. The front-end normalizes `Transfer-Encoding : chunked` to `Transfer-Encoding: chunked` before forwarding. The back-end processes the normalized header normally. But if the proxy made its own framing decision based on the **original** malformed header (possibly treating it as unrecognized), the proxy and back-end disagree on body boundaries. +- **WAF Evasion via Space Injection:** Security devices that check for `Transfer-Encoding:` (without a space before the colon) will not match `Transfer-Encoding :` (with the space). The request bypasses the WAF's smuggling detection, but the origin server's lenient parser strips the space and processes chunked encoding, allowing the attacker to smuggle requests past the security layer. + +## Sources + +- [RFC 9112 §5.1](https://www.rfc-editor.org/rfc/rfc9112#section-5.1) diff --git a/docs/content/docs/smuggling/te-tab-before-value.md b/docs/content/docs/smuggling/te-tab-before-value.md index 2a768f2..cf61bd4 100644 --- a/docs/content/docs/smuggling/te-tab-before-value.md +++ b/docs/content/docs/smuggling/te-tab-before-value.md @@ -1,97 +1,97 @@ ---- -title: "TE-TAB-BEFORE-VALUE" -description: "TE-TAB-BEFORE-VALUE test documentation" -weight: 56 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TE-TAB-BEFORE-VALUE` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §5.6.3](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.3) | -| **Requirement** | Unscored | -| **Expected** | `400` or `2xx` | - -## What it sends - -Transfer-Encoding with a horizontal tab (HTAB) instead of space as the OWS separator before the value, alongside Content-Length. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding:\tchunked\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -The tab character (`\t`, 0x09) separates the colon from `chunked` instead of the usual space (0x20). - - -## What the RFC says - -> "OWS = \*( SP / HTAB ) ; optional whitespace" -- RFC 9110 Section 5.6.3 - -> "The OWS rule is used where zero or more linear whitespace octets might appear." -- RFC 9110 Section 5.6.3 - -Tab (HTAB) is explicitly defined as valid optional whitespace alongside space (SP). A compliant server should parse `Transfer-Encoding:\tchunked` identically to `Transfer-Encoding: chunked`. - -## Why this test is unscored - -HTAB is explicitly valid as OWS per the RFC. A server that accepts `Transfer-Encoding:\tchunked` is RFC-compliant. A server that rejects it is being strict but safe. Since both behaviors are valid, the test cannot be scored. - -**Pass:** Server rejects with `400` (strict, safe -- some parsers only accept SP). -**Warn:** Server accepts and responds `2xx` (RFC-valid per OWS definition). - -## Why it matters - -Despite HTAB being valid OWS per the RFC, some real-world parsers only accept SP (0x20) as whitespace before the field value. When a front-end strips tabs or rejects the header while a back-end accepts it, they disagree on whether Transfer-Encoding is present — creating a CL/TE desync with Content-Length as the fallback. - -## Deep Analysis - -### ABNF - -``` -field-line = field-name ":" OWS field-value OWS ; RFC 9112 §5 -OWS = *( SP / HTAB ) ; RFC 9110 §5.6.3 -Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 -transfer-coding = token ; RFC 9110 §10.1.4 -token = 1*tchar ; RFC 9110 §5.6.2 -``` - -The `OWS` production between the colon and the field-value explicitly includes both SP (0x20) and HTAB (0x09). The `field-line` rule uses `OWS` after the colon, meaning `Transfer-Encoding:\tchunked` has a tab character in the position where OWS is permitted. The ABNF unambiguously permits this. - -### RFC Evidence - -> "OWS = \*( SP / HTAB ) ; optional whitespace" -- RFC 9110 §5.6.3 - -> "A field line value might be preceded and/or followed by optional whitespace (OWS); a single SP preceding the field line value is preferred for consistent readability by humans. The field line value does not include that leading or trailing whitespace." -- RFC 9112 §5.1 - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 - -### Chain of Reasoning - -1. The test sends `Transfer-Encoding:\tchunked` (tab before the value) alongside `Content-Length: 5`. -2. RFC 9110 section 5.6.3 defines OWS as `*( SP / HTAB )`, explicitly including the horizontal tab character. RFC 9112 section 5 uses OWS after the colon in the `field-line` production. -3. RFC 9112 section 5.1 notes that "a single SP preceding the field line value is preferred for consistent readability by humans" -- indicating that SP is the convention, but HTAB is still valid per the grammar. -4. A compliant parser should strip the leading OWS (whether SP or HTAB) and evaluate the field value as `chunked`. This is a valid transfer coding, and the request should be processed with chunked framing. -5. However, real-world parsers often only accept SP (0x20) as whitespace. A parser that treats HTAB as an invalid character in this position may reject the header or include the tab as part of the field value, resulting in `\tchunked` as the transfer coding name -- which does not match `chunked`. -6. The presence of `Content-Length: 5` alongside Transfer-Encoding creates the CL/TE dual-header scenario. If the HTAB causes one parser to not recognize Transfer-Encoding while another parser handles it correctly, the framing disagreement enables smuggling. - -### Scored / Unscored Justification - -This test is **unscored** because HTAB is explicitly valid as OWS per the RFC ABNF. A server that accepts the tab and processes `chunked` is fully RFC-compliant. A server that rejects with `400` is being overly strict but safe. Since both behaviors are defensible and no MUST-level requirement is violated by either, the test cannot be scored. - -- **Pass (400):** Strict rejection -- the server only accepts SP as OWS, which is safer but stricter than the RFC requires. -- **Warn (2xx):** RFC-compliant -- the server correctly parsed the HTAB as valid OWS and processed chunked encoding. - -### Smuggling Attack Scenarios - -- **SP-Only Parser Desync:** A front-end proxy that only recognizes SP as whitespace sees `\tchunked` as part of the field value, treats it as an unrecognized transfer coding, and falls back to Content-Length. A back-end that correctly handles HTAB as OWS sees `chunked` and uses chunked framing. The attacker embeds a smuggled request inside the chunked body. -- **Tab Stripping Inconsistency:** Some proxies strip all whitespace (including tabs) from header values during normalization, while others only strip spaces. If a front-end strips tabs but uses Content-Length for its own framing decision before normalizing, and the normalized `Transfer-Encoding: chunked` is what the back-end receives, the proxy's pre-normalization framing may have consumed different bytes than the back-end expects. -- **Binary Character Confusion:** The HTAB character (0x09) is a control character that some parsers may flag as suspicious or invalid in header values. A WAF that rejects or sanitizes control characters in headers may strip the entire Transfer-Encoding header, while the origin server processes the tab as valid OWS. The WAF's framing analysis (based on Content-Length) diverges from the server's framing (based on chunked encoding). - -## Sources - -- [RFC 9110 §5.6.3](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.3) -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "TE Tab Before Value — Request Smuggling" +description: "Transfer-Encoding with a horizontal tab (HTAB) instead of space as the OWS separator before the value, alongside Content-Length. Tested against RFC 9110 §5.6.3." +weight: 56 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-TAB-BEFORE-VALUE` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §5.6.3](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.3) | +| **Requirement** | Unscored | +| **Expected** | `400` or `2xx` | + +## What it sends + +Transfer-Encoding with a horizontal tab (HTAB) instead of space as the OWS separator before the value, alongside Content-Length. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding:\tchunked\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +The tab character (`\t`, 0x09) separates the colon from `chunked` instead of the usual space (0x20). + + +## What the RFC says + +> "OWS = \*( SP / HTAB ) ; optional whitespace" -- RFC 9110 Section 5.6.3 + +> "The OWS rule is used where zero or more linear whitespace octets might appear." -- RFC 9110 Section 5.6.3 + +Tab (HTAB) is explicitly defined as valid optional whitespace alongside space (SP). A compliant server should parse `Transfer-Encoding:\tchunked` identically to `Transfer-Encoding: chunked`. + +## Why this test is unscored + +HTAB is explicitly valid as OWS per the RFC. A server that accepts `Transfer-Encoding:\tchunked` is RFC-compliant. A server that rejects it is being strict but safe. Since both behaviors are valid, the test cannot be scored. + +**Pass:** Server rejects with `400` (strict, safe -- some parsers only accept SP). +**Warn:** Server accepts and responds `2xx` (RFC-valid per OWS definition). + +## Why it matters + +Despite HTAB being valid OWS per the RFC, some real-world parsers only accept SP (0x20) as whitespace before the field value. When a front-end strips tabs or rejects the header while a back-end accepts it, they disagree on whether Transfer-Encoding is present — creating a CL/TE desync with Content-Length as the fallback. + +## Deep Analysis + +### ABNF + +``` +field-line = field-name ":" OWS field-value OWS ; RFC 9112 §5 +OWS = *( SP / HTAB ) ; RFC 9110 §5.6.3 +Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 +transfer-coding = token ; RFC 9110 §10.1.4 +token = 1*tchar ; RFC 9110 §5.6.2 +``` + +The `OWS` production between the colon and the field-value explicitly includes both SP (0x20) and HTAB (0x09). The `field-line` rule uses `OWS` after the colon, meaning `Transfer-Encoding:\tchunked` has a tab character in the position where OWS is permitted. The ABNF unambiguously permits this. + +### RFC Evidence + +> "OWS = \*( SP / HTAB ) ; optional whitespace" -- RFC 9110 §5.6.3 + +> "A field line value might be preceded and/or followed by optional whitespace (OWS); a single SP preceding the field line value is preferred for consistent readability by humans. The field line value does not include that leading or trailing whitespace." -- RFC 9112 §5.1 + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 + +### Chain of Reasoning + +1. The test sends `Transfer-Encoding:\tchunked` (tab before the value) alongside `Content-Length: 5`. +2. RFC 9110 section 5.6.3 defines OWS as `*( SP / HTAB )`, explicitly including the horizontal tab character. RFC 9112 section 5 uses OWS after the colon in the `field-line` production. +3. RFC 9112 section 5.1 notes that "a single SP preceding the field line value is preferred for consistent readability by humans" -- indicating that SP is the convention, but HTAB is still valid per the grammar. +4. A compliant parser should strip the leading OWS (whether SP or HTAB) and evaluate the field value as `chunked`. This is a valid transfer coding, and the request should be processed with chunked framing. +5. However, real-world parsers often only accept SP (0x20) as whitespace. A parser that treats HTAB as an invalid character in this position may reject the header or include the tab as part of the field value, resulting in `\tchunked` as the transfer coding name -- which does not match `chunked`. +6. The presence of `Content-Length: 5` alongside Transfer-Encoding creates the CL/TE dual-header scenario. If the HTAB causes one parser to not recognize Transfer-Encoding while another parser handles it correctly, the framing disagreement enables smuggling. + +### Scored / Unscored Justification + +This test is **unscored** because HTAB is explicitly valid as OWS per the RFC ABNF. A server that accepts the tab and processes `chunked` is fully RFC-compliant. A server that rejects with `400` is being overly strict but safe. Since both behaviors are defensible and no MUST-level requirement is violated by either, the test cannot be scored. + +- **Pass (400):** Strict rejection -- the server only accepts SP as OWS, which is safer but stricter than the RFC requires. +- **Warn (2xx):** RFC-compliant -- the server correctly parsed the HTAB as valid OWS and processed chunked encoding. + +### Smuggling Attack Scenarios + +- **SP-Only Parser Desync:** A front-end proxy that only recognizes SP as whitespace sees `\tchunked` as part of the field value, treats it as an unrecognized transfer coding, and falls back to Content-Length. A back-end that correctly handles HTAB as OWS sees `chunked` and uses chunked framing. The attacker embeds a smuggled request inside the chunked body. +- **Tab Stripping Inconsistency:** Some proxies strip all whitespace (including tabs) from header values during normalization, while others only strip spaces. If a front-end strips tabs but uses Content-Length for its own framing decision before normalizing, and the normalized `Transfer-Encoding: chunked` is what the back-end receives, the proxy's pre-normalization framing may have consumed different bytes than the back-end expects. +- **Binary Character Confusion:** The HTAB character (0x09) is a control character that some parsers may flag as suspicious or invalid in header values. A WAF that rejects or sanitizes control characters in headers may strip the entire Transfer-Encoding header, while the origin server processes the tab as valid OWS. The WAF's framing analysis (based on Content-Length) diverges from the server's framing (based on chunked encoding). + +## Sources + +- [RFC 9110 §5.6.3](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.3) +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/te-trailing-comma.md b/docs/content/docs/smuggling/te-trailing-comma.md index eb1ef8c..f7d1fc2 100644 --- a/docs/content/docs/smuggling/te-trailing-comma.md +++ b/docs/content/docs/smuggling/te-trailing-comma.md @@ -1,94 +1,94 @@ ---- -title: "TE-TRAILING-COMMA" -description: "TE-TRAILING-COMMA test documentation" -weight: 51 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TE-TRAILING-COMMA` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §5.6.1](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.1) | -| **Requirement** | SHOULD | -| **Expected** | `400` or `2xx` | - -## What it sends - -Transfer-Encoding with a trailing comma after `chunked`, alongside Content-Length. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked,\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -The `Transfer-Encoding` value is `chunked,` — a trailing comma produces an empty list element after `chunked`. - - -## What the RFC says - -> "A recipient MUST parse and ignore a reasonable number of empty list elements: enough to handle common mistakes by senders that merge values, but not so much that they could be used as a denial-of-service mechanism." -- RFC 9110 Section 5.6.1 - -The trailing comma creates an empty list element. Per §5.6.1, the server should strip the empty element and see just `chunked`. However, some parsers reject the value because the trailing comma makes it syntactically unusual, while others strip it and process normally. - -## Pass / Warn - -The RFC requires recipients to ignore empty list elements, so stripping the trailing comma and processing `chunked` is RFC-compliant. However, rejecting the value is the safer choice since the trailing comma creates ambiguity. Both behaviors are valid, but rejection is preferred. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (RFC-valid per Section 5.6.1 empty-element handling). - -## Why it matters - -When Content-Length is also present, parser disagreement on whether `chunked,` is valid Transfer-Encoding creates a CL/TE desync. A parser that rejects the trailing comma falls back to Content-Length framing, while a parser that strips the empty element uses chunked framing. This is the mirror of the leading-comma test (SMUG-TE-LEADING-COMMA) and exploits the same §5.6.1 ambiguity from the opposite direction. - -## Deep Analysis - -### ABNF - -``` -Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 -transfer-coding = token ; RFC 9110 §10.1.4 -token = 1*tchar ; RFC 9110 §5.6.2 -``` - -The `#` list rule allows comma-separated elements. The value `chunked,` consists of the element `chunked` followed by a comma and an implicit empty element. Per the `#` rule semantics in RFC 9110 section 5.6.1, empty list elements (produced by leading, trailing, or consecutive commas) should be ignored by recipients. - -### RFC Evidence - -> "A recipient MUST parse and ignore a reasonable number of empty list elements: enough to handle common mistakes by senders that merge values, but not so much that they could be used as a denial-of-service mechanism." -- RFC 9110 §5.6.1 - -> "A recipient MUST be able to parse the chunked transfer coding (Section 7.1) because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 §6.1 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 - -### Chain of Reasoning - -1. The test sends `Transfer-Encoding: chunked,` alongside `Content-Length: 5`. -2. The trailing comma after `chunked` produces an empty list element. Per RFC 9110 section 5.6.1, recipients MUST parse and ignore a reasonable number of empty list elements. -3. After stripping the empty element, the effective value is `Transfer-Encoding: chunked` -- a valid transfer coding that the server MUST be able to parse per RFC 9112 section 6.1. -4. However, some parsers treat the trailing comma differently. A parser that does not strip empty elements may see `chunked,` as a single token that does not match any registered transfer coding. It would then fall back to Content-Length. -5. Other parsers may interpret the trailing comma as indicating an additional (empty or missing) transfer coding after `chunked`, possibly treating the entire value as malformed. -6. The dual presence of Transfer-Encoding and Content-Length triggers RFC 9112 section 6.3, which states the message "ought to be handled as an error." -7. This test is the mirror image of SMUG-TE-LEADING-COMMA, exploiting the same empty-element ambiguity from the trailing direction. - -### Scored / SHOULD Justification - -This test is scored as **SHOULD** reject because, while RFC 9110 section 5.6.1 requires recipients to handle empty list elements (making `2xx` defensible), the trailing comma creates ambiguity in how different parsers interpret the Transfer-Encoding value. Rejection is the safer behavior, especially given the CL/TE dual-header scenario. - -- **Pass (400):** Strict rejection -- the server refuses to process the syntactically unusual trailing comma. -- **Warn (2xx):** RFC-compliant per section 5.6.1 -- the server correctly stripped the empty list element and processed `chunked`. - -### Smuggling Attack Scenarios - -- **Trailing-Comma Fallback Desync:** A front-end proxy that does not strip empty list elements sees `chunked,` as an unrecognized transfer coding and falls back to Content-Length framing. A back-end that correctly strips the trailing empty element sees `chunked` and uses chunked framing. The attacker can embed a second request in the chunked body that the front-end never parses. -- **Comma Suffix as WAF Bypass:** WAFs and intrusion detection systems that check for exact `Transfer-Encoding: chunked` will not match `Transfer-Encoding: chunked,`. The trailing comma bypasses the security check, but the origin server normalizes the value and processes chunked encoding, allowing smuggled payloads to reach the back-end. -- **Inconsistent Element Counting:** Some parsers count the number of transfer codings by splitting on commas. With `chunked,`, they count two elements: `chunked` and an empty string. A parser that requires exactly one transfer coding may reject the header, while a parser that ignores empty elements sees one coding. This counting disagreement can cause one side of a proxy chain to reject and the other to accept, creating connection state mismatches. - -## Sources - -- [RFC 9110 §5.6.1 -- Lists](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.1) -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "TE Trailing Comma — Request Smuggling" +description: "Transfer-Encoding with a trailing comma after chunked, alongside Content-Length. Tested against RFC 9110 §5.6.1." +weight: 51 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-TRAILING-COMMA` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §5.6.1](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.1) | +| **Requirement** | SHOULD | +| **Expected** | `400` or `2xx` | + +## What it sends + +Transfer-Encoding with a trailing comma after `chunked`, alongside Content-Length. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked,\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +The `Transfer-Encoding` value is `chunked,` — a trailing comma produces an empty list element after `chunked`. + + +## What the RFC says + +> "A recipient MUST parse and ignore a reasonable number of empty list elements: enough to handle common mistakes by senders that merge values, but not so much that they could be used as a denial-of-service mechanism." -- RFC 9110 Section 5.6.1 + +The trailing comma creates an empty list element. Per §5.6.1, the server should strip the empty element and see just `chunked`. However, some parsers reject the value because the trailing comma makes it syntactically unusual, while others strip it and process normally. + +## Pass / Warn + +The RFC requires recipients to ignore empty list elements, so stripping the trailing comma and processing `chunked` is RFC-compliant. However, rejecting the value is the safer choice since the trailing comma creates ambiguity. Both behaviors are valid, but rejection is preferred. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (RFC-valid per Section 5.6.1 empty-element handling). + +## Why it matters + +When Content-Length is also present, parser disagreement on whether `chunked,` is valid Transfer-Encoding creates a CL/TE desync. A parser that rejects the trailing comma falls back to Content-Length framing, while a parser that strips the empty element uses chunked framing. This is the mirror of the leading-comma test (SMUG-TE-LEADING-COMMA) and exploits the same §5.6.1 ambiguity from the opposite direction. + +## Deep Analysis + +### ABNF + +``` +Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 +transfer-coding = token ; RFC 9110 §10.1.4 +token = 1*tchar ; RFC 9110 §5.6.2 +``` + +The `#` list rule allows comma-separated elements. The value `chunked,` consists of the element `chunked` followed by a comma and an implicit empty element. Per the `#` rule semantics in RFC 9110 section 5.6.1, empty list elements (produced by leading, trailing, or consecutive commas) should be ignored by recipients. + +### RFC Evidence + +> "A recipient MUST parse and ignore a reasonable number of empty list elements: enough to handle common mistakes by senders that merge values, but not so much that they could be used as a denial-of-service mechanism." -- RFC 9110 §5.6.1 + +> "A recipient MUST be able to parse the chunked transfer coding (Section 7.1) because it plays a crucial role in framing messages when the content size is not known in advance." -- RFC 9112 §6.1 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 + +### Chain of Reasoning + +1. The test sends `Transfer-Encoding: chunked,` alongside `Content-Length: 5`. +2. The trailing comma after `chunked` produces an empty list element. Per RFC 9110 section 5.6.1, recipients MUST parse and ignore a reasonable number of empty list elements. +3. After stripping the empty element, the effective value is `Transfer-Encoding: chunked` -- a valid transfer coding that the server MUST be able to parse per RFC 9112 section 6.1. +4. However, some parsers treat the trailing comma differently. A parser that does not strip empty elements may see `chunked,` as a single token that does not match any registered transfer coding. It would then fall back to Content-Length. +5. Other parsers may interpret the trailing comma as indicating an additional (empty or missing) transfer coding after `chunked`, possibly treating the entire value as malformed. +6. The dual presence of Transfer-Encoding and Content-Length triggers RFC 9112 section 6.3, which states the message "ought to be handled as an error." +7. This test is the mirror image of SMUG-TE-LEADING-COMMA, exploiting the same empty-element ambiguity from the trailing direction. + +### Scored / SHOULD Justification + +This test is scored as **SHOULD** reject because, while RFC 9110 section 5.6.1 requires recipients to handle empty list elements (making `2xx` defensible), the trailing comma creates ambiguity in how different parsers interpret the Transfer-Encoding value. Rejection is the safer behavior, especially given the CL/TE dual-header scenario. + +- **Pass (400):** Strict rejection -- the server refuses to process the syntactically unusual trailing comma. +- **Warn (2xx):** RFC-compliant per section 5.6.1 -- the server correctly stripped the empty list element and processed `chunked`. + +### Smuggling Attack Scenarios + +- **Trailing-Comma Fallback Desync:** A front-end proxy that does not strip empty list elements sees `chunked,` as an unrecognized transfer coding and falls back to Content-Length framing. A back-end that correctly strips the trailing empty element sees `chunked` and uses chunked framing. The attacker can embed a second request in the chunked body that the front-end never parses. +- **Comma Suffix as WAF Bypass:** WAFs and intrusion detection systems that check for exact `Transfer-Encoding: chunked` will not match `Transfer-Encoding: chunked,`. The trailing comma bypasses the security check, but the origin server normalizes the value and processes chunked encoding, allowing smuggled payloads to reach the back-end. +- **Inconsistent Element Counting:** Some parsers count the number of transfer codings by splitting on commas. With `chunked,`, they count two elements: `chunked` and an empty string. A parser that requires exactly one transfer coding may reject the header, while a parser that ignores empty elements sees one coding. This counting disagreement can cause one side of a proxy chain to reject and the other to accept, creating connection state mismatches. + +## Sources + +- [RFC 9110 §5.6.1 -- Lists](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.1) +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/te-trailing-space.md b/docs/content/docs/smuggling/te-trailing-space.md index cfbcccd..26688cb 100644 --- a/docs/content/docs/smuggling/te-trailing-space.md +++ b/docs/content/docs/smuggling/te-trailing-space.md @@ -1,96 +1,96 @@ ---- -title: "TE-TRAILING-SPACE" -description: "TE-TRAILING-SPACE test documentation" -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** | 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 - -`Transfer-Encoding: chunked ` (with a trailing space). The value does not exactly match `chunked`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked \r\n -Content-Length: 5\r\n -\r\n -hello -``` - -Note the trailing space after `chunked`. - - -## What the RFC says - -> "A field value does not include leading or trailing whitespace. When a specific version of HTTP allows such whitespace to appear in a message, a field parsing implementation MUST exclude such whitespace prior to evaluating the field value." -- RFC 9110 Section 5.5 - -> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 Section 6.1 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 Section 6.3 - -RFC 9110 Section 5.5 requires stripping leading/trailing whitespace from the field value before evaluation. After stripping, the value is `chunked` -- a valid transfer coding. However, some parsers may not strip OWS correctly and treat `chunked ` (with trailing space) as an unrecognized coding name. - -## Why it matters - -If one parser trims trailing whitespace and recognizes `chunked` while another treats `chunked ` as an unknown encoding and falls back to Content-Length, they will disagree on body framing -- a CL/TE desync. - -## Deep Analysis - -### ABNF - -``` -field-line = field-name ":" OWS field-value OWS ; RFC 9112 §5 -field-value = *field-content ; RFC 9110 §5.5 -field-content = field-vchar - [ 1*( SP / HTAB / field-vchar ) field-vchar ] -Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 -transfer-coding = token ; RFC 9110 §10.1.4 -token = 1*tchar ; RFC 9110 §5.6.2 -``` - -The `field-line` rule includes trailing `OWS` after `field-value`. Per RFC 9112 section 5.1, leading and trailing OWS is excluded when extracting the field value. The critical question is whether the trailing space in `chunked ` falls within the OWS that gets stripped, or within the `field-value` as part of the transfer-coding token. - -### RFC Evidence - -> "A field value does not include leading or trailing whitespace. When a specific version of HTTP allows such whitespace to appear in a message, a field parsing implementation MUST exclude such whitespace prior to evaluating the field value." -- RFC 9110 §5.5 - -> "A field line value might be preceded and/or followed by optional whitespace (OWS); a single SP preceding the field line value is preferred for consistent readability by humans. The field line value does not include that leading or trailing whitespace." -- RFC 9112 §5.1 - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 - -### Chain of Reasoning - -1. The test sends `Transfer-Encoding: chunked ` (trailing space) alongside `Content-Length: 5`. -2. RFC 9110 section 5.5 states: "A field value does not include leading or trailing whitespace." The field parsing implementation "MUST exclude such whitespace prior to evaluating the field value." -3. RFC 9112 section 5.1 reinforces this: the trailing OWS in `field-line = field-name ":" OWS field-value OWS` is not part of the field value. -4. After stripping trailing OWS, the field value should be `chunked` -- a valid, recognized transfer coding. -5. However, real-world parsers may not properly strip trailing OWS from the field value. A parser that includes the trailing space sees `chunked ` as the transfer-coding token, which does not match `chunked` in a byte-for-byte comparison. It may treat this as an unrecognized encoding. -6. The dual presence of Transfer-Encoding and Content-Length triggers RFC 9112 section 6.3. If one parser strips the trailing space and uses chunked framing while another does not strip it and falls back to Content-Length, the framing disagreement enables smuggling. - -### Scored / Unscored Justification - -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`/`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 - -- **OWS Stripping Disagreement:** A front-end proxy does not strip trailing OWS and sees `chunked ` as an unrecognized transfer coding, falling back to Content-Length. A back-end correctly strips the trailing space per RFC 9110 section 5.5 and processes `chunked` framing. The desync lets the attacker embed a smuggled request in the chunked body. -- **Trailing Space as Encoding Obfuscation:** Some WAFs check for `Transfer-Encoding: chunked` (exact match). The trailing space causes the check to fail, and the WAF treats Transfer-Encoding as absent. The origin server strips the space and processes chunked encoding normally, allowing smuggled requests to bypass the WAF. -- **Whitespace-Sensitive Token Comparison:** Parsers that compare transfer-coding names using byte-for-byte matching (rather than stripping OWS first) will see `chunked ` (7 bytes) as different from `chunked` (7 bytes without space). If such a parser is on one side of a proxy chain and a whitespace-tolerant parser is on the other, the attacker controls which framing mechanism each parser uses. - -## 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) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "TE Trailing Space — Request Smuggling" +description: "Transfer-Encoding: chunked (with a trailing space). The value does not exactly match chunked. Tested against RFC 9110 §5.5." +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** | 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 + +`Transfer-Encoding: chunked ` (with a trailing space). The value does not exactly match `chunked`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked \r\n +Content-Length: 5\r\n +\r\n +hello +``` + +Note the trailing space after `chunked`. + + +## What the RFC says + +> "A field value does not include leading or trailing whitespace. When a specific version of HTTP allows such whitespace to appear in a message, a field parsing implementation MUST exclude such whitespace prior to evaluating the field value." -- RFC 9110 Section 5.5 + +> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 Section 6.1 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 Section 6.3 + +RFC 9110 Section 5.5 requires stripping leading/trailing whitespace from the field value before evaluation. After stripping, the value is `chunked` -- a valid transfer coding. However, some parsers may not strip OWS correctly and treat `chunked ` (with trailing space) as an unrecognized coding name. + +## Why it matters + +If one parser trims trailing whitespace and recognizes `chunked` while another treats `chunked ` as an unknown encoding and falls back to Content-Length, they will disagree on body framing -- a CL/TE desync. + +## Deep Analysis + +### ABNF + +``` +field-line = field-name ":" OWS field-value OWS ; RFC 9112 §5 +field-value = *field-content ; RFC 9110 §5.5 +field-content = field-vchar + [ 1*( SP / HTAB / field-vchar ) field-vchar ] +Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 +transfer-coding = token ; RFC 9110 §10.1.4 +token = 1*tchar ; RFC 9110 §5.6.2 +``` + +The `field-line` rule includes trailing `OWS` after `field-value`. Per RFC 9112 section 5.1, leading and trailing OWS is excluded when extracting the field value. The critical question is whether the trailing space in `chunked ` falls within the OWS that gets stripped, or within the `field-value` as part of the transfer-coding token. + +### RFC Evidence + +> "A field value does not include leading or trailing whitespace. When a specific version of HTTP allows such whitespace to appear in a message, a field parsing implementation MUST exclude such whitespace prior to evaluating the field value." -- RFC 9110 §5.5 + +> "A field line value might be preceded and/or followed by optional whitespace (OWS); a single SP preceding the field line value is preferred for consistent readability by humans. The field line value does not include that leading or trailing whitespace." -- RFC 9112 §5.1 + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 + +### Chain of Reasoning + +1. The test sends `Transfer-Encoding: chunked ` (trailing space) alongside `Content-Length: 5`. +2. RFC 9110 section 5.5 states: "A field value does not include leading or trailing whitespace." The field parsing implementation "MUST exclude such whitespace prior to evaluating the field value." +3. RFC 9112 section 5.1 reinforces this: the trailing OWS in `field-line = field-name ":" OWS field-value OWS` is not part of the field value. +4. After stripping trailing OWS, the field value should be `chunked` -- a valid, recognized transfer coding. +5. However, real-world parsers may not properly strip trailing OWS from the field value. A parser that includes the trailing space sees `chunked ` as the transfer-coding token, which does not match `chunked` in a byte-for-byte comparison. It may treat this as an unrecognized encoding. +6. The dual presence of Transfer-Encoding and Content-Length triggers RFC 9112 section 6.3. If one parser strips the trailing space and uses chunked framing while another does not strip it and falls back to Content-Length, the framing disagreement enables smuggling. + +### Scored / Unscored Justification + +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`/`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 + +- **OWS Stripping Disagreement:** A front-end proxy does not strip trailing OWS and sees `chunked ` as an unrecognized transfer coding, falling back to Content-Length. A back-end correctly strips the trailing space per RFC 9110 section 5.5 and processes `chunked` framing. The desync lets the attacker embed a smuggled request in the chunked body. +- **Trailing Space as Encoding Obfuscation:** Some WAFs check for `Transfer-Encoding: chunked` (exact match). The trailing space causes the check to fail, and the WAF treats Transfer-Encoding as absent. The origin server strips the space and processes chunked encoding normally, allowing smuggled requests to bypass the WAF. +- **Whitespace-Sensitive Token Comparison:** Parsers that compare transfer-coding names using byte-for-byte matching (rather than stripping OWS first) will see `chunked ` (7 bytes) as different from `chunked` (7 bytes without space). If such a parser is on one side of a proxy chain and a whitespace-tolerant parser is on the other, the attacker controls which framing mechanism each parser uses. + +## 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) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/te-vtab.md b/docs/content/docs/smuggling/te-vtab.md index 451a8c6..dc10962 100644 --- a/docs/content/docs/smuggling/te-vtab.md +++ b/docs/content/docs/smuggling/te-vtab.md @@ -1,35 +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) +--- +title: "TE Vertical-Tab — Request Smuggling" +description: "Transfer-Encoding: chunked with Content-Length present. Tested against RFC 9110 §5.5." +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 39c9ff6..a7e698c 100644 --- a/docs/content/docs/smuggling/te-xchunked.md +++ b/docs/content/docs/smuggling/te-xchunked.md @@ -1,89 +1,89 @@ ---- -title: "TE-XCHUNKED" -description: "TE-XCHUNKED test documentation" -weight: 5 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TE-XCHUNKED` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **Requirement** | MUST reject | -| **Expected** | `400`/`501` or close | - -## What it sends - -`Transfer-Encoding: xchunked` with a Content-Length header. The TE value `xchunked` is not a recognized encoding. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: xchunked\r\n -Content-Length: 5\r\n -\r\n -hello -``` - - -## What the RFC says - -> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 Section 6.1 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 Section 6.3 - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 Section 6.1 - -## Why it matters - -If the front-end ignores the unknown TE and uses CL, but the back-end strips the `x` and processes it as `chunked`, a smuggling vector exists. Some real-world proxies have exhibited this exact behavior. - -## Deep Analysis - -### ABNF - -``` -Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 -transfer-coding = token ; RFC 9110 §10.1.4 -token = 1*tchar ; RFC 9110 §5.6.2 -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" - / DIGIT / ALPHA -``` - -The token `xchunked` is syntactically valid per the ABNF -- it consists entirely of ALPHA characters, all of which are valid `tchar`. However, `xchunked` is not a registered transfer coding in the IANA HTTP Transfer Coding registry. The only similarity to `chunked` is visual; syntactically, `xchunked` is an entirely different token. - -### RFC Evidence - -> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 §6.1 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 - -### Chain of Reasoning - -1. The test sends `Transfer-Encoding: xchunked` alongside `Content-Length: 5`. -2. The token `xchunked` is not registered in the HTTP Transfer Coding registry. It is an unknown transfer coding. -3. RFC 9112 section 6.1 states that a server receiving an unrecognized transfer coding SHOULD respond with `501 (Not Implemented)`. While this is a SHOULD (not MUST), rejecting is the expected behavior. -4. The presence of both Transfer-Encoding and Content-Length triggers RFC 9112 section 6.3, which states that Transfer-Encoding overrides Content-Length and such a message "ought to be handled as an error." -5. RFC 9112 section 6.1 further requires the server to close the connection after responding to any request containing both headers, regardless of how it processes the request. This is a MUST-level requirement. -6. The `xchunked` value is specifically designed to test whether servers perform fuzzy matching or prefix stripping on transfer coding names. Some implementations have been observed to strip an `x` prefix (treating it as an extension marker, similar to MIME `x-` prefixes) and process the remainder as `chunked`. - -### Scored / Unscored Justification - -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/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 - -- **Prefix-Stripping Desync:** An attacker sends `Transfer-Encoding: xchunked`. A front-end proxy does not recognize the coding and falls back to Content-Length framing. A back-end with a buggy parser strips the `x` prefix and processes `chunked` framing. The front-end read 5 bytes using Content-Length; the back-end reads until a chunked terminator. The attacker controls the boundary difference. -- **Fuzzy Matching Exploit:** Some server implementations perform approximate matching on transfer coding names (e.g., substring search for "chunked"). These servers would process `xchunked`, `chunked1`, or `_chunked` as valid chunked encoding. If a strict front-end rejects the unrecognized coding while a fuzzy-matching back-end accepts it, the desync enables smuggling. -- **Unknown-TE Fallback Behavior:** Different servers handle unknown transfer codings differently: some respond with `501`, some with `400`, some close the connection, and some ignore the header entirely and fall back to Content-Length. An attacker can probe a proxy chain to find a combination where one hop rejects and another accepts, then exploit the behavioral gap for smuggling. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +--- +title: "TE X-Chunked — Request Smuggling" +description: "Transfer-Encoding: xchunked with a Content-Length header. The TE value xchunked is not a recognized encoding. Tested against RFC 9112 §6.1." +weight: 5 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TE-XCHUNKED` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **Requirement** | MUST reject | +| **Expected** | `400`/`501` or close | + +## What it sends + +`Transfer-Encoding: xchunked` with a Content-Length header. The TE value `xchunked` is not a recognized encoding. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: xchunked\r\n +Content-Length: 5\r\n +\r\n +hello +``` + + +## What the RFC says + +> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 Section 6.1 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 Section 6.3 + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 Section 6.1 + +## Why it matters + +If the front-end ignores the unknown TE and uses CL, but the back-end strips the `x` and processes it as `chunked`, a smuggling vector exists. Some real-world proxies have exhibited this exact behavior. + +## Deep Analysis + +### ABNF + +``` +Transfer-Encoding = #transfer-coding ; RFC 9112 §6.1 +transfer-coding = token ; RFC 9110 §10.1.4 +token = 1*tchar ; RFC 9110 §5.6.2 +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + / DIGIT / ALPHA +``` + +The token `xchunked` is syntactically valid per the ABNF -- it consists entirely of ALPHA characters, all of which are valid `tchar`. However, `xchunked` is not a registered transfer coding in the IANA HTTP Transfer Coding registry. The only similarity to `chunked` is visual; syntactically, `xchunked` is an entirely different token. + +### RFC Evidence + +> "A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented)." -- RFC 9112 §6.1 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 + +### Chain of Reasoning + +1. The test sends `Transfer-Encoding: xchunked` alongside `Content-Length: 5`. +2. The token `xchunked` is not registered in the HTTP Transfer Coding registry. It is an unknown transfer coding. +3. RFC 9112 section 6.1 states that a server receiving an unrecognized transfer coding SHOULD respond with `501 (Not Implemented)`. While this is a SHOULD (not MUST), rejecting is the expected behavior. +4. The presence of both Transfer-Encoding and Content-Length triggers RFC 9112 section 6.3, which states that Transfer-Encoding overrides Content-Length and such a message "ought to be handled as an error." +5. RFC 9112 section 6.1 further requires the server to close the connection after responding to any request containing both headers, regardless of how it processes the request. This is a MUST-level requirement. +6. The `xchunked` value is specifically designed to test whether servers perform fuzzy matching or prefix stripping on transfer coding names. Some implementations have been observed to strip an `x` prefix (treating it as an extension marker, similar to MIME `x-` prefixes) and process the remainder as `chunked`. + +### Scored / Unscored Justification + +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/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 + +- **Prefix-Stripping Desync:** An attacker sends `Transfer-Encoding: xchunked`. A front-end proxy does not recognize the coding and falls back to Content-Length framing. A back-end with a buggy parser strips the `x` prefix and processes `chunked` framing. The front-end read 5 bytes using Content-Length; the back-end reads until a chunked terminator. The attacker controls the boundary difference. +- **Fuzzy Matching Exploit:** Some server implementations perform approximate matching on transfer coding names (e.g., substring search for "chunked"). These servers would process `xchunked`, `chunked1`, or `_chunked` as valid chunked encoding. If a strict front-end rejects the unrecognized coding while a fuzzy-matching back-end accepts it, the desync enables smuggling. +- **Unknown-TE Fallback Behavior:** Different servers handle unknown transfer codings differently: some respond with `501`, some with `400`, some close the connection, and some ignore the header entirely and fall back to Content-Length. An attacker can probe a proxy chain to find a combination where one hop rejects and another accepts, then exploit the behavioral gap for smuggling. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) diff --git a/docs/content/docs/smuggling/tecl-conn-close.md b/docs/content/docs/smuggling/tecl-conn-close.md index 0f85712..2b73118 100644 --- a/docs/content/docs/smuggling/tecl-conn-close.md +++ b/docs/content/docs/smuggling/tecl-conn-close.md @@ -1,62 +1,62 @@ ---- -title: "TECL-CONN-CLOSE" -description: "TECL-CONN-CLOSE sequence test documentation" -weight: 11 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TECL-CONN-CLOSE` | -| **Category** | Smuggling | -| **Type** | Sequence (2 steps) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or `2xx` + connection close | - -## What it does - -This is a **sequence test** — it sends multiple requests on the same TCP connection to verify server behavior across the full exchange. It is a mirror of [CLTE-CONN-CLOSE](/docs/smuggling/clte-conn-close/) with the header order reversed. - -### Step 1: Ambiguous POST (TE+CL) - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -Content-Length: 5\r\n -\r\n -0\r\n -\r\n -``` - -A POST with `Transfer-Encoding: chunked` listed **before** `Content-Length: 5`. Some parsers treat headers differently depending on order. The chunked body is the `0` terminator (5 bytes), matching the CL value. - -### Step 2: Follow-up GET - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -A normal GET sent on the same connection. This step only executes if the connection is still open after step 1. - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. **Regardless, the server MUST close the connection after responding to such a request** to avoid the potential attacks." — RFC 9112 §6.1 - -The MUST-close requirement applies regardless of header order. This test verifies servers don't accidentally rely on header ordering when deciding whether to close. - -## Why it matters - -Some servers process headers in order and may handle `TE, CL` differently from `CL, TE`. If a server only triggers its MUST-close logic when `Content-Length` appears first, the reversed order could bypass the protection, leaving the connection open for smuggling. - -## Verdicts - -- **Pass** — Server returns `400` (rejected outright), OR returns `2xx` and closes the connection (step 2 never executes) -- **Fail** — Server returns `2xx` and keeps the connection open (step 2 executes and gets a response) - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "TE/CL Conn Close — Request Smuggling" +description: "This is a sequence test — it sends multiple requests on the same TCP connection to verify server behavior across the full exchange. It is a mirror of CLTE-CONN-CLOSE with the header order…" +weight: 11 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TECL-CONN-CLOSE` | +| **Category** | Smuggling | +| **Type** | Sequence (2 steps) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or `2xx` + connection close | + +## What it does + +This is a **sequence test** — it sends multiple requests on the same TCP connection to verify server behavior across the full exchange. It is a mirror of [CLTE-CONN-CLOSE](/docs/smuggling/clte-conn-close/) with the header order reversed. + +### Step 1: Ambiguous POST (TE+CL) + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +Content-Length: 5\r\n +\r\n +0\r\n +\r\n +``` + +A POST with `Transfer-Encoding: chunked` listed **before** `Content-Length: 5`. Some parsers treat headers differently depending on order. The chunked body is the `0` terminator (5 bytes), matching the CL value. + +### Step 2: Follow-up GET + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +A normal GET sent on the same connection. This step only executes if the connection is still open after step 1. + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. **Regardless, the server MUST close the connection after responding to such a request** to avoid the potential attacks." — RFC 9112 §6.1 + +The MUST-close requirement applies regardless of header order. This test verifies servers don't accidentally rely on header ordering when deciding whether to close. + +## Why it matters + +Some servers process headers in order and may handle `TE, CL` differently from `CL, TE`. If a server only triggers its MUST-close logic when `Content-Length` appears first, the reversed order could bypass the protection, leaving the connection open for smuggling. + +## Verdicts + +- **Pass** — Server returns `400` (rejected outright), OR returns `2xx` and closes the connection (step 2 never executes) +- **Fail** — Server returns `2xx` and keeps the connection open (step 2 executes and gets a response) + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/smuggling/tecl-desync.md b/docs/content/docs/smuggling/tecl-desync.md index dd05bbf..1abf87c 100644 --- a/docs/content/docs/smuggling/tecl-desync.md +++ b/docs/content/docs/smuggling/tecl-desync.md @@ -1,66 +1,66 @@ ---- -title: "TECL-DESYNC" -description: "TECL-DESYNC sequence test documentation" -weight: 14 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TECL-DESYNC` | -| **Category** | Smuggling | -| **Type** | Sequence (2 steps) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or connection close | - -## What it does - -This is a **sequence test** that detects TE.CL request boundary desynchronization — the reverse of the classic CL.TE smuggling attack. - -### Step 1: Poison POST (TE terminates early, CL=30) - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -Content-Length: 30\r\n -\r\n -0\r\n -\r\n -X -``` - -The `Transfer-Encoding: chunked` body terminates at `0\r\n\r\n` (5 bytes), but `Content-Length` claims 30 bytes. The extra `X` sits after the chunked terminator. - -- If the server uses **TE**: reads the chunked terminator (5 bytes), body done. Still expects 25 more bytes per CL — `X` and any subsequent data become part of the expected body or a new request. -- If the server uses **CL**: waits for 30 bytes total, which never arrive (timeout). - -### Step 2: Follow-up GET - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -\r\n -``` - -Sent immediately after step 1. If the server used TE and left `X` on the wire, it sees `XGET / HTTP/1.1` — a malformed request that triggers a 400. - -## What the RFC says - -> "**Regardless, the server MUST close the connection after responding to such a request** to avoid the potential attacks." — RFC 9112 §6.1 - -## Why it matters - -In a proxy chain where the front-end uses CL and the back-end uses TE, this pattern allows an attacker to smuggle a request by placing it after the chunked terminator but within the CL-declared body. This test verifies the server doesn't leave the connection in an ambiguous state. - -## Verdicts - -- **Pass** — Server returns `400` (rejected outright), OR closes the connection (step 2 never executes) -- **Fail** — Step 2 executes and returns `400` (desync confirmed — poison byte merged with GET) -- **Fail** — Step 2 executes and returns `2xx` (MUST-close violated, connection stayed open) - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [RFC 9112 §11.2](https://www.rfc-editor.org/rfc/rfc9112#section-11.2) +--- +title: "TE/CL Desync — Request Smuggling" +description: "This is a sequence test that detects TE.CL request boundary desynchronization — the reverse of the classic CL.TE smuggling attack. Tested against RFC 9112 §6.1." +weight: 14 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TECL-DESYNC` | +| **Category** | Smuggling | +| **Type** | Sequence (2 steps) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or connection close | + +## What it does + +This is a **sequence test** that detects TE.CL request boundary desynchronization — the reverse of the classic CL.TE smuggling attack. + +### Step 1: Poison POST (TE terminates early, CL=30) + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +Content-Length: 30\r\n +\r\n +0\r\n +\r\n +X +``` + +The `Transfer-Encoding: chunked` body terminates at `0\r\n\r\n` (5 bytes), but `Content-Length` claims 30 bytes. The extra `X` sits after the chunked terminator. + +- If the server uses **TE**: reads the chunked terminator (5 bytes), body done. Still expects 25 more bytes per CL — `X` and any subsequent data become part of the expected body or a new request. +- If the server uses **CL**: waits for 30 bytes total, which never arrive (timeout). + +### Step 2: Follow-up GET + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +\r\n +``` + +Sent immediately after step 1. If the server used TE and left `X` on the wire, it sees `XGET / HTTP/1.1` — a malformed request that triggers a 400. + +## What the RFC says + +> "**Regardless, the server MUST close the connection after responding to such a request** to avoid the potential attacks." — RFC 9112 §6.1 + +## Why it matters + +In a proxy chain where the front-end uses CL and the back-end uses TE, this pattern allows an attacker to smuggle a request by placing it after the chunked terminator but within the CL-declared body. This test verifies the server doesn't leave the connection in an ambiguous state. + +## Verdicts + +- **Pass** — Server returns `400` (rejected outright), OR closes the connection (step 2 never executes) +- **Fail** — Step 2 executes and returns `400` (desync confirmed — poison byte merged with GET) +- **Fail** — Step 2 executes and returns `2xx` (MUST-close violated, connection stayed open) + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [RFC 9112 §11.2](https://www.rfc-editor.org/rfc/rfc9112#section-11.2) diff --git a/docs/content/docs/smuggling/tecl-pipeline.md b/docs/content/docs/smuggling/tecl-pipeline.md index ccbd5af..7c327cc 100644 --- a/docs/content/docs/smuggling/tecl-pipeline.md +++ b/docs/content/docs/smuggling/tecl-pipeline.md @@ -1,56 +1,56 @@ ---- -title: "TECL-PIPELINE" -description: "TECL-PIPELINE test documentation" -weight: 9 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TECL-PIPELINE` | -| **Category** | Smuggling | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MAY | -| **Expected** | `400` or close preferred; `2xx` acceptable | - -## What it sends - -The reverse of CL.TE — a request with `Transfer-Encoding: chunked` listed first, plus a conflicting `Content-Length`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -Content-Length: 30\r\n -\r\n -0\r\n -\r\n -``` - -A TE parser sees the `0` chunk as end-of-body (5 bytes consumed). A CL parser tries to read 30 bytes, consuming far more than the chunked body. The disagreement is what enables the TE.CL smuggling variant. - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." — RFC 9112 §6.1 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling (Section 11.2) or response splitting (Section 11.1) and ought to be handled as an error." — RFC 9112 §6.3 - -## Why it matters - -Together with CL.TE, this covers both orderings of the dual-header conflict. A proxy chain where one hop prefers TE and the other prefers CL is vulnerable to this variant. Rejecting the request outright is the safest defense. - -## Verdicts - -- **Pass** — Server rejects with `400` or closes the connection (safest behavior) -- **Warn** — Server responds with `2xx` (RFC-compliant if it processes via TE and closes the connection, but the lenient path) -- **Fail** — Any other response - -## Scored / Unscored Justification - -This test is **scored**. Although the RFC uses MAY language, there is a clear preferred outcome: rejecting the ambiguous request is safer than accepting it. The reasoning mirrors CLTE-PIPELINE exactly. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) -- [PortSwigger — HTTP Request Smuggling](https://portswigger.net/web-security/request-smuggling) +--- +title: "TE/CL Pipeline — Request Smuggling" +description: "The reverse of CL.TE — a request with Transfer-Encoding: chunked listed first, plus a conflicting Content-Length. Tested against RFC 9112 §6.1." +weight: 9 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TECL-PIPELINE` | +| **Category** | Smuggling | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MAY | +| **Expected** | `400` or close preferred; `2xx` acceptable | + +## What it sends + +The reverse of CL.TE — a request with `Transfer-Encoding: chunked` listed first, plus a conflicting `Content-Length`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +Content-Length: 30\r\n +\r\n +0\r\n +\r\n +``` + +A TE parser sees the `0` chunk as end-of-body (5 bytes consumed). A CL parser tries to read 30 bytes, consuming far more than the chunked body. The disagreement is what enables the TE.CL smuggling variant. + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." — RFC 9112 §6.1 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling (Section 11.2) or response splitting (Section 11.1) and ought to be handled as an error." — RFC 9112 §6.3 + +## Why it matters + +Together with CL.TE, this covers both orderings of the dual-header conflict. A proxy chain where one hop prefers TE and the other prefers CL is vulnerable to this variant. Rejecting the request outright is the safest defense. + +## Verdicts + +- **Pass** — Server rejects with `400` or closes the connection (safest behavior) +- **Warn** — Server responds with `2xx` (RFC-compliant if it processes via TE and closes the connection, but the lenient path) +- **Fail** — Any other response + +## Scored / Unscored Justification + +This test is **scored**. Although the RFC uses MAY language, there is a clear preferred outcome: rejecting the ambiguous request is safer than accepting it. The reasoning mirrors CLTE-PIPELINE exactly. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) +- [PortSwigger — HTTP Request Smuggling](https://portswigger.net/web-security/request-smuggling) diff --git a/docs/content/docs/smuggling/tecl-smuggled-get.md b/docs/content/docs/smuggling/tecl-smuggled-get.md index 16295a3..0fbb2fd 100644 --- a/docs/content/docs/smuggling/tecl-smuggled-get.md +++ b/docs/content/docs/smuggling/tecl-smuggled-get.md @@ -1,65 +1,65 @@ ---- -title: "TECL-SMUGGLED-GET" -description: "TECL-SMUGGLED-GET sequence test documentation" -weight: 23 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TECL-SMUGGLED-GET` | -| **Category** | Smuggling | -| **Type** | Sequence (single send) | -| **Scored** | Yes | -| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **RFC Level** | MUST | -| **Expected** | `400`, or connection close | - -## What it does - -This test is a TE.CL smuggling confirmation technique inspired by common request smuggling labs. - -The body begins with a valid chunk-size line (for the chunked framing), but the `Content-Length` is set to only cover the chunk-size prefix (the `{hex}\r\n` line). If a server incorrectly uses `Content-Length` framing in the presence of `Transfer-Encoding: chunked`, it can leave the chunk-data bytes on the wire and interpret them as the next request. - -To make the signal cleaner, the smuggled `GET` includes `Content-Length: 7` so the remaining chunked framing bytes (`\r\n0\r\n\r\n`) get consumed as the smuggled request body if it is parsed as a second request. - -## What it sends - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer-Encoding: chunked\r\n -Content-Length: 4\r\n -\r\n -2b\r\n -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Content-Length: 7\r\n -\r\n -\r\n -0\r\n -\r\n -``` - -(`2b` and `Content-Length: 4` are examples; the probe computes the exact chunk size and the corresponding prefix length.) - -## What the RFC says - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... Regardless, the server MUST close the connection after responding to such a request." -- RFC 9112 §6.1 - -## Why it matters - -In real deployments, TE.CL smuggling happens when one hop uses chunked framing and another hop uses Content-Length framing. If the connection is left open, leftover bytes can be interpreted as a new request and smuggled past security controls. - -This test looks for concrete evidence of request-boundary confusion by detecting multiple HTTP status lines in the response to a single client send. In a proxy chain, this same behavior is what enables request smuggling. - -## Verdicts - -- **Pass**: The server rejects with `400`, or closes the connection. -- **Fail**: Multiple HTTP status lines are observed (smuggled `GET` likely executed). -- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. - -## Sources - -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) -- [HTTP Request Smuggling (PortSwigger)](https://portswigger.net/web-security/request-smuggling) -- [Request smuggling (PortSwigger labs)](https://portswigger.net/web-security/request-smuggling/lab-basic-te-cl) +--- +title: "TE/CL Smuggled GET — Request Smuggling" +description: "This test is a TE.CL smuggling confirmation technique inspired by common request smuggling labs. The body begins with a valid chunk-size line (for the chunked framing), but the Content-Length…" +weight: 23 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TECL-SMUGGLED-GET` | +| **Category** | Smuggling | +| **Type** | Sequence (single send) | +| **Scored** | Yes | +| **RFC** | [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **RFC Level** | MUST | +| **Expected** | `400`, or connection close | + +## What it does + +This test is a TE.CL smuggling confirmation technique inspired by common request smuggling labs. + +The body begins with a valid chunk-size line (for the chunked framing), but the `Content-Length` is set to only cover the chunk-size prefix (the `{hex}\r\n` line). If a server incorrectly uses `Content-Length` framing in the presence of `Transfer-Encoding: chunked`, it can leave the chunk-data bytes on the wire and interpret them as the next request. + +To make the signal cleaner, the smuggled `GET` includes `Content-Length: 7` so the remaining chunked framing bytes (`\r\n0\r\n\r\n`) get consumed as the smuggled request body if it is parsed as a second request. + +## What it sends + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer-Encoding: chunked\r\n +Content-Length: 4\r\n +\r\n +2b\r\n +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Content-Length: 7\r\n +\r\n +\r\n +0\r\n +\r\n +``` + +(`2b` and `Content-Length: 4` are examples; the probe computes the exact chunk size and the corresponding prefix length.) + +## What the RFC says + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding... Regardless, the server MUST close the connection after responding to such a request." -- RFC 9112 §6.1 + +## Why it matters + +In real deployments, TE.CL smuggling happens when one hop uses chunked framing and another hop uses Content-Length framing. If the connection is left open, leftover bytes can be interpreted as a new request and smuggled past security controls. + +This test looks for concrete evidence of request-boundary confusion by detecting multiple HTTP status lines in the response to a single client send. In a proxy chain, this same behavior is what enables request smuggling. + +## Verdicts + +- **Pass**: The server rejects with `400`, or closes the connection. +- **Fail**: Multiple HTTP status lines are observed (smuggled `GET` likely executed). +- **Fail**: The server accepts and keeps the connection open (MUST-close violated), even if no extra response is observed. + +## Sources + +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +- [HTTP Request Smuggling (PortSwigger)](https://portswigger.net/web-security/request-smuggling) +- [Request smuggling (PortSwigger labs)](https://portswigger.net/web-security/request-smuggling/lab-basic-te-cl) diff --git a/docs/content/docs/smuggling/trailer-auth.md b/docs/content/docs/smuggling/trailer-auth.md index a554ece..28029b1 100644 --- a/docs/content/docs/smuggling/trailer-auth.md +++ b/docs/content/docs/smuggling/trailer-auth.md @@ -1,37 +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) +--- +title: "Trailer Auth — Request Smuggling" +description: "A chunked request that places Authorization in the trailer section. Tested against RFC 9110 §6.5.1." +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/smuggling/trailer-cl.md b/docs/content/docs/smuggling/trailer-cl.md index 9d838ac..460ccb9 100644 --- a/docs/content/docs/smuggling/trailer-cl.md +++ b/docs/content/docs/smuggling/trailer-cl.md @@ -1,96 +1,96 @@ ---- -title: "TRAILER-CL" -description: "TRAILER-CL test documentation" -weight: 32 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TRAILER-CL` | -| **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 valid chunked request with a `Content-Length: 50` header in the trailer section (after the last chunk). - -```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 -Content-Length: 50\r\n -\r\n -``` - -A `Content-Length: 50` header appears in the chunked trailers section. - - -## What the RFC says - -> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." — RFC 9110 §6.5.1 - -> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." — RFC 9112 §7.1.2 - -Content-Length describes message framing and is therefore prohibited in trailers per the above rule. A compliant server must either reject the request or silently discard the prohibited trailer field. - -## Why this test is unscored - -The sender clearly violates the RFC by placing Content-Length in a trailer. However, the server's obligation is to either reject or ignore the prohibited trailer. Both `400` (reject) and `2xx` (process body, discard trailer) are defensible responses since the chunked body itself is valid. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (processes body and discards prohibited trailer). - -## Why it matters - -If a server or proxy processes the `Content-Length` trailer, it could retroactively change its understanding of the message body length — potentially poisoning a cache or re-framing subsequent requests on the same connection. - -## Deep Analysis - -### Relevant ABNF - -From RFC 9112 Section 7.1, the chunked body structure explicitly includes a trailer section: - -``` -chunked-body = *chunk - last-chunk - trailer-section - CRLF - -trailer-section = *( field-line CRLF ) -``` - -The grammar permits any `field-line` in the trailer section syntactically, but the semantics in RFC 9110 Section 6.5.1 restrict which fields may actually appear there. - -### RFC Evidence - -> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." -- RFC 9110 Section 6.5.1 - -> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." -- RFC 9112 Section 7.1.2 - -> "A sender MUST NOT send a Content-Length header field in any message that contains a Transfer-Encoding header field." -- RFC 9112 Section 6.2 - -### Chain of Reasoning - -1. **Content-Length is a message framing field by definition.** RFC 9110 Section 6.5.1 identifies "message framing" as one of the categories of fields that MUST NOT appear in trailers. Content-Length is the primary mechanism for determining message body length in HTTP/1.1 -- it is the quintessential framing field. Its prohibition from trailers is not merely implied; it falls squarely within the explicit prohibition category. - -2. **The MUST NOT merge rule provides a second layer of defense.** Even if a server's chunked parser extracts the trailer `Content-Length: 50`, RFC 9112 Section 7.1.2 says it MUST NOT merge this into the header section. The Content-Length field definition does not permit trailer usage, so the merge is unconditionally prohibited. A compliant server must either discard the trailer or reject the message entirely. - -3. **Processing the trailer would retroactively reframe the message.** The chunked body contains exactly 5 bytes (`hello`). If a server or intermediary processes the `Content-Length: 50` trailer, it now "believes" the message body is 50 bytes long. Since only 5 bytes were actually sent, the implementation might attempt to read 45 more bytes from the connection -- consuming the next request in the pipeline. This is a direct path to request smuggling. - -4. **Attack scenario.** An attacker sends a valid chunked request with `Content-Length: N` in the trailer, where N is chosen to encompass the next request on the connection. A vulnerable intermediary processes the trailer, retroactively changes its understanding of the body length, and reframes the connection stream. The next legitimate request is consumed as "body" of the attacker's request, and the attacker's crafted follow-up is processed as a new request with the victim's credentials. - -### Scored / Unscored Justification - -This test is **unscored** (`Scored = false`). While the sender clearly violates the RFC by placing Content-Length in a trailer (MUST NOT generate), the server's compliance obligation is to either reject the message or silently discard the prohibited trailer. Both `400` (reject) and `2xx` (accept the valid chunked body, discard the invalid trailer) are correct server behaviors. A `2xx` response does not necessarily mean the server processed the trailer -- it may have properly discarded it. Since the test cannot distinguish "discarded the trailer safely" from "merged the trailer dangerously" based on the status code alone, it flags `2xx` as a warning for manual investigation rather than scoring it as a failure. - -## Sources - -- [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) -- [RFC 9110 §6.5.2](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.2) -- [RFC 9112 §7.1.2](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.2) +--- +title: "Trailer CL — Request Smuggling" +description: "A valid chunked request with a Content-Length: 50 header in the trailer section (after the last chunk). Tested against RFC 9110 §6.5.1." +weight: 32 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TRAILER-CL` | +| **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 valid chunked request with a `Content-Length: 50` header in the trailer section (after the last chunk). + +```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 +Content-Length: 50\r\n +\r\n +``` + +A `Content-Length: 50` header appears in the chunked trailers section. + + +## What the RFC says + +> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." — RFC 9110 §6.5.1 + +> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." — RFC 9112 §7.1.2 + +Content-Length describes message framing and is therefore prohibited in trailers per the above rule. A compliant server must either reject the request or silently discard the prohibited trailer field. + +## Why this test is unscored + +The sender clearly violates the RFC by placing Content-Length in a trailer. However, the server's obligation is to either reject or ignore the prohibited trailer. Both `400` (reject) and `2xx` (process body, discard trailer) are defensible responses since the chunked body itself is valid. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (processes body and discards prohibited trailer). + +## Why it matters + +If a server or proxy processes the `Content-Length` trailer, it could retroactively change its understanding of the message body length — potentially poisoning a cache or re-framing subsequent requests on the same connection. + +## Deep Analysis + +### Relevant ABNF + +From RFC 9112 Section 7.1, the chunked body structure explicitly includes a trailer section: + +``` +chunked-body = *chunk + last-chunk + trailer-section + CRLF + +trailer-section = *( field-line CRLF ) +``` + +The grammar permits any `field-line` in the trailer section syntactically, but the semantics in RFC 9110 Section 6.5.1 restrict which fields may actually appear there. + +### RFC Evidence + +> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." -- RFC 9110 Section 6.5.1 + +> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." -- RFC 9112 Section 7.1.2 + +> "A sender MUST NOT send a Content-Length header field in any message that contains a Transfer-Encoding header field." -- RFC 9112 Section 6.2 + +### Chain of Reasoning + +1. **Content-Length is a message framing field by definition.** RFC 9110 Section 6.5.1 identifies "message framing" as one of the categories of fields that MUST NOT appear in trailers. Content-Length is the primary mechanism for determining message body length in HTTP/1.1 -- it is the quintessential framing field. Its prohibition from trailers is not merely implied; it falls squarely within the explicit prohibition category. + +2. **The MUST NOT merge rule provides a second layer of defense.** Even if a server's chunked parser extracts the trailer `Content-Length: 50`, RFC 9112 Section 7.1.2 says it MUST NOT merge this into the header section. The Content-Length field definition does not permit trailer usage, so the merge is unconditionally prohibited. A compliant server must either discard the trailer or reject the message entirely. + +3. **Processing the trailer would retroactively reframe the message.** The chunked body contains exactly 5 bytes (`hello`). If a server or intermediary processes the `Content-Length: 50` trailer, it now "believes" the message body is 50 bytes long. Since only 5 bytes were actually sent, the implementation might attempt to read 45 more bytes from the connection -- consuming the next request in the pipeline. This is a direct path to request smuggling. + +4. **Attack scenario.** An attacker sends a valid chunked request with `Content-Length: N` in the trailer, where N is chosen to encompass the next request on the connection. A vulnerable intermediary processes the trailer, retroactively changes its understanding of the body length, and reframes the connection stream. The next legitimate request is consumed as "body" of the attacker's request, and the attacker's crafted follow-up is processed as a new request with the victim's credentials. + +### Scored / Unscored Justification + +This test is **unscored** (`Scored = false`). While the sender clearly violates the RFC by placing Content-Length in a trailer (MUST NOT generate), the server's compliance obligation is to either reject the message or silently discard the prohibited trailer. Both `400` (reject) and `2xx` (accept the valid chunked body, discard the invalid trailer) are correct server behaviors. A `2xx` response does not necessarily mean the server processed the trailer -- it may have properly discarded it. Since the test cannot distinguish "discarded the trailer safely" from "merged the trailer dangerously" based on the status code alone, it flags `2xx` as a warning for manual investigation rather than scoring it as a failure. + +## Sources + +- [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) +- [RFC 9110 §6.5.2](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.2) +- [RFC 9112 §7.1.2](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.2) diff --git a/docs/content/docs/smuggling/trailer-content-type.md b/docs/content/docs/smuggling/trailer-content-type.md index 8f32cbe..5611ce8 100644 --- a/docs/content/docs/smuggling/trailer-content-type.md +++ b/docs/content/docs/smuggling/trailer-content-type.md @@ -1,96 +1,96 @@ ---- -title: "TRAILER-CONTENT-TYPE" -description: "TRAILER-CONTENT-TYPE test documentation" -weight: 58 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TRAILER-CONTENT-TYPE` | -| **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 valid chunked request with a `Content-Type: text/evil` header in the trailer section (after the last chunk). - -```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 -Content-Type: text/evil\r\n -\r\n -``` - -A `Content-Type: text/evil` header appears in the chunked trailers section. - - -## What the RFC says - -> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." — RFC 9110 §6.5.1 - -> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." — RFC 9112 §7.1.2 - -Content-Type describes content format and is therefore prohibited in trailers per the above rule. A compliant server should either reject the request or silently discard the prohibited trailer field. - -## Why this test is unscored - -The sender violates the RFC by placing Content-Type in a trailer. The server must either reject or ignore the prohibited trailer. Both `400` (reject) and `2xx` (process body, discard trailer) are defensible since the chunked body itself is valid. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (processes body and discards prohibited trailer). - -## Why it matters - -If a server or middleware processes the `Content-Type` trailer, it could retroactively change how the already-received body is interpreted. An attacker could send a benign `Content-Type` in the headers to pass WAF inspection, then inject a different `Content-Type` in the trailer to trick downstream processors into interpreting the body differently — for example, changing `application/json` to `text/xml` to trigger different parsing paths or bypass content-type-based security filters. - -## Deep Analysis - -### Relevant ABNF - -From RFC 9112 Section 7.1: - -``` -chunked-body = *chunk - last-chunk - trailer-section - CRLF - -trailer-section = *( field-line CRLF ) -``` - -The trailer section can contain any syntactic field-line, but RFC 9110 Section 6.5.1 semantically restricts which fields are permitted based on their function. - -### RFC Evidence - -> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." -- RFC 9110 Section 6.5.1 - -> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." -- RFC 9112 Section 7.1.2 - -> "A sender MUST NOT generate a trailer that contains a field necessary for message framing, routing, request modifiers, authentication, response control, or determining how to process the payload." -- RFC 9110 Section 6.5.1 - -### Chain of Reasoning - -1. **Content-Type describes "content format" -- an explicitly prohibited trailer category.** RFC 9110 Section 6.5.1 lists "content format" among the field categories that cannot be processed outside the header section. Content-Type is the canonical content format field. Its value must be known before the body is parsed so that the recipient can select the correct decoder (JSON parser, XML parser, multipart boundary scanner, etc.). Receiving it after the body has already been transmitted and parsed defeats its purpose. - -2. **The MUST NOT merge rule prevents retroactive reinterpretation.** Even if a parser encounters `Content-Type: text/evil` in the trailer section, RFC 9112 Section 7.1.2 prohibits merging it into the header section. The Content-Type field definition does not permit trailer usage, so the merge condition is never satisfied. A compliant recipient must discard it. - -3. **The attack exploits the gap between header-time and trailer-time processing.** Security filters (WAFs, input validators, content scanners) inspect headers before the body arrives. If the Content-Type header says `application/json`, the WAF applies JSON validation rules. But if a different Content-Type arrives in the trailer after the body has already passed through, and the back-end uses the trailer value to select its parser, the body may be reinterpreted under a completely different content type that the WAF never validated. - -4. **Attack scenario.** An attacker sends a chunked POST with `Content-Type: application/json` in the headers and a body containing XML with embedded XXE payloads. The WAF sees JSON content-type and applies JSON rules (the XML passes because the WAF is not running XML checks). In the trailer, the attacker sends `Content-Type: application/xml`. A vulnerable back-end processes the trailer, switches to the XML parser, and the XXE payload executes -- all because the WAF and back-end disagreed on content type due to the trailer injection. - -### Scored / Unscored Justification - -This test is **unscored** (`Scored = false`). The sender is in clear violation of the RFC (MUST NOT generate Content-Type as a trailer), but the server has two compliant responses: reject the message (`400`) or accept the valid chunked body while discarding the prohibited trailer (`2xx`). A `2xx` response is ambiguous -- it could mean the server correctly discarded the trailer (safe) or that it processed it (vulnerable). Since the status code alone cannot distinguish these cases, the test flags `2xx` as a warning. The risk depends entirely on whether the back-end or any intermediary actually applies the trailer Content-Type value, which requires manual investigation. - -## Sources - -- [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) -- [RFC 9110 §6.5.2](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.2) -- [RFC 9112 §7.1.2](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.2) +--- +title: "Trailer Content Type — Request Smuggling" +description: "A valid chunked request with a Content-Type: text/evil header in the trailer section (after the last chunk). Tested against RFC 9110 §6.5.1." +weight: 58 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TRAILER-CONTENT-TYPE` | +| **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 valid chunked request with a `Content-Type: text/evil` header in the trailer section (after the last chunk). + +```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 +Content-Type: text/evil\r\n +\r\n +``` + +A `Content-Type: text/evil` header appears in the chunked trailers section. + + +## What the RFC says + +> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." — RFC 9110 §6.5.1 + +> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." — RFC 9112 §7.1.2 + +Content-Type describes content format and is therefore prohibited in trailers per the above rule. A compliant server should either reject the request or silently discard the prohibited trailer field. + +## Why this test is unscored + +The sender violates the RFC by placing Content-Type in a trailer. The server must either reject or ignore the prohibited trailer. Both `400` (reject) and `2xx` (process body, discard trailer) are defensible since the chunked body itself is valid. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (processes body and discards prohibited trailer). + +## Why it matters + +If a server or middleware processes the `Content-Type` trailer, it could retroactively change how the already-received body is interpreted. An attacker could send a benign `Content-Type` in the headers to pass WAF inspection, then inject a different `Content-Type` in the trailer to trick downstream processors into interpreting the body differently — for example, changing `application/json` to `text/xml` to trigger different parsing paths or bypass content-type-based security filters. + +## Deep Analysis + +### Relevant ABNF + +From RFC 9112 Section 7.1: + +``` +chunked-body = *chunk + last-chunk + trailer-section + CRLF + +trailer-section = *( field-line CRLF ) +``` + +The trailer section can contain any syntactic field-line, but RFC 9110 Section 6.5.1 semantically restricts which fields are permitted based on their function. + +### RFC Evidence + +> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." -- RFC 9110 Section 6.5.1 + +> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." -- RFC 9112 Section 7.1.2 + +> "A sender MUST NOT generate a trailer that contains a field necessary for message framing, routing, request modifiers, authentication, response control, or determining how to process the payload." -- RFC 9110 Section 6.5.1 + +### Chain of Reasoning + +1. **Content-Type describes "content format" -- an explicitly prohibited trailer category.** RFC 9110 Section 6.5.1 lists "content format" among the field categories that cannot be processed outside the header section. Content-Type is the canonical content format field. Its value must be known before the body is parsed so that the recipient can select the correct decoder (JSON parser, XML parser, multipart boundary scanner, etc.). Receiving it after the body has already been transmitted and parsed defeats its purpose. + +2. **The MUST NOT merge rule prevents retroactive reinterpretation.** Even if a parser encounters `Content-Type: text/evil` in the trailer section, RFC 9112 Section 7.1.2 prohibits merging it into the header section. The Content-Type field definition does not permit trailer usage, so the merge condition is never satisfied. A compliant recipient must discard it. + +3. **The attack exploits the gap between header-time and trailer-time processing.** Security filters (WAFs, input validators, content scanners) inspect headers before the body arrives. If the Content-Type header says `application/json`, the WAF applies JSON validation rules. But if a different Content-Type arrives in the trailer after the body has already passed through, and the back-end uses the trailer value to select its parser, the body may be reinterpreted under a completely different content type that the WAF never validated. + +4. **Attack scenario.** An attacker sends a chunked POST with `Content-Type: application/json` in the headers and a body containing XML with embedded XXE payloads. The WAF sees JSON content-type and applies JSON rules (the XML passes because the WAF is not running XML checks). In the trailer, the attacker sends `Content-Type: application/xml`. A vulnerable back-end processes the trailer, switches to the XML parser, and the XXE payload executes -- all because the WAF and back-end disagreed on content type due to the trailer injection. + +### Scored / Unscored Justification + +This test is **unscored** (`Scored = false`). The sender is in clear violation of the RFC (MUST NOT generate Content-Type as a trailer), but the server has two compliant responses: reject the message (`400`) or accept the valid chunked body while discarding the prohibited trailer (`2xx`). A `2xx` response is ambiguous -- it could mean the server correctly discarded the trailer (safe) or that it processed it (vulnerable). Since the status code alone cannot distinguish these cases, the test flags `2xx` as a warning. The risk depends entirely on whether the back-end or any intermediary actually applies the trailer Content-Type value, which requires manual investigation. + +## Sources + +- [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) +- [RFC 9110 §6.5.2](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.2) +- [RFC 9112 §7.1.2](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.2) diff --git a/docs/content/docs/smuggling/trailer-host.md b/docs/content/docs/smuggling/trailer-host.md index d3ca6f4..b66629b 100644 --- a/docs/content/docs/smuggling/trailer-host.md +++ b/docs/content/docs/smuggling/trailer-host.md @@ -1,102 +1,102 @@ ---- -title: "TRAILER-HOST" -description: "TRAILER-HOST test documentation" -weight: 34 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TRAILER-HOST` | -| **Category** | Smuggling | -| **RFC** | [RFC 9110 §6.5.2](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.2) | -| **Requirement** | Unscored | -| **Expected** | `400` or `2xx` | - -## What it sends - -A valid chunked request with `Host: evil.example.com` in the trailer section, while the actual Host header in the request points to the real server. - -```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 -Host: evil.example.com\r\n -\r\n -``` - -A `Host: evil.example.com` header appears in the chunked trailers section. - - -## What the RFC says - -> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." — RFC 9110 §6.5.1 - -> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." — RFC 9112 §7.1.2 - -Host is a routing field that controls which virtual host processes the request. It must not appear in trailers because its evaluation is necessary prior to receiving the content, and it could alter message routing semantics after the body has been processed. - -## Why this test is unscored - -The sender violates the RFC by placing Host in a trailer. The server must either reject the request or silently discard the prohibited trailer field. Both `400` (reject) and `2xx` (process body, discard trailer) are defensible since the chunked body itself is valid. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (processes body and discards prohibited trailer). - -## Why it matters - -If a server or middleware reads the Host trailer and uses it for routing decisions, an attacker could redirect requests to a different virtual host or backend after the message body has already been accepted. This is a form of late-binding host injection. - -## Deep Analysis - -### Relevant ABNF - -From RFC 9112 Section 7.1: - -``` -chunked-body = *chunk - last-chunk - trailer-section - CRLF - -trailer-section = *( field-line CRLF ) -``` - -And the Host header itself (from RFC 9110 Section 7.2): - -``` -Host = uri-host [ ":" port ] -``` - -Host is a singleton routing field that must be evaluated before content processing begins -- making it categorically prohibited from appearing in trailers. - -### RFC Evidence - -> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." -- RFC 9110 Section 6.5.1 - -> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." -- RFC 9112 Section 7.1.2 - -> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 - -### Chain of Reasoning - -1. **Host is a routing field -- the most dangerous trailer category.** RFC 9110 Section 6.5.1 lists "routing" as a prohibited trailer field category. Host is the primary routing field in HTTP/1.1 -- it determines which virtual host processes the request, which back-end the proxy selects, and which cache key is used. Its value must be known before the server begins processing the request, not after the body has been received. - -2. **The Host trailer creates a temporal paradox for routing.** When the server receives the request headers, it routes to `localhost:8080` based on the Host header. The body is received and processed. Then the trailer arrives with `Host: evil.example.com`. If any component processes this trailer, the routing decision made at header-time is now contradicted by a value received after the fact. This is not merely a parsing disagreement -- it is a deliberate attempt to change the request's routing identity after the body has been accepted. - -3. **The MUST NOT merge rule is the critical defense.** RFC 9112 Section 7.1.2 prohibits merging trailer fields into the header section unless the field definition explicitly allows it. The Host field definition does not permit trailer usage. If a server or intermediary violates this rule and merges the Host trailer, it effectively has two Host values -- the original (`localhost:8080`) and the trailer (`evil.example.com`). This creates the same "more than one Host" condition that RFC 9112 Section 3.2 says MUST be rejected with 400. - -4. **Attack scenario.** An attacker sends a chunked POST to a caching proxy. The headers contain `Host: legitimate.com`, so the proxy routes to the legitimate origin and begins computing the cache key. The body is received normally. In the trailer, the attacker sends `Host: attacker.com`. If the proxy merges the trailer Host into its request metadata, it may either (a) use `attacker.com` for the cache key, poisoning the cache for that host, or (b) forward a request with conflicting Host values to the origin, causing the origin to route to the wrong virtual host. In either case, the attacker has injected a Host value that bypassed the proxy's initial routing check. - -### Scored / Unscored Justification - -This test is **unscored** (`Scored = false`). The sender is in clear violation (MUST NOT generate Host as a trailer), but the server has two compliant responses: reject (`400`) or accept the valid chunked body while discarding the prohibited Host trailer (`2xx`). A `2xx` response is ambiguous -- the server may have correctly discarded the trailer (safe) or may have processed it (vulnerable). The real danger depends on whether any component in the request chain -- the server, a reverse proxy, a WAF, or a caching layer -- reads and acts on the Host trailer. This cannot be determined from a single status code, so the test flags `2xx` as a warning for manual investigation. - -## Sources - -- [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) -- [RFC 9110 §6.5.2](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.2) -- [RFC 9112 §7.1.2](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.2) +--- +title: "Trailer Host — Request Smuggling" +description: "A valid chunked request with Host: evil.example.com in the trailer section, while the actual Host header in the request points to the real server. Tested against RFC 9110 §6.5.2." +weight: 34 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TRAILER-HOST` | +| **Category** | Smuggling | +| **RFC** | [RFC 9110 §6.5.2](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.2) | +| **Requirement** | Unscored | +| **Expected** | `400` or `2xx` | + +## What it sends + +A valid chunked request with `Host: evil.example.com` in the trailer section, while the actual Host header in the request points to the real server. + +```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 +Host: evil.example.com\r\n +\r\n +``` + +A `Host: evil.example.com` header appears in the chunked trailers section. + + +## What the RFC says + +> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." — RFC 9110 §6.5.1 + +> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." — RFC 9112 §7.1.2 + +Host is a routing field that controls which virtual host processes the request. It must not appear in trailers because its evaluation is necessary prior to receiving the content, and it could alter message routing semantics after the body has been processed. + +## Why this test is unscored + +The sender violates the RFC by placing Host in a trailer. The server must either reject the request or silently discard the prohibited trailer field. Both `400` (reject) and `2xx` (process body, discard trailer) are defensible since the chunked body itself is valid. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (processes body and discards prohibited trailer). + +## Why it matters + +If a server or middleware reads the Host trailer and uses it for routing decisions, an attacker could redirect requests to a different virtual host or backend after the message body has already been accepted. This is a form of late-binding host injection. + +## Deep Analysis + +### Relevant ABNF + +From RFC 9112 Section 7.1: + +``` +chunked-body = *chunk + last-chunk + trailer-section + CRLF + +trailer-section = *( field-line CRLF ) +``` + +And the Host header itself (from RFC 9110 Section 7.2): + +``` +Host = uri-host [ ":" port ] +``` + +Host is a singleton routing field that must be evaluated before content processing begins -- making it categorically prohibited from appearing in trailers. + +### RFC Evidence + +> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." -- RFC 9110 Section 6.5.1 + +> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." -- RFC 9112 Section 7.1.2 + +> "A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field line or a Host header field with an invalid field value." -- RFC 9112 Section 3.2 + +### Chain of Reasoning + +1. **Host is a routing field -- the most dangerous trailer category.** RFC 9110 Section 6.5.1 lists "routing" as a prohibited trailer field category. Host is the primary routing field in HTTP/1.1 -- it determines which virtual host processes the request, which back-end the proxy selects, and which cache key is used. Its value must be known before the server begins processing the request, not after the body has been received. + +2. **The Host trailer creates a temporal paradox for routing.** When the server receives the request headers, it routes to `localhost:8080` based on the Host header. The body is received and processed. Then the trailer arrives with `Host: evil.example.com`. If any component processes this trailer, the routing decision made at header-time is now contradicted by a value received after the fact. This is not merely a parsing disagreement -- it is a deliberate attempt to change the request's routing identity after the body has been accepted. + +3. **The MUST NOT merge rule is the critical defense.** RFC 9112 Section 7.1.2 prohibits merging trailer fields into the header section unless the field definition explicitly allows it. The Host field definition does not permit trailer usage. If a server or intermediary violates this rule and merges the Host trailer, it effectively has two Host values -- the original (`localhost:8080`) and the trailer (`evil.example.com`). This creates the same "more than one Host" condition that RFC 9112 Section 3.2 says MUST be rejected with 400. + +4. **Attack scenario.** An attacker sends a chunked POST to a caching proxy. The headers contain `Host: legitimate.com`, so the proxy routes to the legitimate origin and begins computing the cache key. The body is received normally. In the trailer, the attacker sends `Host: attacker.com`. If the proxy merges the trailer Host into its request metadata, it may either (a) use `attacker.com` for the cache key, poisoning the cache for that host, or (b) forward a request with conflicting Host values to the origin, causing the origin to route to the wrong virtual host. In either case, the attacker has injected a Host value that bypassed the proxy's initial routing check. + +### Scored / Unscored Justification + +This test is **unscored** (`Scored = false`). The sender is in clear violation (MUST NOT generate Host as a trailer), but the server has two compliant responses: reject (`400`) or accept the valid chunked body while discarding the prohibited Host trailer (`2xx`). A `2xx` response is ambiguous -- the server may have correctly discarded the trailer (safe) or may have processed it (vulnerable). The real danger depends on whether any component in the request chain -- the server, a reverse proxy, a WAF, or a caching layer -- reads and acts on the Host trailer. This cannot be determined from a single status code, so the test flags `2xx` as a warning for manual investigation. + +## Sources + +- [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) +- [RFC 9110 §6.5.2](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.2) +- [RFC 9112 §7.1.2](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.2) diff --git a/docs/content/docs/smuggling/trailer-te.md b/docs/content/docs/smuggling/trailer-te.md index 58766a3..5ace09f 100644 --- a/docs/content/docs/smuggling/trailer-te.md +++ b/docs/content/docs/smuggling/trailer-te.md @@ -1,101 +1,101 @@ ---- -title: "TRAILER-TE" -description: "TRAILER-TE test documentation" -weight: 33 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TRAILER-TE` | -| **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 valid chunked request with a `Transfer-Encoding: chunked` header 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 -Transfer-Encoding: chunked\r\n -\r\n -``` - -A `Transfer-Encoding: chunked` header appears in the chunked trailers section. - - -## What the RFC says - -> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." — RFC 9110 §6.5.1 - -> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." — RFC 9112 §7.1.2 - -Transfer-Encoding describes message framing and is therefore prohibited in trailers per the above rule. A compliant server must either reject the request or silently discard the prohibited trailer field. - -## Why this test is unscored - -The sender violates the RFC by placing Transfer-Encoding in a trailer. However, the server's obligation is to either reject or ignore the prohibited trailer. Both `400` (reject) and `2xx` (process body, discard trailer) are defensible responses since the chunked body itself is valid. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (processes body and discards prohibited trailer). - -## Why it matters - -If a server processes the `Transfer-Encoding` trailer, it could attempt to re-decode the already-decoded body or change framing expectations for the next message on the connection. A compliant server should either reject the request or silently discard the prohibited trailer field. - -## Deep Analysis - -### Relevant ABNF - -From RFC 9112 Section 7.1: - -``` -chunked-body = *chunk - last-chunk - trailer-section - CRLF - -trailer-section = *( field-line CRLF ) -``` - -And the Transfer-Encoding header (from RFC 9112 Section 6.1): - -``` -Transfer-Encoding = #transfer-coding -``` - -Transfer-Encoding is the primary message framing field for HTTP/1.1. It determines how the message body is encoded and decoded, making it categorically prohibited from trailers. - -### RFC Evidence - -> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." -- RFC 9110 Section 6.5.1 - -> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." -- RFC 9112 Section 7.1.2 - -> "A recipient MUST be able to parse and decode the chunked transfer coding." -- RFC 9112 Section 7.1 - -### Chain of Reasoning - -1. **Transfer-Encoding is the defining framing field.** RFC 9110 Section 6.5.1 identifies "message framing" as the first category of prohibited trailer fields. Transfer-Encoding literally determines how the message body is framed -- whether it uses chunked encoding, compression, or other transfer codings. Its value must be known before the first body byte is read, because the value dictates the decoding algorithm. Placing it in a trailer (which arrives after the body) is a temporal impossibility under normal processing. - -2. **The trailer creates a circular dependency.** The message is received with `Transfer-Encoding: chunked` in the headers. The server decodes the chunked body, reads the `0` terminator, and then encounters `Transfer-Encoding: chunked` again in the trailer section. If the server were to process this trailer, it would need to retroactively re-apply chunked decoding to a body that has already been decoded. This is logically impossible for the current message, but it can corrupt the server's state for the next message on the connection. - -3. **State corruption is the primary attack vector.** Unlike Content-Length (which retroactively changes body length) or Host (which changes routing), a Transfer-Encoding trailer targets the server's framing state machine. If an intermediary processes the trailer and updates its Transfer-Encoding state, it may expect the next message on the connection to be chunked when it is not, or vice versa. This framing desync between the intermediary and the origin can cause entire requests to be misinterpreted. - -4. **Attack scenario.** An attacker sends a chunked POST with `Transfer-Encoding: chunked` in the trailer to a reverse proxy. The proxy correctly decodes the chunked body and forwards the dechunked content to the origin. But if the proxy processes the trailer and updates its state to expect chunked encoding on the next message, the next legitimate request (sent without chunked encoding) will be misinterpreted. The proxy may attempt to parse the raw request bytes as chunk-size lines, causing a complete protocol-level desync that the attacker can exploit to inject arbitrary requests. - -### Scored / Unscored Justification - -This test is **unscored** (`Scored = false`). The sender violates the RFC (MUST NOT generate Transfer-Encoding as a trailer), but the server has two compliant responses: reject the message (`400`) or accept the valid chunked body while discarding the prohibited trailer (`2xx`). A `2xx` response does not inherently indicate vulnerability -- the server may have correctly discarded the Transfer-Encoding trailer. The test cannot distinguish "discarded safely" from "processed dangerously" based on the status code, so it flags `2xx` as a warning. The real risk depends on whether any intermediary in the request chain processes the trailer and corrupts its framing state, which requires behavioral analysis beyond a single response code. - -## Sources - -- [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) -- [RFC 9112 §7.1.2](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.2) +--- +title: "Trailer TE — Request Smuggling" +description: "A valid chunked request with a Transfer-Encoding: chunked header in the trailer section. Tested against RFC 9110 §6.5.1." +weight: 33 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TRAILER-TE` | +| **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 valid chunked request with a `Transfer-Encoding: chunked` header 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 +Transfer-Encoding: chunked\r\n +\r\n +``` + +A `Transfer-Encoding: chunked` header appears in the chunked trailers section. + + +## What the RFC says + +> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." — RFC 9110 §6.5.1 + +> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." — RFC 9112 §7.1.2 + +Transfer-Encoding describes message framing and is therefore prohibited in trailers per the above rule. A compliant server must either reject the request or silently discard the prohibited trailer field. + +## Why this test is unscored + +The sender violates the RFC by placing Transfer-Encoding in a trailer. However, the server's obligation is to either reject or ignore the prohibited trailer. Both `400` (reject) and `2xx` (process body, discard trailer) are defensible responses since the chunked body itself is valid. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (processes body and discards prohibited trailer). + +## Why it matters + +If a server processes the `Transfer-Encoding` trailer, it could attempt to re-decode the already-decoded body or change framing expectations for the next message on the connection. A compliant server should either reject the request or silently discard the prohibited trailer field. + +## Deep Analysis + +### Relevant ABNF + +From RFC 9112 Section 7.1: + +``` +chunked-body = *chunk + last-chunk + trailer-section + CRLF + +trailer-section = *( field-line CRLF ) +``` + +And the Transfer-Encoding header (from RFC 9112 Section 6.1): + +``` +Transfer-Encoding = #transfer-coding +``` + +Transfer-Encoding is the primary message framing field for HTTP/1.1. It determines how the message body is encoded and decoded, making it categorically prohibited from trailers. + +### RFC Evidence + +> "Many fields cannot be processed outside the header section because their evaluation is necessary prior to receiving the content, such as those that describe message framing, routing, authentication, request modifiers, response controls, or content format. A sender MUST NOT generate a trailer field unless the sender knows the corresponding header field name's definition permits the field to be sent in trailers." -- RFC 9110 Section 6.5.1 + +> "A recipient MUST NOT merge a received trailer field into the header section unless its corresponding header field definition explicitly permits and instructs how the trailer field value can be safely merged." -- RFC 9112 Section 7.1.2 + +> "A recipient MUST be able to parse and decode the chunked transfer coding." -- RFC 9112 Section 7.1 + +### Chain of Reasoning + +1. **Transfer-Encoding is the defining framing field.** RFC 9110 Section 6.5.1 identifies "message framing" as the first category of prohibited trailer fields. Transfer-Encoding literally determines how the message body is framed -- whether it uses chunked encoding, compression, or other transfer codings. Its value must be known before the first body byte is read, because the value dictates the decoding algorithm. Placing it in a trailer (which arrives after the body) is a temporal impossibility under normal processing. + +2. **The trailer creates a circular dependency.** The message is received with `Transfer-Encoding: chunked` in the headers. The server decodes the chunked body, reads the `0` terminator, and then encounters `Transfer-Encoding: chunked` again in the trailer section. If the server were to process this trailer, it would need to retroactively re-apply chunked decoding to a body that has already been decoded. This is logically impossible for the current message, but it can corrupt the server's state for the next message on the connection. + +3. **State corruption is the primary attack vector.** Unlike Content-Length (which retroactively changes body length) or Host (which changes routing), a Transfer-Encoding trailer targets the server's framing state machine. If an intermediary processes the trailer and updates its Transfer-Encoding state, it may expect the next message on the connection to be chunked when it is not, or vice versa. This framing desync between the intermediary and the origin can cause entire requests to be misinterpreted. + +4. **Attack scenario.** An attacker sends a chunked POST with `Transfer-Encoding: chunked` in the trailer to a reverse proxy. The proxy correctly decodes the chunked body and forwards the dechunked content to the origin. But if the proxy processes the trailer and updates its state to expect chunked encoding on the next message, the next legitimate request (sent without chunked encoding) will be misinterpreted. The proxy may attempt to parse the raw request bytes as chunk-size lines, causing a complete protocol-level desync that the attacker can exploit to inject arbitrary requests. + +### Scored / Unscored Justification + +This test is **unscored** (`Scored = false`). The sender violates the RFC (MUST NOT generate Transfer-Encoding as a trailer), but the server has two compliant responses: reject the message (`400`) or accept the valid chunked body while discarding the prohibited trailer (`2xx`). A `2xx` response does not inherently indicate vulnerability -- the server may have correctly discarded the Transfer-Encoding trailer. The test cannot distinguish "discarded safely" from "processed dangerously" based on the status code, so it flags `2xx` as a warning. The real risk depends on whether any intermediary in the request chain processes the trailer and corrupts its framing state, which requires behavioral analysis beyond a single response code. + +## Sources + +- [RFC 9110 §6.5.1](https://www.rfc-editor.org/rfc/rfc9110#section-6.5.1) +- [RFC 9112 §7.1.2](https://www.rfc-editor.org/rfc/rfc9112#section-7.1.2) diff --git a/docs/content/docs/smuggling/transfer-encoding-underscore.md b/docs/content/docs/smuggling/transfer-encoding-underscore.md index 6342b39..9c156e4 100644 --- a/docs/content/docs/smuggling/transfer-encoding-underscore.md +++ b/docs/content/docs/smuggling/transfer-encoding-underscore.md @@ -1,99 +1,99 @@ ---- -title: "TRANSFER_ENCODING" -description: "TRANSFER_ENCODING test documentation" -weight: 30 ---- - -| | | -|---|---| -| **Test ID** | `SMUG-TRANSFER_ENCODING` | -| **Category** | Smuggling | -| **RFC** | [RFC 9112 Section 6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | -| **Requirement** | Unscored | -| **Expected** | `400` or `2xx` | - -## What it sends - -`Transfer_Encoding: chunked` (underscore instead of hyphen) with `Content-Length: 5`. - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Transfer_Encoding: chunked\r\n -Content-Length: 5\r\n -\r\n -hello -``` - -Note `Transfer_Encoding` with an underscore instead of a hyphen. - - -## What the RFC says - -> "field-name = token" -- RFC 9110 Section 5.1 - -> "token = 1\*tchar" where "tchar = '!' / '#' / '$' / '%' / '&' / ''' / '\*' / '+' / '-' / '.' / '^' / '\_' / '\`' / '|' / '~' / DIGIT / ALPHA" -- RFC 9110 Section 5.6.2 - -The underscore character (`_`) is explicitly included in the `tchar` production, making `Transfer_Encoding` a syntactically valid field name (token). However, it is not the registered `Transfer-Encoding` header field and has no defined semantics. A server receiving this header should treat it as an unknown custom header, not as Transfer-Encoding. - -## Why this test is unscored - -`Transfer_Encoding` is a valid token but not the `Transfer-Encoding` header. The server is correct to ignore it as an unknown header (resulting in `2xx` using Content-Length for framing) or to reject it with `400` (strict policy). The test is unscored because neither response is wrong per the RFC. - -**Pass:** Server rejects with `400` (strict, safe). -**Warn:** Server accepts and responds `2xx` (treats it as unknown header, uses CL). - -## Why it matters - -Some proxies normalize underscores to hyphens (notably certain Python/Ruby frameworks like Gunicorn and WEBrick), making this a known smuggling vector. If a front-end passes `Transfer_Encoding` through as-is but a back-end normalizes the underscore to a hyphen and processes it as `Transfer-Encoding: chunked`, the two parsers disagree on message framing. - -## Deep Analysis - -### ABNF - -``` -field-line = field-name ":" OWS field-value OWS ; RFC 9112 §5 -field-name = token ; RFC 9110 §5.1 -token = 1*tchar ; RFC 9110 §5.6.2 -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" - / DIGIT / ALPHA -``` - -The underscore (`_`) is explicitly listed as a valid `tchar` character. Therefore, `Transfer_Encoding` is a syntactically valid `token` and a valid `field-name`. However, HTTP header field names are matched by their registered names, and the registered name is `Transfer-Encoding` (with a hyphen). `Transfer_Encoding` is a completely different, unregistered header field name. - -### RFC Evidence - -> "Each field line consists of a case-insensitive field name followed by a colon (':'), optional leading whitespace, the field line value, and optional trailing whitespace." -- RFC 9112 §5 - -> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 - -> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 - -### Chain of Reasoning - -1. The test sends `Transfer_Encoding: chunked` (underscore) alongside `Content-Length: 5`. -2. The header field name `Transfer_Encoding` is syntactically valid -- the underscore is a permitted `tchar`. However, it is **not** the registered `Transfer-Encoding` header. -3. A compliant server should treat `Transfer_Encoding` as an unknown/custom header with no defined semantics. Since the actual `Transfer-Encoding` header is absent, the server should use `Content-Length: 5` for framing and respond normally with `2xx`. -4. The danger arises from server or proxy implementations that normalize header field names. Some frameworks (notably Python's WSGI/CGI interface, Ruby's WEBrick, and Gunicorn) convert header names to uppercase with underscores replacing hyphens (`HTTP_TRANSFER_ENCODING`). If a back-end framework reverses this normalization and converts underscores back to hyphens, `Transfer_Encoding` becomes `Transfer-Encoding`. -5. If the back-end sees `Transfer-Encoding: chunked` (after normalization) while the front-end saw `Transfer_Encoding` (an unknown header) and used Content-Length, the two parsers disagree on message framing. -6. Since `Transfer_Encoding` is not `Transfer-Encoding`, the CL/TE dual-header rules in RFC 9112 section 6.3 do not technically apply to a compliant server. The server simply has `Content-Length: 5` and an unknown header. - -### Scored / Unscored Justification - -This test is **unscored** because `Transfer_Encoding` is a syntactically valid but unregistered header name. The RFC does not mandate any specific behavior for unknown headers beyond ignoring them. A server that treats it as unknown and uses Content-Length is correct. A server that rejects with `400` is being defensively strict. Neither behavior violates the specification. - -- **Pass (400):** Strict rejection -- the server flags the suspicious header name. -- **Warn (2xx):** Correct behavior -- the server treated `Transfer_Encoding` as an unknown header and used Content-Length. - -### Smuggling Attack Scenarios - -- **Underscore-to-Hyphen Normalization Desync:** A front-end proxy passes `Transfer_Encoding: chunked` through as-is (an unknown header). The back-end, running a framework that normalizes underscores to hyphens (e.g., Gunicorn behind Nginx), sees `Transfer-Encoding: chunked` and uses chunked framing. The front-end used Content-Length; the back-end uses chunked encoding. The attacker injects a second request in the body that only the back-end parses. -- **CGI/WSGI Environment Variable Poisoning:** In CGI and WSGI environments, headers are converted to environment variables like `HTTP_TRANSFER_ENCODING`. Some reverse mappings do not distinguish between `Transfer-Encoding` (original) and `Transfer_Encoding` (underscore variant) because both map to the same environment variable. An attacker can use the underscore variant to inject a Transfer-Encoding header that the front-end never intended to forward. -- **Double Header Injection:** An attacker sends both `Transfer-Encoding: chunked` and `Transfer_Encoding: identity`. A front-end proxy processes the legitimate `Transfer-Encoding: chunked`. A back-end that normalizes underscores ends up with two `Transfer-Encoding` headers with conflicting values, creating an additional layer of ambiguity in transfer coding selection. - -## Sources - -- [RFC 9110 §5.1](https://www.rfc-editor.org/rfc/rfc9110#section-5.1) -- [RFC 9110 §5.6.2](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.2) -- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) +--- +title: "Transfer Encoding Underscore — Request Smuggling" +description: "Transfer_Encoding: chunked (underscore instead of hyphen) with Content-Length: 5. Tested against RFC 9112 Section 6.1." +weight: 30 +--- + +| | | +|---|---| +| **Test ID** | `SMUG-TRANSFER_ENCODING` | +| **Category** | Smuggling | +| **RFC** | [RFC 9112 Section 6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) | +| **Requirement** | Unscored | +| **Expected** | `400` or `2xx` | + +## What it sends + +`Transfer_Encoding: chunked` (underscore instead of hyphen) with `Content-Length: 5`. + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Transfer_Encoding: chunked\r\n +Content-Length: 5\r\n +\r\n +hello +``` + +Note `Transfer_Encoding` with an underscore instead of a hyphen. + + +## What the RFC says + +> "field-name = token" -- RFC 9110 Section 5.1 + +> "token = 1\*tchar" where "tchar = '!' / '#' / '$' / '%' / '&' / ''' / '\*' / '+' / '-' / '.' / '^' / '\_' / '\`' / '|' / '~' / DIGIT / ALPHA" -- RFC 9110 Section 5.6.2 + +The underscore character (`_`) is explicitly included in the `tchar` production, making `Transfer_Encoding` a syntactically valid field name (token). However, it is not the registered `Transfer-Encoding` header field and has no defined semantics. A server receiving this header should treat it as an unknown custom header, not as Transfer-Encoding. + +## Why this test is unscored + +`Transfer_Encoding` is a valid token but not the `Transfer-Encoding` header. The server is correct to ignore it as an unknown header (resulting in `2xx` using Content-Length for framing) or to reject it with `400` (strict policy). The test is unscored because neither response is wrong per the RFC. + +**Pass:** Server rejects with `400` (strict, safe). +**Warn:** Server accepts and responds `2xx` (treats it as unknown header, uses CL). + +## Why it matters + +Some proxies normalize underscores to hyphens (notably certain Python/Ruby frameworks like Gunicorn and WEBrick), making this a known smuggling vector. If a front-end passes `Transfer_Encoding` through as-is but a back-end normalizes the underscore to a hyphen and processes it as `Transfer-Encoding: chunked`, the two parsers disagree on message framing. + +## Deep Analysis + +### ABNF + +``` +field-line = field-name ":" OWS field-value OWS ; RFC 9112 §5 +field-name = token ; RFC 9110 §5.1 +token = 1*tchar ; RFC 9110 §5.6.2 +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + / DIGIT / ALPHA +``` + +The underscore (`_`) is explicitly listed as a valid `tchar` character. Therefore, `Transfer_Encoding` is a syntactically valid `token` and a valid `field-name`. However, HTTP header field names are matched by their registered names, and the registered name is `Transfer-Encoding` (with a hyphen). `Transfer_Encoding` is a completely different, unregistered header field name. + +### RFC Evidence + +> "Each field line consists of a case-insensitive field name followed by a colon (':'), optional leading whitespace, the field line value, and optional trailing whitespace." -- RFC 9112 §5 + +> "A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks." -- RFC 9112 §6.1 + +> "If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling or response splitting and ought to be handled as an error." -- RFC 9112 §6.3 + +### Chain of Reasoning + +1. The test sends `Transfer_Encoding: chunked` (underscore) alongside `Content-Length: 5`. +2. The header field name `Transfer_Encoding` is syntactically valid -- the underscore is a permitted `tchar`. However, it is **not** the registered `Transfer-Encoding` header. +3. A compliant server should treat `Transfer_Encoding` as an unknown/custom header with no defined semantics. Since the actual `Transfer-Encoding` header is absent, the server should use `Content-Length: 5` for framing and respond normally with `2xx`. +4. The danger arises from server or proxy implementations that normalize header field names. Some frameworks (notably Python's WSGI/CGI interface, Ruby's WEBrick, and Gunicorn) convert header names to uppercase with underscores replacing hyphens (`HTTP_TRANSFER_ENCODING`). If a back-end framework reverses this normalization and converts underscores back to hyphens, `Transfer_Encoding` becomes `Transfer-Encoding`. +5. If the back-end sees `Transfer-Encoding: chunked` (after normalization) while the front-end saw `Transfer_Encoding` (an unknown header) and used Content-Length, the two parsers disagree on message framing. +6. Since `Transfer_Encoding` is not `Transfer-Encoding`, the CL/TE dual-header rules in RFC 9112 section 6.3 do not technically apply to a compliant server. The server simply has `Content-Length: 5` and an unknown header. + +### Scored / Unscored Justification + +This test is **unscored** because `Transfer_Encoding` is a syntactically valid but unregistered header name. The RFC does not mandate any specific behavior for unknown headers beyond ignoring them. A server that treats it as unknown and uses Content-Length is correct. A server that rejects with `400` is being defensively strict. Neither behavior violates the specification. + +- **Pass (400):** Strict rejection -- the server flags the suspicious header name. +- **Warn (2xx):** Correct behavior -- the server treated `Transfer_Encoding` as an unknown header and used Content-Length. + +### Smuggling Attack Scenarios + +- **Underscore-to-Hyphen Normalization Desync:** A front-end proxy passes `Transfer_Encoding: chunked` through as-is (an unknown header). The back-end, running a framework that normalizes underscores to hyphens (e.g., Gunicorn behind Nginx), sees `Transfer-Encoding: chunked` and uses chunked framing. The front-end used Content-Length; the back-end uses chunked encoding. The attacker injects a second request in the body that only the back-end parses. +- **CGI/WSGI Environment Variable Poisoning:** In CGI and WSGI environments, headers are converted to environment variables like `HTTP_TRANSFER_ENCODING`. Some reverse mappings do not distinguish between `Transfer-Encoding` (original) and `Transfer_Encoding` (underscore variant) because both map to the same environment variable. An attacker can use the underscore variant to inject a Transfer-Encoding header that the front-end never intended to forward. +- **Double Header Injection:** An attacker sends both `Transfer-Encoding: chunked` and `Transfer_Encoding: identity`. A front-end proxy processes the legitimate `Transfer-Encoding: chunked`. A back-end that normalizes underscores ends up with two `Transfer-Encoding` headers with conflicting values, creating an additional layer of ambiguity in transfer coding selection. + +## Sources + +- [RFC 9110 §5.1](https://www.rfc-editor.org/rfc/rfc9110#section-5.1) +- [RFC 9110 §5.6.2](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.2) +- [RFC 9112 §6.1](https://www.rfc-editor.org/rfc/rfc9112#section-6.1) diff --git a/docs/content/docs/websockets/_index.md b/docs/content/docs/websockets/_index.md index d2a4caf..ac08a9f 100644 --- a/docs/content/docs/websockets/_index.md +++ b/docs/content/docs/websockets/_index.md @@ -1,6 +1,6 @@ --- title: WebSockets -description: "WebSockets — Http11Probe documentation" +description: "WebSocket Upgrade handshake validation tests, checking the Connection header, GET-only method requirement, and version negotiation per RFC 9110/6455." weight: 12 sidebar: open: false diff --git a/docs/content/docs/websockets/upgrade-http10.md b/docs/content/docs/websockets/upgrade-http10.md index f1b0861..549d2f1 100644 --- a/docs/content/docs/websockets/upgrade-http10.md +++ b/docs/content/docs/websockets/upgrade-http10.md @@ -1,47 +1,47 @@ ---- -title: "UPGRADE-HTTP10" -description: "UPGRADE-HTTP10 test documentation" -weight: 5 ---- - -| | | -|---|---| -| **Test ID** | `WS-UPGRADE-HTTP10` | -| **Category** | WebSockets | -| **Scored** | Yes | -| **RFC** | [RFC 9110 §7.8](https://www.rfc-editor.org/rfc/rfc9110#section-7.8) | -| **RFC Level** | MUST | -| **Expected** | Not `101` | - -## What it sends - -An HTTP/1.0 request with WebSocket upgrade headers. The server must ignore the Upgrade field because it was received in an HTTP/1.0 request. - -```http -GET / HTTP/1.0\r\n -Host: localhost:8080\r\n -Connection: Upgrade\r\n -Upgrade: websocket\r\n -Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n -Sec-WebSocket-Version: 13\r\n -\r\n -``` - -## What the RFC says - -> "A server that receives an Upgrade header field in an HTTP/1.0 request MUST ignore that Upgrade field." — RFC 9110 §7.8 - -The Upgrade mechanism is an HTTP/1.1 feature. An HTTP/1.0 client cannot participate in protocol switching, so the server must not attempt it. - -## Why it matters - -If a server processes an Upgrade from an HTTP/1.0 client and returns `101 Switching Protocols`, the client likely cannot handle the protocol switch. This could lead to connection corruption or security issues, especially if a proxy is involved that speaks HTTP/1.0 to the backend. - -## Verdicts - -- **Pass** — Server returns any status other than `101` (correctly ignored Upgrade) -- **Fail** — Server returns `101 Switching Protocols` (incorrectly upgraded an HTTP/1.0 request) - -## Sources - -- [RFC 9110 §7.8](https://www.rfc-editor.org/rfc/rfc9110#section-7.8) +--- +title: "Upgrade HTTP/1.0 — WebSocket Upgrade" +description: "An HTTP/1.0 request with WebSocket upgrade headers. The server must ignore the Upgrade field because it was received in an HTTP/1.0 request. Tested against RFC 9110 §7.8." +weight: 5 +--- + +| | | +|---|---| +| **Test ID** | `WS-UPGRADE-HTTP10` | +| **Category** | WebSockets | +| **Scored** | Yes | +| **RFC** | [RFC 9110 §7.8](https://www.rfc-editor.org/rfc/rfc9110#section-7.8) | +| **RFC Level** | MUST | +| **Expected** | Not `101` | + +## What it sends + +An HTTP/1.0 request with WebSocket upgrade headers. The server must ignore the Upgrade field because it was received in an HTTP/1.0 request. + +```http +GET / HTTP/1.0\r\n +Host: localhost:8080\r\n +Connection: Upgrade\r\n +Upgrade: websocket\r\n +Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n +Sec-WebSocket-Version: 13\r\n +\r\n +``` + +## What the RFC says + +> "A server that receives an Upgrade header field in an HTTP/1.0 request MUST ignore that Upgrade field." — RFC 9110 §7.8 + +The Upgrade mechanism is an HTTP/1.1 feature. An HTTP/1.0 client cannot participate in protocol switching, so the server must not attempt it. + +## Why it matters + +If a server processes an Upgrade from an HTTP/1.0 client and returns `101 Switching Protocols`, the client likely cannot handle the protocol switch. This could lead to connection corruption or security issues, especially if a proxy is involved that speaks HTTP/1.0 to the backend. + +## Verdicts + +- **Pass** — Server returns any status other than `101` (correctly ignored Upgrade) +- **Fail** — Server returns `101 Switching Protocols` (incorrectly upgraded an HTTP/1.0 request) + +## Sources + +- [RFC 9110 §7.8](https://www.rfc-editor.org/rfc/rfc9110#section-7.8) diff --git a/docs/content/docs/websockets/upgrade-invalid-ver.md b/docs/content/docs/websockets/upgrade-invalid-ver.md index 8695113..4bb833c 100644 --- a/docs/content/docs/websockets/upgrade-invalid-ver.md +++ b/docs/content/docs/websockets/upgrade-invalid-ver.md @@ -1,85 +1,85 @@ ---- -title: "UPGRADE-INVALID-VER" -description: "UPGRADE-INVALID-VER test documentation" -weight: 4 ---- - -| | | -|---|---| -| **Test ID** | `WS-UPGRADE-INVALID-VER` | -| **Category** | WebSockets | -| **RFC** | [RFC 6455 Section 4.4](https://www.rfc-editor.org/rfc/rfc6455#section-4.4) | -| **Requirement** | MUST abort handshake (426 preferred) | -| **Expected** | `426` or non-`101` | - -## What it sends - -A valid WebSocket upgrade request with `Sec-WebSocket-Version: 99` — a version the server does not support. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Connection: Upgrade\r\n -Upgrade: websocket\r\n -Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n -Sec-WebSocket-Version: 99\r\n -\r\n -``` - -WebSocket version `99` is not a valid version (only `13` is standard). - - -## What the RFC says - -> "If this version does not match a version understood by the server, the server MUST abort the WebSocket handshake described in this section and instead send an appropriate HTTP error code (such as 426 Upgrade Required) and a |Sec-WebSocket-Version| header field indicating the version(s) the server is capable of understanding." -- RFC 6455 Section 4.2.2 - -> "If the server doesn't support the requested version, it MUST respond with a |Sec-WebSocket-Version| header field (or multiple |Sec-WebSocket-Version| header fields) containing all versions it is willing to use." -- RFC 6455 Section 4.4 - -## Why it matters - -A server that returns `101` for an unsupported WebSocket version would enter an undefined protocol state. The ideal response is `426` with a `Sec-WebSocket-Version` header listing supported versions. Servers that don't support WebSocket at all will return 2xx (ignoring the upgrade), which is acceptable but noted as a warning. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -; From RFC 9110 Section 7.8: -Upgrade = 1#protocol -protocol = protocol-name [ "/" protocol-version ] -protocol-name = token -protocol-version = token -``` - -The WebSocket version is communicated via `Sec-WebSocket-Version`, not within the Upgrade header's protocol-version component. The only standardized WebSocket version is 13 (defined in RFC 6455). - -### RFC Evidence - -**RFC 6455 Section 4.2.2** mandates version validation by the server: - -> "If this version does not match a version understood by the server, the server MUST abort the WebSocket handshake described in this section and instead send an appropriate HTTP error code (such as 426 Upgrade Required) and a |Sec-WebSocket-Version| header field indicating the version(s) the server is capable of understanding." -- RFC 6455 Section 4.2.2 - -**RFC 6455 Section 4.4** reinforces the version negotiation requirement: - -> "If the server doesn't support the requested version, it MUST respond with a |Sec-WebSocket-Version| header field (or multiple |Sec-WebSocket-Version| header fields) containing all versions it is willing to use." -- RFC 6455 Section 4.4 - -**RFC 6455 Section 4.2** establishes the required version value: - -> "A |Sec-WebSocket-Version| header field, with a value of 13." -- RFC 6455 Section 4.2 - -### Chain of Reasoning - -1. The test sends `Sec-WebSocket-Version: 99` -- a version that no server supports (only version 13 is standardized). -2. RFC 6455 Section 4.2.2 requires the server to abort the WebSocket handshake and return an error code (suggesting 426) with a `Sec-WebSocket-Version` header listing supported versions. -3. The MUST in Section 4.2.2 is clear: the server must abort and send an error. The "such as 426" phrasing suggests 426 is the preferred but not the only acceptable error code. -4. Servers that do not implement WebSocket at all will ignore the Upgrade header and return 2xx (processing the GET normally). This is acceptable because the server is not entering a WebSocket protocol state. -5. A 101 response for version 99 is a critical failure: the server is claiming to switch to WebSocket version 99, which does not exist. The connection enters an undefined state. - -### Scoring Justification - -**Scored (MUST).** RFC 6455 Section 4.2.2 uses MUST for aborting the handshake on unsupported versions. The ideal response is 426 with `Sec-WebSocket-Version: 13`. Any non-101 response is acceptable (including 2xx from servers that do not support WebSocket). Only 101 is a fail, as it represents switching to an unsupported protocol version. The SHOULD-level preference for 426 specifically means that other 4xx codes are also passing but less informative. - -## Sources - -- [RFC 6455 Section 4.2.2](https://www.rfc-editor.org/rfc/rfc6455#section-4.2.2) -- [RFC 6455 Section 4.4](https://www.rfc-editor.org/rfc/rfc6455#section-4.4) +--- +title: "Upgrade Invalid Version — WebSocket Upgrade" +description: "A valid WebSocket upgrade request with Sec-WebSocket-Version: 99 — a version the server does not support. Tested against RFC 6455 Section 4.4." +weight: 4 +--- + +| | | +|---|---| +| **Test ID** | `WS-UPGRADE-INVALID-VER` | +| **Category** | WebSockets | +| **RFC** | [RFC 6455 Section 4.4](https://www.rfc-editor.org/rfc/rfc6455#section-4.4) | +| **Requirement** | MUST abort handshake (426 preferred) | +| **Expected** | `426` or non-`101` | + +## What it sends + +A valid WebSocket upgrade request with `Sec-WebSocket-Version: 99` — a version the server does not support. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Connection: Upgrade\r\n +Upgrade: websocket\r\n +Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n +Sec-WebSocket-Version: 99\r\n +\r\n +``` + +WebSocket version `99` is not a valid version (only `13` is standard). + + +## What the RFC says + +> "If this version does not match a version understood by the server, the server MUST abort the WebSocket handshake described in this section and instead send an appropriate HTTP error code (such as 426 Upgrade Required) and a |Sec-WebSocket-Version| header field indicating the version(s) the server is capable of understanding." -- RFC 6455 Section 4.2.2 + +> "If the server doesn't support the requested version, it MUST respond with a |Sec-WebSocket-Version| header field (or multiple |Sec-WebSocket-Version| header fields) containing all versions it is willing to use." -- RFC 6455 Section 4.4 + +## Why it matters + +A server that returns `101` for an unsupported WebSocket version would enter an undefined protocol state. The ideal response is `426` with a `Sec-WebSocket-Version` header listing supported versions. Servers that don't support WebSocket at all will return 2xx (ignoring the upgrade), which is acceptable but noted as a warning. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +; From RFC 9110 Section 7.8: +Upgrade = 1#protocol +protocol = protocol-name [ "/" protocol-version ] +protocol-name = token +protocol-version = token +``` + +The WebSocket version is communicated via `Sec-WebSocket-Version`, not within the Upgrade header's protocol-version component. The only standardized WebSocket version is 13 (defined in RFC 6455). + +### RFC Evidence + +**RFC 6455 Section 4.2.2** mandates version validation by the server: + +> "If this version does not match a version understood by the server, the server MUST abort the WebSocket handshake described in this section and instead send an appropriate HTTP error code (such as 426 Upgrade Required) and a |Sec-WebSocket-Version| header field indicating the version(s) the server is capable of understanding." -- RFC 6455 Section 4.2.2 + +**RFC 6455 Section 4.4** reinforces the version negotiation requirement: + +> "If the server doesn't support the requested version, it MUST respond with a |Sec-WebSocket-Version| header field (or multiple |Sec-WebSocket-Version| header fields) containing all versions it is willing to use." -- RFC 6455 Section 4.4 + +**RFC 6455 Section 4.2** establishes the required version value: + +> "A |Sec-WebSocket-Version| header field, with a value of 13." -- RFC 6455 Section 4.2 + +### Chain of Reasoning + +1. The test sends `Sec-WebSocket-Version: 99` -- a version that no server supports (only version 13 is standardized). +2. RFC 6455 Section 4.2.2 requires the server to abort the WebSocket handshake and return an error code (suggesting 426) with a `Sec-WebSocket-Version` header listing supported versions. +3. The MUST in Section 4.2.2 is clear: the server must abort and send an error. The "such as 426" phrasing suggests 426 is the preferred but not the only acceptable error code. +4. Servers that do not implement WebSocket at all will ignore the Upgrade header and return 2xx (processing the GET normally). This is acceptable because the server is not entering a WebSocket protocol state. +5. A 101 response for version 99 is a critical failure: the server is claiming to switch to WebSocket version 99, which does not exist. The connection enters an undefined state. + +### Scoring Justification + +**Scored (MUST).** RFC 6455 Section 4.2.2 uses MUST for aborting the handshake on unsupported versions. The ideal response is 426 with `Sec-WebSocket-Version: 13`. Any non-101 response is acceptable (including 2xx from servers that do not support WebSocket). Only 101 is a fail, as it represents switching to an unsupported protocol version. The SHOULD-level preference for 426 specifically means that other 4xx codes are also passing but less informative. + +## Sources + +- [RFC 6455 Section 4.2.2](https://www.rfc-editor.org/rfc/rfc6455#section-4.2.2) +- [RFC 6455 Section 4.4](https://www.rfc-editor.org/rfc/rfc6455#section-4.4) diff --git a/docs/content/docs/websockets/upgrade-missing-conn.md b/docs/content/docs/websockets/upgrade-missing-conn.md index 2bf3e55..a513954 100644 --- a/docs/content/docs/websockets/upgrade-missing-conn.md +++ b/docs/content/docs/websockets/upgrade-missing-conn.md @@ -1,93 +1,93 @@ ---- -title: "UPGRADE-MISSING-CONN" -description: "UPGRADE-MISSING-CONN test documentation" -weight: 2 ---- - -| | | -|---|---| -| **Test ID** | `WS-UPGRADE-MISSING-CONN` | -| **Category** | WebSockets | -| **RFC** | [RFC 9110 Section 7.8](https://www.rfc-editor.org/rfc/rfc9110#section-7.8) | -| **Requirement** | MUST NOT switch | -| **Expected** | any non-`101` response | - -## What it sends - -A GET request with `Upgrade: websocket` and WebSocket handshake headers, but **without** the required `Connection: Upgrade` header. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Upgrade: websocket\r\n -Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n -Sec-WebSocket-Version: 13\r\n -\r\n -``` - -No `Connection: Upgrade` header — the `Upgrade` header is present alone. - - -## What the RFC says - -> "A sender of Upgrade MUST also send an 'Upgrade' connection option in the Connection header field (Section 7.6.1) to inform intermediaries not to forward this field." -- RFC 9110 Section 7.8 - -The WebSocket RFC reinforces this at the server side: - -> "A |Connection| header field that includes the token 'Upgrade', treated as an ASCII case-insensitive value." -- RFC 6455 Section 4.2.1 (required handshake element #4) - -> "If the server, while reading the handshake, finds that the client did not send a handshake that matches the description below... the server MUST stop processing the client's handshake and return an HTTP response with an appropriate error code (such as 400 Bad Request)." -- RFC 6455 Section 4.2.1 - -Without `Connection: Upgrade`, the Upgrade header is hop-by-hop metadata that intermediaries should strip. A server that switches protocol anyway could be tricked via proxies. - -## Why it matters - -If a server switches protocol without `Connection: Upgrade`, a proxy forwarding the request would not know the connection semantics changed. This can lead to connection desync and smuggling through WebSocket-unaware intermediaries. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -; From RFC 9110 Section 7.8: -Upgrade = 1#protocol -protocol = protocol-name [ "/" protocol-version ] -protocol-name = token - -; From RFC 9110 Section 7.6.1: -Connection = 1#connection-option -connection-option = token -``` - -The Upgrade and Connection headers work in tandem. The Upgrade header names the target protocol, while the `Connection: Upgrade` token signals that the Upgrade header is hop-by-hop and should not be forwarded by intermediaries. - -### RFC Evidence - -**RFC 9110 Section 7.8** mandates the pairing of Upgrade with Connection: - -> "A sender of Upgrade MUST also send an 'Upgrade' connection option in the Connection header field to inform intermediaries not to forward this field." -- RFC 9110 Section 7.8 - -**RFC 6455 Section 4.2** lists Connection as a required handshake element: - -> "A |Connection| header field that includes the token 'Upgrade', treated as an ASCII case-insensitive value." -- RFC 6455 Section 4.2 - -**RFC 6455 Section 4.2** mandates rejection of incomplete handshakes: - -> "If the server, while reading the handshake, finds that the client did not send a handshake that matches the description below...the server MUST stop processing the client's handshake and return an HTTP response with an appropriate error code (such as 400 Bad Request)." -- RFC 6455 Section 4.2 - -### Chain of Reasoning - -1. The test sends `Upgrade: websocket` with all other WebSocket headers, but omits `Connection: Upgrade`. -2. RFC 9110 Section 7.8 requires senders to include `Connection: Upgrade` alongside the Upgrade header. Without it, the Upgrade header is technically malformed from the sender's perspective. -3. From the server's perspective, the absence of `Connection: Upgrade` means an intermediary may have already stripped the Upgrade header's hop-by-hop semantics. The server should treat the Upgrade header as if it were not present. -4. RFC 6455 Section 4.2 lists `Connection: Upgrade` as a required element of the WebSocket handshake. Without it, the handshake is incomplete and the server MUST stop processing it. -5. A server that switches protocol without `Connection: Upgrade` creates a dangerous situation: any proxy in the request chain would be unaware that the connection semantics have changed, leading to connection desynchronization and potential smuggling. - -### Scoring Justification - -**Scored (MUST).** Both RFC 9110 Section 7.8 (MUST send Connection: Upgrade) and RFC 6455 Section 4.2 (MUST stop processing incomplete handshakes) establish MUST-level requirements. Any non-101 response is a pass. A 101 response is a fail because the server switched protocols without the required `Connection: Upgrade` header, violating both the HTTP and WebSocket specifications. - -## Sources - -- [RFC 9110 Section 7.8](https://www.rfc-editor.org/rfc/rfc9110#section-7.8) -- [RFC 6455 Section 4.1](https://www.rfc-editor.org/rfc/rfc6455#section-4.1) +--- +title: "Upgrade Missing Conn — WebSocket Upgrade" +description: "A GET request with Upgrade: websocket and WebSocket handshake headers, but without the required Connection: Upgrade header. Tested against RFC 9110 Section 7.8." +weight: 2 +--- + +| | | +|---|---| +| **Test ID** | `WS-UPGRADE-MISSING-CONN` | +| **Category** | WebSockets | +| **RFC** | [RFC 9110 Section 7.8](https://www.rfc-editor.org/rfc/rfc9110#section-7.8) | +| **Requirement** | MUST NOT switch | +| **Expected** | any non-`101` response | + +## What it sends + +A GET request with `Upgrade: websocket` and WebSocket handshake headers, but **without** the required `Connection: Upgrade` header. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Upgrade: websocket\r\n +Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n +Sec-WebSocket-Version: 13\r\n +\r\n +``` + +No `Connection: Upgrade` header — the `Upgrade` header is present alone. + + +## What the RFC says + +> "A sender of Upgrade MUST also send an 'Upgrade' connection option in the Connection header field (Section 7.6.1) to inform intermediaries not to forward this field." -- RFC 9110 Section 7.8 + +The WebSocket RFC reinforces this at the server side: + +> "A |Connection| header field that includes the token 'Upgrade', treated as an ASCII case-insensitive value." -- RFC 6455 Section 4.2.1 (required handshake element #4) + +> "If the server, while reading the handshake, finds that the client did not send a handshake that matches the description below... the server MUST stop processing the client's handshake and return an HTTP response with an appropriate error code (such as 400 Bad Request)." -- RFC 6455 Section 4.2.1 + +Without `Connection: Upgrade`, the Upgrade header is hop-by-hop metadata that intermediaries should strip. A server that switches protocol anyway could be tricked via proxies. + +## Why it matters + +If a server switches protocol without `Connection: Upgrade`, a proxy forwarding the request would not know the connection semantics changed. This can lead to connection desync and smuggling through WebSocket-unaware intermediaries. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +; From RFC 9110 Section 7.8: +Upgrade = 1#protocol +protocol = protocol-name [ "/" protocol-version ] +protocol-name = token + +; From RFC 9110 Section 7.6.1: +Connection = 1#connection-option +connection-option = token +``` + +The Upgrade and Connection headers work in tandem. The Upgrade header names the target protocol, while the `Connection: Upgrade` token signals that the Upgrade header is hop-by-hop and should not be forwarded by intermediaries. + +### RFC Evidence + +**RFC 9110 Section 7.8** mandates the pairing of Upgrade with Connection: + +> "A sender of Upgrade MUST also send an 'Upgrade' connection option in the Connection header field to inform intermediaries not to forward this field." -- RFC 9110 Section 7.8 + +**RFC 6455 Section 4.2** lists Connection as a required handshake element: + +> "A |Connection| header field that includes the token 'Upgrade', treated as an ASCII case-insensitive value." -- RFC 6455 Section 4.2 + +**RFC 6455 Section 4.2** mandates rejection of incomplete handshakes: + +> "If the server, while reading the handshake, finds that the client did not send a handshake that matches the description below...the server MUST stop processing the client's handshake and return an HTTP response with an appropriate error code (such as 400 Bad Request)." -- RFC 6455 Section 4.2 + +### Chain of Reasoning + +1. The test sends `Upgrade: websocket` with all other WebSocket headers, but omits `Connection: Upgrade`. +2. RFC 9110 Section 7.8 requires senders to include `Connection: Upgrade` alongside the Upgrade header. Without it, the Upgrade header is technically malformed from the sender's perspective. +3. From the server's perspective, the absence of `Connection: Upgrade` means an intermediary may have already stripped the Upgrade header's hop-by-hop semantics. The server should treat the Upgrade header as if it were not present. +4. RFC 6455 Section 4.2 lists `Connection: Upgrade` as a required element of the WebSocket handshake. Without it, the handshake is incomplete and the server MUST stop processing it. +5. A server that switches protocol without `Connection: Upgrade` creates a dangerous situation: any proxy in the request chain would be unaware that the connection semantics have changed, leading to connection desynchronization and potential smuggling. + +### Scoring Justification + +**Scored (MUST).** Both RFC 9110 Section 7.8 (MUST send Connection: Upgrade) and RFC 6455 Section 4.2 (MUST stop processing incomplete handshakes) establish MUST-level requirements. Any non-101 response is a pass. A 101 response is a fail because the server switched protocols without the required `Connection: Upgrade` header, violating both the HTTP and WebSocket specifications. + +## Sources + +- [RFC 9110 Section 7.8](https://www.rfc-editor.org/rfc/rfc9110#section-7.8) +- [RFC 6455 Section 4.1](https://www.rfc-editor.org/rfc/rfc6455#section-4.1) diff --git a/docs/content/docs/websockets/upgrade-post.md b/docs/content/docs/websockets/upgrade-post.md index 62756a8..a914568 100644 --- a/docs/content/docs/websockets/upgrade-post.md +++ b/docs/content/docs/websockets/upgrade-post.md @@ -1,88 +1,88 @@ ---- -title: "UPGRADE-POST" -description: "UPGRADE-POST test documentation" -weight: 1 ---- - -| | | -|---|---| -| **Test ID** | `WS-UPGRADE-POST` | -| **Category** | WebSockets | -| **RFC** | [RFC 6455 Section 4.1](https://www.rfc-editor.org/rfc/rfc6455#section-4.1) | -| **Requirement** | MUST use GET | -| **Expected** | any non-`101` response | - -## What it sends - -A WebSocket upgrade request using `POST` instead of `GET`, with all standard WebSocket headers (`Connection: Upgrade`, `Upgrade: websocket`, `Sec-WebSocket-Key`, `Sec-WebSocket-Version: 13`). - -```http -POST / HTTP/1.1\r\n -Host: localhost:8080\r\n -Connection: Upgrade\r\n -Upgrade: websocket\r\n -Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n -Sec-WebSocket-Version: 13\r\n -\r\n -``` - - -## What the RFC says - -> "The method of the request MUST be GET, and the HTTP version MUST be at least 1.1." -- RFC 6455 Section 4.1 - -The server-side validation reinforces this: - -> "The client's opening handshake consists of the following parts. If the server, while reading the handshake, finds that the client did not send a handshake that matches the description below... the server MUST stop processing the client's handshake and return an HTTP response with an appropriate error code (such as 400 Bad Request)." -- RFC 6455 Section 4.2.1 - -> "1. An HTTP/1.1 or higher GET request" -- RFC 6455 Section 4.2.1 - -The opening handshake is defined exclusively for GET. A server that accepts an upgrade via POST is violating the WebSocket protocol. - -## Why it matters - -Allowing WebSocket upgrades on non-GET methods expands the attack surface. POST-based upgrades could bypass CSRF protections or WAF rules that only inspect GET-based WebSocket handshakes. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -; From RFC 9110 Section 7.8: -Upgrade = 1#protocol -protocol = protocol-name [ "/" protocol-version ] -protocol-name = token -protocol-version = token -``` - -The Upgrade header grammar defines the protocol negotiation syntax, but the WebSocket-specific requirement comes from RFC 6455, which restricts the opening handshake to the GET method only. - -### RFC Evidence - -**RFC 6455 Section 4.1** mandates the method: - -> "The method of the request MUST be GET, and the HTTP version MUST be at least 1.1." -- RFC 6455 Section 4.1 - -**RFC 6455 Section 4.2** mandates server-side validation: - -> "The server MUST stop processing the client's handshake and return an HTTP response with an appropriate error code (such as 400 Bad Request)" -- RFC 6455 Section 4.2 - -**RFC 6455 Section 4.2** specifies the expected structure: - -> "An HTTP/1.1 or higher GET request, including a 'Request-URI'...that should be interpreted as a /resource name/" -- RFC 6455 Section 4.2 - -### Chain of Reasoning - -1. The test sends a WebSocket upgrade request using POST instead of GET. All other WebSocket headers are correct (`Connection: Upgrade`, `Upgrade: websocket`, `Sec-WebSocket-Key`, `Sec-WebSocket-Version: 13`). -2. RFC 6455 Section 4.1 uses MUST for the GET method requirement. The opening handshake is exclusively defined for GET. -3. On the server side, RFC 6455 Section 4.2 requires the server to validate that the request is "An HTTP/1.1 or higher GET request." If validation fails, the server "MUST stop processing the client's handshake and return an HTTP response with an appropriate error code." -4. The test expects any non-101 response. A 400 (Bad Request) or 405 (Method Not Allowed) would be ideal, but even a 200 (ignoring the upgrade entirely) is acceptable because it means the server did not switch protocols. -5. A 101 response to a POST-based upgrade is a compliance failure -- the server entered the WebSocket protocol through an unauthorized method, potentially bypassing WAF rules and CSRF protections that only inspect GET-based handshakes. - -### Scoring Justification - -**Scored (MUST).** RFC 6455 requires GET for the WebSocket opening handshake. Any non-101 response is a pass because it means the server did not incorrectly switch protocols. A 101 response is a fail because the server accepted a WebSocket upgrade via an unauthorized method. Servers that do not support WebSocket at all will naturally pass this test by returning 2xx or 4xx for the POST request. - -## Sources - -- [RFC 6455 Section 4.1](https://www.rfc-editor.org/rfc/rfc6455#section-4.1) +--- +title: "Upgrade POST — WebSocket Upgrade" +description: "A WebSocket upgrade request using POST instead of GET, with all standard WebSocket headers (Connection: Upgrade, Upgrade: websocket, Sec-WebSocket-Key, Sec-WebSocket-Version: 13)." +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `WS-UPGRADE-POST` | +| **Category** | WebSockets | +| **RFC** | [RFC 6455 Section 4.1](https://www.rfc-editor.org/rfc/rfc6455#section-4.1) | +| **Requirement** | MUST use GET | +| **Expected** | any non-`101` response | + +## What it sends + +A WebSocket upgrade request using `POST` instead of `GET`, with all standard WebSocket headers (`Connection: Upgrade`, `Upgrade: websocket`, `Sec-WebSocket-Key`, `Sec-WebSocket-Version: 13`). + +```http +POST / HTTP/1.1\r\n +Host: localhost:8080\r\n +Connection: Upgrade\r\n +Upgrade: websocket\r\n +Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n +Sec-WebSocket-Version: 13\r\n +\r\n +``` + + +## What the RFC says + +> "The method of the request MUST be GET, and the HTTP version MUST be at least 1.1." -- RFC 6455 Section 4.1 + +The server-side validation reinforces this: + +> "The client's opening handshake consists of the following parts. If the server, while reading the handshake, finds that the client did not send a handshake that matches the description below... the server MUST stop processing the client's handshake and return an HTTP response with an appropriate error code (such as 400 Bad Request)." -- RFC 6455 Section 4.2.1 + +> "1. An HTTP/1.1 or higher GET request" -- RFC 6455 Section 4.2.1 + +The opening handshake is defined exclusively for GET. A server that accepts an upgrade via POST is violating the WebSocket protocol. + +## Why it matters + +Allowing WebSocket upgrades on non-GET methods expands the attack surface. POST-based upgrades could bypass CSRF protections or WAF rules that only inspect GET-based WebSocket handshakes. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +; From RFC 9110 Section 7.8: +Upgrade = 1#protocol +protocol = protocol-name [ "/" protocol-version ] +protocol-name = token +protocol-version = token +``` + +The Upgrade header grammar defines the protocol negotiation syntax, but the WebSocket-specific requirement comes from RFC 6455, which restricts the opening handshake to the GET method only. + +### RFC Evidence + +**RFC 6455 Section 4.1** mandates the method: + +> "The method of the request MUST be GET, and the HTTP version MUST be at least 1.1." -- RFC 6455 Section 4.1 + +**RFC 6455 Section 4.2** mandates server-side validation: + +> "The server MUST stop processing the client's handshake and return an HTTP response with an appropriate error code (such as 400 Bad Request)" -- RFC 6455 Section 4.2 + +**RFC 6455 Section 4.2** specifies the expected structure: + +> "An HTTP/1.1 or higher GET request, including a 'Request-URI'...that should be interpreted as a /resource name/" -- RFC 6455 Section 4.2 + +### Chain of Reasoning + +1. The test sends a WebSocket upgrade request using POST instead of GET. All other WebSocket headers are correct (`Connection: Upgrade`, `Upgrade: websocket`, `Sec-WebSocket-Key`, `Sec-WebSocket-Version: 13`). +2. RFC 6455 Section 4.1 uses MUST for the GET method requirement. The opening handshake is exclusively defined for GET. +3. On the server side, RFC 6455 Section 4.2 requires the server to validate that the request is "An HTTP/1.1 or higher GET request." If validation fails, the server "MUST stop processing the client's handshake and return an HTTP response with an appropriate error code." +4. The test expects any non-101 response. A 400 (Bad Request) or 405 (Method Not Allowed) would be ideal, but even a 200 (ignoring the upgrade entirely) is acceptable because it means the server did not switch protocols. +5. A 101 response to a POST-based upgrade is a compliance failure -- the server entered the WebSocket protocol through an unauthorized method, potentially bypassing WAF rules and CSRF protections that only inspect GET-based handshakes. + +### Scoring Justification + +**Scored (MUST).** RFC 6455 requires GET for the WebSocket opening handshake. Any non-101 response is a pass because it means the server did not incorrectly switch protocols. A 101 response is a fail because the server accepted a WebSocket upgrade via an unauthorized method. Servers that do not support WebSocket at all will naturally pass this test by returning 2xx or 4xx for the POST request. + +## Sources + +- [RFC 6455 Section 4.1](https://www.rfc-editor.org/rfc/rfc6455#section-4.1) diff --git a/docs/content/docs/websockets/upgrade-unknown.md b/docs/content/docs/websockets/upgrade-unknown.md index 4756187..2fa531b 100644 --- a/docs/content/docs/websockets/upgrade-unknown.md +++ b/docs/content/docs/websockets/upgrade-unknown.md @@ -1,83 +1,83 @@ ---- -title: "UPGRADE-UNKNOWN" -description: "UPGRADE-UNKNOWN test documentation" -weight: 3 ---- - -| | | -|---|---| -| **Test ID** | `WS-UPGRADE-UNKNOWN` | -| **Category** | WebSockets | -| **RFC** | [RFC 9110 Section 7.8](https://www.rfc-editor.org/rfc/rfc9110#section-7.8) | -| **Requirement** | MUST NOT switch | -| **Expected** | any non-`101` response | - -## What it sends - -A GET request with `Connection: Upgrade` and `Upgrade: totally-made-up/1.0` — a protocol the server cannot possibly support. - -```http -GET / HTTP/1.1\r\n -Host: localhost:8080\r\n -Connection: Upgrade\r\n -Upgrade: totally-made-up/1.0\r\n -\r\n -``` - - -## What the RFC says - -> "A server MAY ignore a received Upgrade header field if it wishes to continue using the current protocol on that connection. Upgrade cannot be used to insist on a protocol change." -- RFC 9110 Section 7.8 - -> "A server MUST NOT switch to a protocol that was not indicated by the client in the corresponding request's Upgrade header field." -- RFC 9110 Section 7.8 - -> "A server MUST NOT switch protocols unless the received message semantics can be honored by the new protocol; an OPTIONS request can be honored by any protocol." -- RFC 9110 Section 7.8 - -A server must not respond with `101 Switching Protocols` for a protocol it does not implement. It should ignore the Upgrade header and process the request normally (2xx) or reject it. - -## Why it matters - -A server that blindly returns `101` for any Upgrade value is broken — it switches the connection to an undefined state, potentially leaving the TCP stream in a desynced state exploitable for smuggling. - -## Deep Analysis - -### Relevant ABNF Grammar - -``` -Upgrade = 1#protocol -protocol = protocol-name [ "/" protocol-version ] -protocol-name = token -protocol-version = token -``` - -The Upgrade grammar accepts any `token "/" token` combination as a protocol identifier. The value `totally-made-up/1.0` is syntactically valid per this grammar -- it is a well-formed protocol name with a version. The issue is semantic: the server does not implement this protocol. - -### RFC Evidence - -**RFC 9110 Section 7.8** permits servers to ignore the Upgrade: - -> "A server MAY ignore a received Upgrade header field if it wishes to continue using the current protocol on that connection. Upgrade cannot be used to insist on a protocol change." -- RFC 9110 Section 7.8 - -**RFC 9110 Section 7.8** prohibits switching to unlisted protocols: - -> "A server MUST NOT switch to a protocol that was not indicated by the client in the corresponding request's Upgrade header field." -- RFC 9110 Section 7.8 - -**RFC 9110 Section 7.8** requires semantic compatibility: - -> "A server MUST NOT switch protocols unless the received message semantics can be honored by the new protocol; an OPTIONS request can be honored by any protocol." -- RFC 9110 Section 7.8 - -### Chain of Reasoning - -1. The test sends `Upgrade: totally-made-up/1.0` -- a protocol that no server implements. -2. The server has two compliant options: (a) ignore the Upgrade header and process the GET request normally (returning 2xx), or (b) return a non-101 error response. -3. A server MUST NOT respond with 101 for a protocol it does not implement. The 101 status code means "I am switching to the protocol you requested," which is impossible for an unknown protocol. -4. If a server blindly returns 101 for any Upgrade value, the TCP connection enters an undefined state. The client expects to speak the requested protocol, but the server has no implementation. This desync can be exploited for smuggling. -5. The MUST NOT in "A server MUST NOT switch to a protocol that was not indicated by the client" is technically about switching to a different protocol than requested. But the stronger implication is that a server cannot switch to a protocol it does not support at all. - -### Scoring Justification - -**Scored (MUST NOT).** A 101 response for an unknown protocol violates the MUST NOT requirements in RFC 9110 Section 7.8. Any non-101 response is a pass, including 2xx (server ignored the Upgrade and processed the GET normally) or 4xx (server rejected the request). Only 101 is a fail, as it indicates the server attempted to switch to a protocol it cannot support. - -## Sources - -- [RFC 9110 Section 7.8](https://www.rfc-editor.org/rfc/rfc9110#section-7.8) +--- +title: "Upgrade Unknown — WebSocket Upgrade" +description: "A GET request with Connection: Upgrade and Upgrade: totally-made-up/1.0 — a protocol the server cannot possibly support. Tested against RFC 9110 Section 7.8." +weight: 3 +--- + +| | | +|---|---| +| **Test ID** | `WS-UPGRADE-UNKNOWN` | +| **Category** | WebSockets | +| **RFC** | [RFC 9110 Section 7.8](https://www.rfc-editor.org/rfc/rfc9110#section-7.8) | +| **Requirement** | MUST NOT switch | +| **Expected** | any non-`101` response | + +## What it sends + +A GET request with `Connection: Upgrade` and `Upgrade: totally-made-up/1.0` — a protocol the server cannot possibly support. + +```http +GET / HTTP/1.1\r\n +Host: localhost:8080\r\n +Connection: Upgrade\r\n +Upgrade: totally-made-up/1.0\r\n +\r\n +``` + + +## What the RFC says + +> "A server MAY ignore a received Upgrade header field if it wishes to continue using the current protocol on that connection. Upgrade cannot be used to insist on a protocol change." -- RFC 9110 Section 7.8 + +> "A server MUST NOT switch to a protocol that was not indicated by the client in the corresponding request's Upgrade header field." -- RFC 9110 Section 7.8 + +> "A server MUST NOT switch protocols unless the received message semantics can be honored by the new protocol; an OPTIONS request can be honored by any protocol." -- RFC 9110 Section 7.8 + +A server must not respond with `101 Switching Protocols` for a protocol it does not implement. It should ignore the Upgrade header and process the request normally (2xx) or reject it. + +## Why it matters + +A server that blindly returns `101` for any Upgrade value is broken — it switches the connection to an undefined state, potentially leaving the TCP stream in a desynced state exploitable for smuggling. + +## Deep Analysis + +### Relevant ABNF Grammar + +``` +Upgrade = 1#protocol +protocol = protocol-name [ "/" protocol-version ] +protocol-name = token +protocol-version = token +``` + +The Upgrade grammar accepts any `token "/" token` combination as a protocol identifier. The value `totally-made-up/1.0` is syntactically valid per this grammar -- it is a well-formed protocol name with a version. The issue is semantic: the server does not implement this protocol. + +### RFC Evidence + +**RFC 9110 Section 7.8** permits servers to ignore the Upgrade: + +> "A server MAY ignore a received Upgrade header field if it wishes to continue using the current protocol on that connection. Upgrade cannot be used to insist on a protocol change." -- RFC 9110 Section 7.8 + +**RFC 9110 Section 7.8** prohibits switching to unlisted protocols: + +> "A server MUST NOT switch to a protocol that was not indicated by the client in the corresponding request's Upgrade header field." -- RFC 9110 Section 7.8 + +**RFC 9110 Section 7.8** requires semantic compatibility: + +> "A server MUST NOT switch protocols unless the received message semantics can be honored by the new protocol; an OPTIONS request can be honored by any protocol." -- RFC 9110 Section 7.8 + +### Chain of Reasoning + +1. The test sends `Upgrade: totally-made-up/1.0` -- a protocol that no server implements. +2. The server has two compliant options: (a) ignore the Upgrade header and process the GET request normally (returning 2xx), or (b) return a non-101 error response. +3. A server MUST NOT respond with 101 for a protocol it does not implement. The 101 status code means "I am switching to the protocol you requested," which is impossible for an unknown protocol. +4. If a server blindly returns 101 for any Upgrade value, the TCP connection enters an undefined state. The client expects to speak the requested protocol, but the server has no implementation. This desync can be exploited for smuggling. +5. The MUST NOT in "A server MUST NOT switch to a protocol that was not indicated by the client" is technically about switching to a different protocol than requested. But the stronger implication is that a server cannot switch to a protocol it does not support at all. + +### Scoring Justification + +**Scored (MUST NOT).** A 101 response for an unknown protocol violates the MUST NOT requirements in RFC 9110 Section 7.8. Any non-101 response is a pass, including 2xx (server ignored the Upgrade and processed the GET normally) or 4xx (server rejected the request). Only 101 is a fail, as it indicates the server attempted to switch to a protocol it cannot support. + +## Sources + +- [RFC 9110 Section 7.8](https://www.rfc-editor.org/rfc/rfc9110#section-7.8) diff --git a/docs/content/malformed-input/_index.md b/docs/content/malformed-input/_index.md index 8e13002..bf9c725 100644 --- a/docs/content/malformed-input/_index.md +++ b/docs/content/malformed-input/_index.md @@ -1,5 +1,6 @@ --- title: Malformed Input +description: "How HTTP/1.1 servers handle oversized, binary, and pathological request data, testing for graceful 400/414/431 rejection instead of crashes or hangs." layout: wide toc: false --- diff --git a/docs/content/normalization/_index.md b/docs/content/normalization/_index.md index d282489..2d9322f 100644 --- a/docs/content/normalization/_index.md +++ b/docs/content/normalization/_index.md @@ -1,5 +1,6 @@ --- title: Normalization +description: "Header normalization test results, showing whether servers drop, preserve, or silently rewrite malformed header names in ways that create request smuggling risk." layout: wide toc: false --- diff --git a/docs/content/probe-results/_index.md b/docs/content/probe-results/_index.md index 2300f3e..f6eb4b5 100644 --- a/docs/content/probe-results/_index.md +++ b/docs/content/probe-results/_index.md @@ -1,5 +1,6 @@ --- title: Probe Results +description: "Side-by-side HTTP/1.1 compliance leaderboard comparing servers and frameworks against RFC 9110/9112 requirements, request smuggling, and malformed input handling." layout: wide toc: false --- diff --git a/docs/content/servers/_index.md b/docs/content/servers/_index.md index faf4512..b794a77 100644 --- a/docs/content/servers/_index.md +++ b/docs/content/servers/_index.md @@ -1,5 +1,6 @@ --- title: "Servers" +description: "Dockerfile and source code for every HTTP/1.1 server and framework tested by Http11Probe, with links to per-server RFC 9110/9112 compliance results." toc: false breadcrumbs: false sidebar: diff --git a/docs/content/servers/actix.md b/docs/content/servers/actix.md index a51b24c..1da1025 100644 --- a/docs/content/servers/actix.md +++ b/docs/content/servers/actix.md @@ -1,118 +1,119 @@ ---- -title: "Actix" -toc: true -breadcrumbs: false ---- - -**Language:** Rust · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/ActixServer) - -## Dockerfile - -```dockerfile -FROM rust:1-slim AS build -WORKDIR /src - -# Cache dependencies with dummy main -COPY src/Servers/ActixServer/Cargo.toml . -RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src target/release/.fingerprint/actix-server-* - -COPY src/Servers/ActixServer/src/ src/ -RUN cargo build --release - -FROM debian:bookworm-slim -COPY --from=build /src/target/release/actix-server /usr/local/bin/ -ENTRYPOINT ["actix-server", "8080"] -``` - -## Source - -```rust -use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse, Responder}; - -async fn echo(req: HttpRequest) -> impl Responder { - let mut body = String::new(); - for (name, value) in req.headers() { - body.push_str(&format!("{}: {}\n", name, value.to_str().unwrap_or(""))); - } - HttpResponse::Ok().content_type("text/plain").body(body) -} - -async fn cookie(req: HttpRequest) -> impl Responder { - let mut body = String::new(); - if let Some(raw) = req.headers().get("cookie").and_then(|v| v.to_str().ok()) { - for pair in raw.split(';') { - let trimmed = pair.trim_start(); - if let Some(eq) = trimmed.find('=') { - body.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); - } - } - } - HttpResponse::Ok().content_type("text/plain").body(body) -} - -async fn handler(req: HttpRequest, body: web::Bytes) -> HttpResponse { - if req.method() == actix_web::http::Method::POST { - HttpResponse::Ok() - .content_type("text/plain") - .body(body) - } else { - HttpResponse::Ok() - .content_type("text/plain") - .body("OK") - } -} - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - let port: u16 = std::env::args() - .nth(1) - .and_then(|s| s.parse().ok()) - .unwrap_or(8080); - - HttpServer::new(|| { - App::new() - .route("/echo", web::to(echo)) - .route("/cookie", web::to(cookie)) - .default_service(web::to(handler)) - }) - .bind(("0.0.0.0", port))? - .run() - .await -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Actix" +description: "Actix (Rust) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Rust · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/ActixServer) + +## Dockerfile + +```dockerfile +FROM rust:1-slim AS build +WORKDIR /src + +# Cache dependencies with dummy main +COPY src/Servers/ActixServer/Cargo.toml . +RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src target/release/.fingerprint/actix-server-* + +COPY src/Servers/ActixServer/src/ src/ +RUN cargo build --release + +FROM debian:bookworm-slim +COPY --from=build /src/target/release/actix-server /usr/local/bin/ +ENTRYPOINT ["actix-server", "8080"] +``` + +## Source + +```rust +use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse, Responder}; + +async fn echo(req: HttpRequest) -> impl Responder { + let mut body = String::new(); + for (name, value) in req.headers() { + body.push_str(&format!("{}: {}\n", name, value.to_str().unwrap_or(""))); + } + HttpResponse::Ok().content_type("text/plain").body(body) +} + +async fn cookie(req: HttpRequest) -> impl Responder { + let mut body = String::new(); + if let Some(raw) = req.headers().get("cookie").and_then(|v| v.to_str().ok()) { + for pair in raw.split(';') { + let trimmed = pair.trim_start(); + if let Some(eq) = trimmed.find('=') { + body.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); + } + } + } + HttpResponse::Ok().content_type("text/plain").body(body) +} + +async fn handler(req: HttpRequest, body: web::Bytes) -> HttpResponse { + if req.method() == actix_web::http::Method::POST { + HttpResponse::Ok() + .content_type("text/plain") + .body(body) + } else { + HttpResponse::Ok() + .content_type("text/plain") + .body("OK") + } +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let port: u16 = std::env::args() + .nth(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(8080); + + HttpServer::new(|| { + App::new() + .route("/echo", web::to(echo)) + .route("/cookie", web::to(cookie)) + .default_service(web::to(handler)) + }) + .bind(("0.0.0.0", port))? + .run() + .await +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/apache.md b/docs/content/servers/apache.md index 8cf7dd7..7a57ded 100644 --- a/docs/content/servers/apache.md +++ b/docs/content/servers/apache.md @@ -1,117 +1,118 @@ ---- -title: "Apache" -toc: true -breadcrumbs: false ---- - -**Language:** C · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/ApacheServer) - -## Dockerfile - -```dockerfile -FROM httpd:2.4 - -COPY src/Servers/ApacheServer/httpd-probe.conf /usr/local/apache2/conf/httpd.conf -RUN echo "OK" > /usr/local/apache2/htdocs/index.html -COPY src/Servers/ApacheServer/echo.cgi /usr/local/apache2/cgi-bin/echo.cgi -COPY src/Servers/ApacheServer/cookie.cgi /usr/local/apache2/cgi-bin/cookie.cgi -RUN chmod +x /usr/local/apache2/cgi-bin/echo.cgi /usr/local/apache2/cgi-bin/cookie.cgi -``` - -## Source - -**`httpd-probe.conf`** - -```apache -ServerRoot "/usr/local/apache2" -Listen 8080 - -LoadModule mpm_event_module modules/mod_mpm_event.so -LoadModule dir_module modules/mod_dir.so -LoadModule unixd_module modules/mod_unixd.so -LoadModule authz_core_module modules/mod_authz_core.so -LoadModule cgi_module modules/mod_cgi.so -LoadModule alias_module modules/mod_alias.so - -ErrorLog /proc/self/fd/2 -LogLevel warn - -DocumentRoot "/usr/local/apache2/htdocs" - - - Require all granted - - -ScriptAlias /echo /usr/local/apache2/cgi-bin/echo.cgi -ScriptAlias /cookie /usr/local/apache2/cgi-bin/cookie.cgi - - - Require all granted - -``` - -**`echo.cgi`** - -```bash -#!/bin/sh -printf 'Content-Type: text/plain\r\n\r\n' -env | grep '^HTTP_' | while IFS='=' read -r key value; do - name=$(echo "$key" | sed 's/^HTTP_//;s/_/-/g') - printf '%s: %s\n' "$name" "$value" -done -if [ -n "$CONTENT_TYPE" ]; then - printf 'Content-Type: %s\n' "$CONTENT_TYPE" -fi -if [ -n "$CONTENT_LENGTH" ]; then - printf 'Content-Length: %s\n' "$CONTENT_LENGTH" -fi -``` - -**`cookie.cgi`** - -```bash -#!/bin/sh -printf 'Content-Type: text/plain\r\n\r\n' -if [ -n "$HTTP_COOKIE" ]; then - echo "$HTTP_COOKIE" | tr ';' '\n' | while read -r pair; do - trimmed=$(echo "$pair" | sed 's/^ *//') - printf '%s\n' "$trimmed" - done -fi -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Apache" +description: "Apache (C) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/ApacheServer) + +## Dockerfile + +```dockerfile +FROM httpd:2.4 + +COPY src/Servers/ApacheServer/httpd-probe.conf /usr/local/apache2/conf/httpd.conf +RUN echo "OK" > /usr/local/apache2/htdocs/index.html +COPY src/Servers/ApacheServer/echo.cgi /usr/local/apache2/cgi-bin/echo.cgi +COPY src/Servers/ApacheServer/cookie.cgi /usr/local/apache2/cgi-bin/cookie.cgi +RUN chmod +x /usr/local/apache2/cgi-bin/echo.cgi /usr/local/apache2/cgi-bin/cookie.cgi +``` + +## Source + +**`httpd-probe.conf`** + +```apache +ServerRoot "/usr/local/apache2" +Listen 8080 + +LoadModule mpm_event_module modules/mod_mpm_event.so +LoadModule dir_module modules/mod_dir.so +LoadModule unixd_module modules/mod_unixd.so +LoadModule authz_core_module modules/mod_authz_core.so +LoadModule cgi_module modules/mod_cgi.so +LoadModule alias_module modules/mod_alias.so + +ErrorLog /proc/self/fd/2 +LogLevel warn + +DocumentRoot "/usr/local/apache2/htdocs" + + + Require all granted + + +ScriptAlias /echo /usr/local/apache2/cgi-bin/echo.cgi +ScriptAlias /cookie /usr/local/apache2/cgi-bin/cookie.cgi + + + Require all granted + +``` + +**`echo.cgi`** + +```bash +#!/bin/sh +printf 'Content-Type: text/plain\r\n\r\n' +env | grep '^HTTP_' | while IFS='=' read -r key value; do + name=$(echo "$key" | sed 's/^HTTP_//;s/_/-/g') + printf '%s: %s\n' "$name" "$value" +done +if [ -n "$CONTENT_TYPE" ]; then + printf 'Content-Type: %s\n' "$CONTENT_TYPE" +fi +if [ -n "$CONTENT_LENGTH" ]; then + printf 'Content-Length: %s\n' "$CONTENT_LENGTH" +fi +``` + +**`cookie.cgi`** + +```bash +#!/bin/sh +printf 'Content-Type: text/plain\r\n\r\n' +if [ -n "$HTTP_COOKIE" ]; then + echo "$HTTP_COOKIE" | tr ';' '\n' | while read -r pair; do + trimmed=$(echo "$pair" | sed 's/^ *//') + printf '%s\n' "$trimmed" + done +fi +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/aspnet-minimal.md b/docs/content/servers/aspnet-minimal.md index 201ac5e..7932cf5 100644 --- a/docs/content/servers/aspnet-minimal.md +++ b/docs/content/servers/aspnet-minimal.md @@ -1,105 +1,106 @@ ---- -title: "ASP.NET Minimal" -toc: true -breadcrumbs: false ---- - -**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/AspNetMinimal) - -## Dockerfile - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY Directory.Build.props . -COPY src/Servers/AspNetMinimal/ src/Servers/AspNetMinimal/ -RUN dotnet restore src/Servers/AspNetMinimal/AspNetMinimal.csproj -RUN dotnet publish src/Servers/AspNetMinimal/AspNetMinimal.csproj -c Release -o /app --no-restore - -FROM mcr.microsoft.com/dotnet/aspnet:10.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "AspNetMinimal.dll"] -``` - -## Source - -```csharp -var builder = WebApplication.CreateBuilder(args); - -builder.WebHost.UseUrls("http://+:8080"); - -var app = builder.Build(); - -app.MapGet("/", () => "OK"); - -app.MapMethods("/", ["HEAD"], () => Results.Ok()); - -app.MapMethods("/", ["OPTIONS"], (HttpContext ctx) => -{ - ctx.Response.Headers["Allow"] = "GET, HEAD, POST, OPTIONS"; - return Results.Ok(); -}); - -app.MapPost("/", async (HttpContext ctx) => -{ - using var reader = new StreamReader(ctx.Request.Body); - var body = await reader.ReadToEndAsync(); - return Results.Text(body); -}); - -app.Map("/echo", (HttpContext ctx) => -{ - var sb = new System.Text.StringBuilder(); - foreach (var h in ctx.Request.Headers) - foreach (var v in h.Value) - sb.AppendLine($"{h.Key}: {v}"); - return Results.Text(sb.ToString()); -}); - -app.Map("/cookie", (HttpContext ctx) => -{ - var sb = new System.Text.StringBuilder(); - foreach (var cookie in ctx.Request.Cookies) - sb.AppendLine($"{cookie.Key}={cookie.Value}"); - return Results.Text(sb.ToString()); -}); - -app.Run(); -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "ASP.NET Minimal" +description: "ASP.NET Minimal (C#) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/AspNetMinimal) + +## Dockerfile + +```dockerfile +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Directory.Build.props . +COPY src/Servers/AspNetMinimal/ src/Servers/AspNetMinimal/ +RUN dotnet restore src/Servers/AspNetMinimal/AspNetMinimal.csproj +RUN dotnet publish src/Servers/AspNetMinimal/AspNetMinimal.csproj -c Release -o /app --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app . +ENTRYPOINT ["dotnet", "AspNetMinimal.dll"] +``` + +## Source + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.WebHost.UseUrls("http://+:8080"); + +var app = builder.Build(); + +app.MapGet("/", () => "OK"); + +app.MapMethods("/", ["HEAD"], () => Results.Ok()); + +app.MapMethods("/", ["OPTIONS"], (HttpContext ctx) => +{ + ctx.Response.Headers["Allow"] = "GET, HEAD, POST, OPTIONS"; + return Results.Ok(); +}); + +app.MapPost("/", async (HttpContext ctx) => +{ + using var reader = new StreamReader(ctx.Request.Body); + var body = await reader.ReadToEndAsync(); + return Results.Text(body); +}); + +app.Map("/echo", (HttpContext ctx) => +{ + var sb = new System.Text.StringBuilder(); + foreach (var h in ctx.Request.Headers) + foreach (var v in h.Value) + sb.AppendLine($"{h.Key}: {v}"); + return Results.Text(sb.ToString()); +}); + +app.Map("/cookie", (HttpContext ctx) => +{ + var sb = new System.Text.StringBuilder(); + foreach (var cookie in ctx.Request.Cookies) + sb.AppendLine($"{cookie.Key}={cookie.Value}"); + return Results.Text(sb.ToString()); +}); + +app.Run(); +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/bun.md b/docs/content/servers/bun.md index 460f4b5..da35105 100644 --- a/docs/content/servers/bun.md +++ b/docs/content/servers/bun.md @@ -1,90 +1,91 @@ ---- -title: "Bun" -toc: true -breadcrumbs: false ---- - -**Language:** TypeScript · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/BunServer) - -## Dockerfile - -```dockerfile -FROM oven/bun:1-slim -WORKDIR /app -COPY src/Servers/BunServer/server.ts . -ENTRYPOINT ["bun", "run", "server.ts", "8080"] -``` - -## Source - -```typescript -const port = parseInt(Bun.argv[2] || "8080", 10); - -Bun.serve({ - port, - hostname: "0.0.0.0", - async fetch(req) { - const url = new URL(req.url); - if (url.pathname === "/echo") { - let body = ""; - for (const [name, value] of req.headers) { - body += name + ": " + value + "\n"; - } - return new Response(body, { headers: { "Content-Type": "text/plain" } }); - } - if (url.pathname === "/cookie") { - let body = ""; - const raw = req.headers.get("cookie") || ""; - for (const pair of raw.split(";")) { - const trimmed = pair.trimStart(); - const eq = trimmed.indexOf("="); - if (eq > 0) body += trimmed.substring(0, eq) + "=" + trimmed.substring(eq + 1) + "\n"; - } - return new Response(body, { headers: { "Content-Type": "text/plain" } }); - } - if (req.method === "POST") { - const body = await req.text(); - return new Response(body); - } - return new Response("OK"); - }, -}); - -console.log(`Bun listening on 127.0.0.1:${port}`); -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Bun" +description: "Bun (TypeScript) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** TypeScript · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/BunServer) + +## Dockerfile + +```dockerfile +FROM oven/bun:1-slim +WORKDIR /app +COPY src/Servers/BunServer/server.ts . +ENTRYPOINT ["bun", "run", "server.ts", "8080"] +``` + +## Source + +```typescript +const port = parseInt(Bun.argv[2] || "8080", 10); + +Bun.serve({ + port, + hostname: "0.0.0.0", + async fetch(req) { + const url = new URL(req.url); + if (url.pathname === "/echo") { + let body = ""; + for (const [name, value] of req.headers) { + body += name + ": " + value + "\n"; + } + return new Response(body, { headers: { "Content-Type": "text/plain" } }); + } + if (url.pathname === "/cookie") { + let body = ""; + const raw = req.headers.get("cookie") || ""; + for (const pair of raw.split(";")) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf("="); + if (eq > 0) body += trimmed.substring(0, eq) + "=" + trimmed.substring(eq + 1) + "\n"; + } + return new Response(body, { headers: { "Content-Type": "text/plain" } }); + } + if (req.method === "POST") { + const body = await req.text(); + return new Response(body); + } + return new Response("OK"); + }, +}); + +console.log(`Bun listening on 127.0.0.1:${port}`); +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/caddy.md b/docs/content/servers/caddy.md index 58d152e..5262187 100644 --- a/docs/content/servers/caddy.md +++ b/docs/content/servers/caddy.md @@ -1,106 +1,107 @@ ---- -title: "Caddy" -toc: true -breadcrumbs: false ---- - -**Language:** Go · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/CaddyServer) - -## Dockerfile - -```dockerfile -FROM caddy:2 -COPY src/Servers/CaddyServer/Caddyfile /etc/caddy/Caddyfile -COPY src/Servers/CaddyServer/echo.html /srv/echo.html -COPY src/Servers/CaddyServer/cookie.html /srv/cookie.html -``` - -## Source - -**`Caddyfile`** - -```text -:8080 { - request_body { - max_size 1MB - } - - @post_root { - method POST - path / - } - handle @post_root { - respond "{http.request.body}" 200 - } - - handle /echo { - root * /srv - templates { - mime text/plain - } - rewrite * /echo.html - file_server - } - - handle /cookie { - root * /srv - templates { - mime text/plain - } - rewrite * /cookie.html - file_server - } - - respond "OK" 200 -} -``` - -**`echo.html`** - -```html -{{range $key, $vals := .Req.Header}}{{range $vals}}{{$key}}: {{.}} -{{end}}{{end}} -``` - -**`cookie.html`** - -```html -{{range .Req.Header.Cookie}}{{range splitList ";" .}}{{trim .}} -{{end}}{{end}} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Caddy" +description: "Caddy (Go) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Go · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/CaddyServer) + +## Dockerfile + +```dockerfile +FROM caddy:2 +COPY src/Servers/CaddyServer/Caddyfile /etc/caddy/Caddyfile +COPY src/Servers/CaddyServer/echo.html /srv/echo.html +COPY src/Servers/CaddyServer/cookie.html /srv/cookie.html +``` + +## Source + +**`Caddyfile`** + +```text +:8080 { + request_body { + max_size 1MB + } + + @post_root { + method POST + path / + } + handle @post_root { + respond "{http.request.body}" 200 + } + + handle /echo { + root * /srv + templates { + mime text/plain + } + rewrite * /echo.html + file_server + } + + handle /cookie { + root * /srv + templates { + mime text/plain + } + rewrite * /cookie.html + file_server + } + + respond "OK" 200 +} +``` + +**`echo.html`** + +```html +{{range $key, $vals := .Req.Header}}{{range $vals}}{{$key}}: {{.}} +{{end}}{{end}} +``` + +**`cookie.html`** + +```html +{{range .Req.Header.Cookie}}{{range splitList ";" .}}{{trim .}} +{{end}}{{end}} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/deno.md b/docs/content/servers/deno.md index 1ba5e2a..9a24856 100644 --- a/docs/content/servers/deno.md +++ b/docs/content/servers/deno.md @@ -1,84 +1,85 @@ ---- -title: "Deno" -toc: true -breadcrumbs: false ---- - -**Language:** TypeScript · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/DenoServer) - -## Dockerfile - -```dockerfile -FROM denoland/deno:latest -COPY src/Servers/DenoServer/server.ts /app/server.ts -WORKDIR /app -RUN deno cache server.ts -EXPOSE 8080 -CMD ["deno", "run", "--allow-net", "server.ts"] -``` - -## Source - -```typescript -Deno.serve({ port: 8080, hostname: "0.0.0.0" }, async (req) => { - const url = new URL(req.url); - if (url.pathname === "/echo") { - let body = ""; - for (const [name, value] of req.headers) { - body += name + ": " + value + "\n"; - } - return new Response(body, { headers: { "content-type": "text/plain" } }); - } - if (url.pathname === "/cookie") { - let body = ""; - const raw = req.headers.get("cookie") || ""; - for (const pair of raw.split(";")) { - const trimmed = pair.trimStart(); - const eq = trimmed.indexOf("="); - if (eq > 0) body += trimmed.substring(0, eq) + "=" + trimmed.substring(eq + 1) + "\n"; - } - return new Response(body, { headers: { "content-type": "text/plain" } }); - } - if (req.method === "POST") { - const body = await req.text(); - return new Response(body, { headers: { "content-type": "text/plain" } }); - } - return new Response("OK", { headers: { "content-type": "text/plain" } }); -}); -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Deno" +description: "Deno (TypeScript) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** TypeScript · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/DenoServer) + +## Dockerfile + +```dockerfile +FROM denoland/deno:latest +COPY src/Servers/DenoServer/server.ts /app/server.ts +WORKDIR /app +RUN deno cache server.ts +EXPOSE 8080 +CMD ["deno", "run", "--allow-net", "server.ts"] +``` + +## Source + +```typescript +Deno.serve({ port: 8080, hostname: "0.0.0.0" }, async (req) => { + const url = new URL(req.url); + if (url.pathname === "/echo") { + let body = ""; + for (const [name, value] of req.headers) { + body += name + ": " + value + "\n"; + } + return new Response(body, { headers: { "content-type": "text/plain" } }); + } + if (url.pathname === "/cookie") { + let body = ""; + const raw = req.headers.get("cookie") || ""; + for (const pair of raw.split(";")) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf("="); + if (eq > 0) body += trimmed.substring(0, eq) + "=" + trimmed.substring(eq + 1) + "\n"; + } + return new Response(body, { headers: { "content-type": "text/plain" } }); + } + if (req.method === "POST") { + const body = await req.text(); + return new Response(body, { headers: { "content-type": "text/plain" } }); + } + return new Response("OK", { headers: { "content-type": "text/plain" } }); +}); +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/effinitive.md b/docs/content/servers/effinitive.md index 8d11e40..aea174c 100644 --- a/docs/content/servers/effinitive.md +++ b/docs/content/servers/effinitive.md @@ -1,178 +1,179 @@ ---- -title: "Effinitive" -toc: false -breadcrumbs: false ---- - -**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/Http11Probe/tree/main/src/Servers/EffinitiveServer) - -## Dockerfile - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY Directory.Build.props . -COPY src/Servers/EffinitiveServer/ src/Servers/EffinitiveServer/ -RUN dotnet restore src/Servers/EffinitiveServer/EffinitiveServer.csproj -RUN dotnet publish src/Servers/EffinitiveServer/EffinitiveServer.csproj -c Release -o /app --no-restore - -FROM mcr.microsoft.com/dotnet/runtime:10.0 -WORKDIR /app -COPY --from=build /app . -USER $APP_UID -ENTRYPOINT ["dotnet", "EffinitiveServer.dll", "8080"] -``` - -## Source — `Program.cs` - -```csharp -using System.Text; -using EffinitiveFramework.Core; -using EffinitiveFramework.Core.Http; - -var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; - -var app = EffinitiveApp - .Create() - .UsePort(port) - .MapEndpoints() - .Build(); - -Console.WriteLine($"Effinitive listening on http://localhost:{port}"); -await app.RunAsync(); - -namespace EffinitiveServer.Endpoints -{ - // ── GET / ────────────────────────────────────────────────────── - - sealed class GetRoot : NoRequestEndpointBase - { - protected override string Method => "GET"; - protected override string Route => "/"; - protected override string ContentType => Helpers.TextPlain; - - public override ValueTask HandleAsync(CancellationToken ct = default) - => ValueTask.FromResult("OK"); - } - - // ── POST / ───────────────────────────────────────────────────── - - sealed class PostRoot : NoRequestEndpointBase - { - protected override string Method => "POST"; - protected override string Route => "/"; - protected override string ContentType => Helpers.TextPlain; - - public override ValueTask HandleAsync(CancellationToken ct = default) - { - var body = HttpContext?.Body; - return ValueTask.FromResult(body is { Length: > 0 } ? Encoding.UTF8.GetString(body.Value.Span) : ""); - } - } - - // ── GET/POST /echo ──────────────────────────────────────────── - - sealed class EchoGet : NoRequestEndpointBase - { - protected override string Method => "GET"; - protected override string Route => "/echo"; - protected override string ContentType => Helpers.TextPlain; - - public override ValueTask HandleAsync(CancellationToken ct = default) - => ValueTask.FromResult(Helpers.EchoHeaders(HttpContext)); - } - - sealed class EchoPost : NoRequestEndpointBase - { - protected override string Method => "POST"; - protected override string Route => "/echo"; - protected override string ContentType => Helpers.TextPlain; - - public override ValueTask HandleAsync(CancellationToken ct = default) - => ValueTask.FromResult(Helpers.EchoHeaders(HttpContext)); - } - - // ── GET/POST /cookie ────────────────────────────────────────── - - sealed class CookieGet : NoRequestEndpointBase - { - protected override string Method => "GET"; - protected override string Route => "/cookie"; - protected override string ContentType => Helpers.TextPlain; - - public override ValueTask HandleAsync(CancellationToken ct = default) - => ValueTask.FromResult(Helpers.ParseCookies(HttpContext)); - } - - sealed class CookiePost : NoRequestEndpointBase - { - protected override string Method => "POST"; - protected override string Route => "/cookie"; - protected override string ContentType => Helpers.TextPlain; - - public override ValueTask HandleAsync(CancellationToken ct = default) - => ValueTask.FromResult(Helpers.ParseCookies(HttpContext)); - } - - // ── Shared helpers ──────────────────────────────────────────── - - static class Helpers - { - public const string TextPlain = "text/plain"; - - public static string EchoHeaders(HttpRequest? ctx) - { - if (ctx?.Headers is null) return ""; - var sb = new StringBuilder(); - foreach (var h in ctx.Headers) - sb.Append(h.Key).Append(": ").Append(h.Value).Append("\r\n"); - return sb.ToString(); - } - - public static string ParseCookies(HttpRequest? ctx) - { - if (ctx is null) return ""; - var sb = new StringBuilder(); - foreach (var c in ctx.Cookies) - sb.Append(c.Key).Append('=').Append(c.Value).Append("\r\n"); - return sb.ToString(); - } - } -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Effinitive" +description: "Effinitive (C#) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: false +breadcrumbs: false +--- + +**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/Http11Probe/tree/main/src/Servers/EffinitiveServer) + +## Dockerfile + +```dockerfile +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Directory.Build.props . +COPY src/Servers/EffinitiveServer/ src/Servers/EffinitiveServer/ +RUN dotnet restore src/Servers/EffinitiveServer/EffinitiveServer.csproj +RUN dotnet publish src/Servers/EffinitiveServer/EffinitiveServer.csproj -c Release -o /app --no-restore + +FROM mcr.microsoft.com/dotnet/runtime:10.0 +WORKDIR /app +COPY --from=build /app . +USER $APP_UID +ENTRYPOINT ["dotnet", "EffinitiveServer.dll", "8080"] +``` + +## Source — `Program.cs` + +```csharp +using System.Text; +using EffinitiveFramework.Core; +using EffinitiveFramework.Core.Http; + +var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; + +var app = EffinitiveApp + .Create() + .UsePort(port) + .MapEndpoints() + .Build(); + +Console.WriteLine($"Effinitive listening on http://localhost:{port}"); +await app.RunAsync(); + +namespace EffinitiveServer.Endpoints +{ + // ── GET / ────────────────────────────────────────────────────── + + sealed class GetRoot : NoRequestEndpointBase + { + protected override string Method => "GET"; + protected override string Route => "/"; + protected override string ContentType => Helpers.TextPlain; + + public override ValueTask HandleAsync(CancellationToken ct = default) + => ValueTask.FromResult("OK"); + } + + // ── POST / ───────────────────────────────────────────────────── + + sealed class PostRoot : NoRequestEndpointBase + { + protected override string Method => "POST"; + protected override string Route => "/"; + protected override string ContentType => Helpers.TextPlain; + + public override ValueTask HandleAsync(CancellationToken ct = default) + { + var body = HttpContext?.Body; + return ValueTask.FromResult(body is { Length: > 0 } ? Encoding.UTF8.GetString(body.Value.Span) : ""); + } + } + + // ── GET/POST /echo ──────────────────────────────────────────── + + sealed class EchoGet : NoRequestEndpointBase + { + protected override string Method => "GET"; + protected override string Route => "/echo"; + protected override string ContentType => Helpers.TextPlain; + + public override ValueTask HandleAsync(CancellationToken ct = default) + => ValueTask.FromResult(Helpers.EchoHeaders(HttpContext)); + } + + sealed class EchoPost : NoRequestEndpointBase + { + protected override string Method => "POST"; + protected override string Route => "/echo"; + protected override string ContentType => Helpers.TextPlain; + + public override ValueTask HandleAsync(CancellationToken ct = default) + => ValueTask.FromResult(Helpers.EchoHeaders(HttpContext)); + } + + // ── GET/POST /cookie ────────────────────────────────────────── + + sealed class CookieGet : NoRequestEndpointBase + { + protected override string Method => "GET"; + protected override string Route => "/cookie"; + protected override string ContentType => Helpers.TextPlain; + + public override ValueTask HandleAsync(CancellationToken ct = default) + => ValueTask.FromResult(Helpers.ParseCookies(HttpContext)); + } + + sealed class CookiePost : NoRequestEndpointBase + { + protected override string Method => "POST"; + protected override string Route => "/cookie"; + protected override string ContentType => Helpers.TextPlain; + + public override ValueTask HandleAsync(CancellationToken ct = default) + => ValueTask.FromResult(Helpers.ParseCookies(HttpContext)); + } + + // ── Shared helpers ──────────────────────────────────────────── + + static class Helpers + { + public const string TextPlain = "text/plain"; + + public static string EchoHeaders(HttpRequest? ctx) + { + if (ctx?.Headers is null) return ""; + var sb = new StringBuilder(); + foreach (var h in ctx.Headers) + sb.Append(h.Key).Append(": ").Append(h.Value).Append("\r\n"); + return sb.ToString(); + } + + public static string ParseCookies(HttpRequest? ctx) + { + if (ctx is null) return ""; + var sb = new StringBuilder(); + foreach (var c in ctx.Cookies) + sb.Append(c.Key).Append('=').Append(c.Value).Append("\r\n"); + return sb.ToString(); + } + } +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/embedio.md b/docs/content/servers/embedio.md index 20d1f14..13676ef 100644 --- a/docs/content/servers/embedio.md +++ b/docs/content/servers/embedio.md @@ -1,105 +1,106 @@ ---- -title: "EmbedIO" -toc: true -breadcrumbs: false ---- - -**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/EmbedIOServer) - -## Dockerfile - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY Directory.Build.props . -COPY src/Servers/EmbedIOServer/ src/Servers/EmbedIOServer/ -RUN dotnet restore src/Servers/EmbedIOServer/EmbedIOServer.csproj -RUN dotnet publish src/Servers/EmbedIOServer/EmbedIOServer.csproj -c Release -o /app --no-restore - -FROM mcr.microsoft.com/dotnet/runtime:10.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "EmbedIOServer.dll", "8080"] -``` - -## Source - -```csharp -using EmbedIO; -using EmbedIO.Actions; - -var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; -var url = $"http://*:{port}/"; - -using var server = new WebServer(o => o - .WithUrlPrefix(url) - .WithMode(HttpListenerMode.EmbedIO)) - .WithModule(new ActionModule("/cookie", HttpVerbs.Any, async ctx => - { - var sb = new System.Text.StringBuilder(); - foreach (System.Net.Cookie cookie in ctx.Request.Cookies) - sb.AppendLine($"{cookie.Name}={cookie.Value}"); - await ctx.SendStringAsync(sb.ToString(), "text/plain", System.Text.Encoding.UTF8); - })) - .WithModule(new ActionModule("/echo", HttpVerbs.Any, async ctx => - { - var sb = new System.Text.StringBuilder(); - foreach (var key in ctx.Request.Headers.AllKeys) - foreach (var val in ctx.Request.Headers.GetValues(key)!) - sb.AppendLine($"{key}: {val}"); - await ctx.SendStringAsync(sb.ToString(), "text/plain", System.Text.Encoding.UTF8); - })) - .WithModule(new ActionModule("/", HttpVerbs.Any, async ctx => - { - ctx.Response.ContentType = "text/plain"; - if (ctx.Request.HttpVerb == HttpVerbs.Post) - { - using var reader = new System.IO.StreamReader(ctx.Request.InputStream); - var body = await reader.ReadToEndAsync(); - await ctx.SendStringAsync(body, "text/plain", System.Text.Encoding.UTF8); - } - else - { - await ctx.SendStringAsync("OK", "text/plain", System.Text.Encoding.UTF8); - } - })); - -Console.WriteLine($"EmbedIO listening on http://localhost:{port}"); -await server.RunAsync(); -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "EmbedIO" +description: "EmbedIO (C#) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/EmbedIOServer) + +## Dockerfile + +```dockerfile +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Directory.Build.props . +COPY src/Servers/EmbedIOServer/ src/Servers/EmbedIOServer/ +RUN dotnet restore src/Servers/EmbedIOServer/EmbedIOServer.csproj +RUN dotnet publish src/Servers/EmbedIOServer/EmbedIOServer.csproj -c Release -o /app --no-restore + +FROM mcr.microsoft.com/dotnet/runtime:10.0 +WORKDIR /app +COPY --from=build /app . +ENTRYPOINT ["dotnet", "EmbedIOServer.dll", "8080"] +``` + +## Source + +```csharp +using EmbedIO; +using EmbedIO.Actions; + +var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; +var url = $"http://*:{port}/"; + +using var server = new WebServer(o => o + .WithUrlPrefix(url) + .WithMode(HttpListenerMode.EmbedIO)) + .WithModule(new ActionModule("/cookie", HttpVerbs.Any, async ctx => + { + var sb = new System.Text.StringBuilder(); + foreach (System.Net.Cookie cookie in ctx.Request.Cookies) + sb.AppendLine($"{cookie.Name}={cookie.Value}"); + await ctx.SendStringAsync(sb.ToString(), "text/plain", System.Text.Encoding.UTF8); + })) + .WithModule(new ActionModule("/echo", HttpVerbs.Any, async ctx => + { + var sb = new System.Text.StringBuilder(); + foreach (var key in ctx.Request.Headers.AllKeys) + foreach (var val in ctx.Request.Headers.GetValues(key)!) + sb.AppendLine($"{key}: {val}"); + await ctx.SendStringAsync(sb.ToString(), "text/plain", System.Text.Encoding.UTF8); + })) + .WithModule(new ActionModule("/", HttpVerbs.Any, async ctx => + { + ctx.Response.ContentType = "text/plain"; + if (ctx.Request.HttpVerb == HttpVerbs.Post) + { + using var reader = new System.IO.StreamReader(ctx.Request.InputStream); + var body = await reader.ReadToEndAsync(); + await ctx.SendStringAsync(body, "text/plain", System.Text.Encoding.UTF8); + } + else + { + await ctx.SendStringAsync("OK", "text/plain", System.Text.Encoding.UTF8); + } + })); + +Console.WriteLine($"EmbedIO listening on http://localhost:{port}"); +await server.RunAsync(); +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/envoy.md b/docs/content/servers/envoy.md index 8175676..2bf618a 100644 --- a/docs/content/servers/envoy.md +++ b/docs/content/servers/envoy.md @@ -1,114 +1,115 @@ ---- -title: "Envoy" -toc: true -breadcrumbs: false ---- - -**Language:** C++ · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/EnvoyServer) - -## Dockerfile - -```dockerfile -FROM envoyproxy/envoy:v1.32-latest -COPY src/Servers/EnvoyServer/envoy.yaml /etc/envoy/envoy.yaml -``` - -## Source - -```yaml -static_resources: - listeners: - - name: listener_0 - address: - socket_address: - address: 0.0.0.0 - port_value: 8080 - filter_chains: - - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: ingress_http - http_filters: - - name: envoy.filters.http.lua - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua - default_source_code: - inline_string: | - function envoy_on_request(request_handle) - local path = request_handle:headers():get(":path") - if path == "/echo" then - local body = "" - local headers = request_handle:headers() - for key, value in pairs(headers) do - if key:sub(1,1) ~= ":" then - body = body .. key .. ": " .. value .. "\n" - end - end - request_handle:respond({[":status"] = "200", ["content-type"] = "text/plain"}, body) - elseif path == "/cookie" then - local body = "" - local raw = request_handle:headers():get("cookie") - if raw then - for pair in raw:gmatch("[^;]+") do - local trimmed = pair:match("^%s*(.*)") - local eq = trimmed:find("=") - if eq and eq > 1 then - body = body .. trimmed:sub(1, eq-1) .. "=" .. trimmed:sub(eq+1) .. "\n" - end - end - end - request_handle:respond({[":status"] = "200", ["content-type"] = "text/plain"}, body) - end - end - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - route_config: - virtual_hosts: - - name: local_service - domains: ["*"] - routes: - - match: - prefix: "/" - direct_response: - status: 200 - body: - inline_string: "OK" -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Envoy" +description: "Envoy (C++) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C++ · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/EnvoyServer) + +## Dockerfile + +```dockerfile +FROM envoyproxy/envoy:v1.32-latest +COPY src/Servers/EnvoyServer/envoy.yaml /etc/envoy/envoy.yaml +``` + +## Source + +```yaml +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + address: 0.0.0.0 + port_value: 8080 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + http_filters: + - name: envoy.filters.http.lua + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + default_source_code: + inline_string: | + function envoy_on_request(request_handle) + local path = request_handle:headers():get(":path") + if path == "/echo" then + local body = "" + local headers = request_handle:headers() + for key, value in pairs(headers) do + if key:sub(1,1) ~= ":" then + body = body .. key .. ": " .. value .. "\n" + end + end + request_handle:respond({[":status"] = "200", ["content-type"] = "text/plain"}, body) + elseif path == "/cookie" then + local body = "" + local raw = request_handle:headers():get("cookie") + if raw then + for pair in raw:gmatch("[^;]+") do + local trimmed = pair:match("^%s*(.*)") + local eq = trimmed:find("=") + if eq and eq > 1 then + body = body .. trimmed:sub(1, eq-1) .. "=" .. trimmed:sub(eq+1) .. "\n" + end + end + end + request_handle:respond({[":status"] = "200", ["content-type"] = "text/plain"}, body) + end + end + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + route_config: + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "OK" +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/express.md b/docs/content/servers/express.md index 02a87bc..925b2c4 100644 --- a/docs/content/servers/express.md +++ b/docs/content/servers/express.md @@ -1,97 +1,98 @@ ---- -title: "Express" -toc: true -breadcrumbs: false ---- - -**Language:** JavaScript · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/ExpressServer) - -## Dockerfile - -```dockerfile -FROM node:22-slim -WORKDIR /app -COPY src/Servers/ExpressServer/package.json . -RUN npm install --omit=dev -COPY src/Servers/ExpressServer/server.js . -ENTRYPOINT ["node", "server.js", "8080"] -``` - -## Source - -```javascript -const express = require("express"); - -const app = express(); -const port = parseInt(process.argv[2] || "9003", 10); - -app.get("/", (_req, res) => { - res.send("OK"); -}); - -app.post("/", (req, res) => { - const chunks = []; - req.on("data", (chunk) => chunks.push(chunk)); - req.on("end", () => res.send(Buffer.concat(chunks))); -}); - -app.all('/cookie', (req, res) => { - let body = ''; - const raw = req.headers.cookie || ''; - for (const pair of raw.split(';')) { - const trimmed = pair.trimStart(); - const eq = trimmed.indexOf('='); - if (eq > 0) body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; - } - res.set('Content-Type', 'text/plain').send(body); -}); - -app.all('/echo', (req, res) => { - let body = ''; - for (const [name, value] of Object.entries(req.headers)) { - if (Array.isArray(value)) value.forEach(v => body += name + ': ' + v + '\n'); - else body += name + ': ' + value + '\n'; - } - res.set('Content-Type', 'text/plain').send(body); -}); - -app.listen(port, "127.0.0.1", () => { - console.log(`Express listening on 127.0.0.1:${port}`); -}); -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Express" +description: "Express (JavaScript) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** JavaScript · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/ExpressServer) + +## Dockerfile + +```dockerfile +FROM node:22-slim +WORKDIR /app +COPY src/Servers/ExpressServer/package.json . +RUN npm install --omit=dev +COPY src/Servers/ExpressServer/server.js . +ENTRYPOINT ["node", "server.js", "8080"] +``` + +## Source + +```javascript +const express = require("express"); + +const app = express(); +const port = parseInt(process.argv[2] || "9003", 10); + +app.get("/", (_req, res) => { + res.send("OK"); +}); + +app.post("/", (req, res) => { + const chunks = []; + req.on("data", (chunk) => chunks.push(chunk)); + req.on("end", () => res.send(Buffer.concat(chunks))); +}); + +app.all('/cookie', (req, res) => { + let body = ''; + const raw = req.headers.cookie || ''; + for (const pair of raw.split(';')) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf('='); + if (eq > 0) body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; + } + res.set('Content-Type', 'text/plain').send(body); +}); + +app.all('/echo', (req, res) => { + let body = ''; + for (const [name, value] of Object.entries(req.headers)) { + if (Array.isArray(value)) value.forEach(v => body += name + ': ' + v + '\n'); + else body += name + ': ' + value + '\n'; + } + res.set('Content-Type', 'text/plain').send(body); +}); + +app.listen(port, "127.0.0.1", () => { + console.log(`Express listening on 127.0.0.1:${port}`); +}); +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/fastendpoints.md b/docs/content/servers/fastendpoints.md index 294bbb4..cd3b7ad 100644 --- a/docs/content/servers/fastendpoints.md +++ b/docs/content/servers/fastendpoints.md @@ -1,208 +1,209 @@ ---- -title: "FastEndpoints" -toc: true -breadcrumbs: false ---- - -**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/FastEndpointsServer) - -## Dockerfile - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY Directory.Build.props . -COPY src/Servers/FastEndpointsServer/ src/Servers/FastEndpointsServer/ -RUN dotnet restore src/Servers/FastEndpointsServer/FastEndpointsServer.csproj -RUN dotnet publish src/Servers/FastEndpointsServer/FastEndpointsServer.csproj -c Release -o /app --no-restore - -FROM mcr.microsoft.com/dotnet/aspnet:10.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "FastEndpointsServer.dll"] -``` - -## Source - -**`Program.cs`** - -```csharp -using FastEndpoints; - -var builder = WebApplication.CreateBuilder(args); - -builder.WebHost.UseUrls("http://+:8080"); -builder.Services.AddFastEndpoints(o => o.Assemblies = [typeof(GetRoot).Assembly]); - -var app = builder.Build(); - -app.UseFastEndpoints(); - -app.Run(); - -// ── GET / ────────────────────────────────────────────────────── - -sealed class GetRoot : EndpointWithoutRequest -{ - public override void Configure() - { - Get("/"); - AllowAnonymous(); - } - - public override async Task HandleAsync(CancellationToken ct) - { - await HttpContext.Response.WriteAsync("OK", ct); - } -} - -// ── HEAD / ───────────────────────────────────────────────────── - -sealed class HeadRoot : EndpointWithoutRequest -{ - public override void Configure() - { - Verbs("HEAD"); - Routes("/"); - AllowAnonymous(); - } - - public override async Task HandleAsync(CancellationToken ct) - { - HttpContext.Response.StatusCode = 200; - await HttpContext.Response.WriteAsync("", ct); - } -} - -// ── POST / ───────────────────────────────────────────────────── - -sealed class PostRoot : EndpointWithoutRequest -{ - public override void Configure() - { - Post("/"); - AllowAnonymous(); - } - - public override async Task HandleAsync(CancellationToken ct) - { - using var reader = new StreamReader(HttpContext.Request.Body); - var body = await reader.ReadToEndAsync(ct); - await HttpContext.Response.WriteAsync(body, ct); - } -} - -// ── OPTIONS / ────────────────────────────────────────────────── - -sealed class OptionsRoot : EndpointWithoutRequest -{ - public override void Configure() - { - Verbs("OPTIONS"); - Routes("/"); - AllowAnonymous(); - } - - public override async Task HandleAsync(CancellationToken ct) - { - HttpContext.Response.Headers["Allow"] = "GET, HEAD, POST, OPTIONS"; - HttpContext.Response.StatusCode = 200; - await HttpContext.Response.WriteAsync("", ct); - } -} - -// ── GET/POST /cookie ────────────────────────────────────────── - -sealed class CookieEndpoint : EndpointWithoutRequest -{ - public override void Configure() - { - Verbs("GET", "POST"); - Routes("/cookie"); - AllowAnonymous(); - } - - public override async Task HandleAsync(CancellationToken ct) - { - var sb = new System.Text.StringBuilder(); - foreach (var cookie in HttpContext.Request.Cookies) - sb.AppendLine($"{cookie.Key}={cookie.Value}"); - await HttpContext.Response.WriteAsync(sb.ToString(), ct); - } -} - -// ── POST /echo ───────────────────────────────────────────────── - -sealed class PostEcho : EndpointWithoutRequest -{ - public override void Configure() - { - Post("/echo"); - AllowAnonymous(); - } - - public override async Task HandleAsync(CancellationToken ct) - { - var sb = new System.Text.StringBuilder(); - foreach (var h in HttpContext.Request.Headers) - foreach (var v in h.Value) - sb.AppendLine($"{h.Key}: {v}"); - await HttpContext.Response.WriteAsync(sb.ToString(), ct); - } -} -``` - -**`FastEndpointsServer.csproj`** - -```xml - - - - net10.0 - enable - enable - false - - - - - - - -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "FastEndpoints" +description: "FastEndpoints (C#) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/FastEndpointsServer) + +## Dockerfile + +```dockerfile +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Directory.Build.props . +COPY src/Servers/FastEndpointsServer/ src/Servers/FastEndpointsServer/ +RUN dotnet restore src/Servers/FastEndpointsServer/FastEndpointsServer.csproj +RUN dotnet publish src/Servers/FastEndpointsServer/FastEndpointsServer.csproj -c Release -o /app --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app . +ENTRYPOINT ["dotnet", "FastEndpointsServer.dll"] +``` + +## Source + +**`Program.cs`** + +```csharp +using FastEndpoints; + +var builder = WebApplication.CreateBuilder(args); + +builder.WebHost.UseUrls("http://+:8080"); +builder.Services.AddFastEndpoints(o => o.Assemblies = [typeof(GetRoot).Assembly]); + +var app = builder.Build(); + +app.UseFastEndpoints(); + +app.Run(); + +// ── GET / ────────────────────────────────────────────────────── + +sealed class GetRoot : EndpointWithoutRequest +{ + public override void Configure() + { + Get("/"); + AllowAnonymous(); + } + + public override async Task HandleAsync(CancellationToken ct) + { + await HttpContext.Response.WriteAsync("OK", ct); + } +} + +// ── HEAD / ───────────────────────────────────────────────────── + +sealed class HeadRoot : EndpointWithoutRequest +{ + public override void Configure() + { + Verbs("HEAD"); + Routes("/"); + AllowAnonymous(); + } + + public override async Task HandleAsync(CancellationToken ct) + { + HttpContext.Response.StatusCode = 200; + await HttpContext.Response.WriteAsync("", ct); + } +} + +// ── POST / ───────────────────────────────────────────────────── + +sealed class PostRoot : EndpointWithoutRequest +{ + public override void Configure() + { + Post("/"); + AllowAnonymous(); + } + + public override async Task HandleAsync(CancellationToken ct) + { + using var reader = new StreamReader(HttpContext.Request.Body); + var body = await reader.ReadToEndAsync(ct); + await HttpContext.Response.WriteAsync(body, ct); + } +} + +// ── OPTIONS / ────────────────────────────────────────────────── + +sealed class OptionsRoot : EndpointWithoutRequest +{ + public override void Configure() + { + Verbs("OPTIONS"); + Routes("/"); + AllowAnonymous(); + } + + public override async Task HandleAsync(CancellationToken ct) + { + HttpContext.Response.Headers["Allow"] = "GET, HEAD, POST, OPTIONS"; + HttpContext.Response.StatusCode = 200; + await HttpContext.Response.WriteAsync("", ct); + } +} + +// ── GET/POST /cookie ────────────────────────────────────────── + +sealed class CookieEndpoint : EndpointWithoutRequest +{ + public override void Configure() + { + Verbs("GET", "POST"); + Routes("/cookie"); + AllowAnonymous(); + } + + public override async Task HandleAsync(CancellationToken ct) + { + var sb = new System.Text.StringBuilder(); + foreach (var cookie in HttpContext.Request.Cookies) + sb.AppendLine($"{cookie.Key}={cookie.Value}"); + await HttpContext.Response.WriteAsync(sb.ToString(), ct); + } +} + +// ── POST /echo ───────────────────────────────────────────────── + +sealed class PostEcho : EndpointWithoutRequest +{ + public override void Configure() + { + Post("/echo"); + AllowAnonymous(); + } + + public override async Task HandleAsync(CancellationToken ct) + { + var sb = new System.Text.StringBuilder(); + foreach (var h in HttpContext.Request.Headers) + foreach (var v in h.Value) + sb.AppendLine($"{h.Key}: {v}"); + await HttpContext.Response.WriteAsync(sb.ToString(), ct); + } +} +``` + +**`FastEndpointsServer.csproj`** + +```xml + + + + net10.0 + enable + enable + false + + + + + + + +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/fasthttp.md b/docs/content/servers/fasthttp.md index 3954839..28a5803 100644 --- a/docs/content/servers/fasthttp.md +++ b/docs/content/servers/fasthttp.md @@ -1,105 +1,106 @@ ---- -title: "FastHTTP" -toc: true -breadcrumbs: false ---- - -**Language:** Go · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/FastHttpServer) - -## Dockerfile - -```dockerfile -FROM golang:1.23-alpine AS build -WORKDIR /src -COPY src/Servers/FastHttpServer/go.mod . -COPY src/Servers/FastHttpServer/main.go . -RUN go mod tidy && CGO_ENABLED=0 go build -o /fasthttp-server . - -FROM alpine:3.20 -COPY --from=build /fasthttp-server /usr/local/bin/ -ENTRYPOINT ["fasthttp-server", "8080"] -``` - -## Source - -```go -package main - -import ( - "os" - "strings" - - "github.com/valyala/fasthttp" -) - -func main() { - port := "8080" - if len(os.Args) > 1 { - port = os.Args[1] - } - - handler := func(ctx *fasthttp.RequestCtx) { - ctx.SetStatusCode(200) - switch string(ctx.Path()) { - case "/echo": - ctx.SetContentType("text/plain") - ctx.Request.Header.VisitAll(func(key, value []byte) { - ctx.WriteString(string(key) + ": " + string(value) + "\n") - }) - case "/cookie": - ctx.SetContentType("text/plain") - raw := string(ctx.Request.Header.Peek("Cookie")) - for _, pair := range strings.Split(raw, ";") { - pair = strings.TrimLeft(pair, " ") - if eq := strings.Index(pair, "="); eq > 0 { - ctx.WriteString(pair[:eq] + "=" + pair[eq+1:] + "\n") - } - } - default: - if string(ctx.Method()) == "POST" { - ctx.SetBody(ctx.Request.Body()) - return - } - ctx.SetBodyString("OK") - } - } - - fasthttp.ListenAndServe("0.0.0.0:"+port, handler) -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "FastHTTP" +description: "FastHTTP (Go) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Go · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/FastHttpServer) + +## Dockerfile + +```dockerfile +FROM golang:1.23-alpine AS build +WORKDIR /src +COPY src/Servers/FastHttpServer/go.mod . +COPY src/Servers/FastHttpServer/main.go . +RUN go mod tidy && CGO_ENABLED=0 go build -o /fasthttp-server . + +FROM alpine:3.20 +COPY --from=build /fasthttp-server /usr/local/bin/ +ENTRYPOINT ["fasthttp-server", "8080"] +``` + +## Source + +```go +package main + +import ( + "os" + "strings" + + "github.com/valyala/fasthttp" +) + +func main() { + port := "8080" + if len(os.Args) > 1 { + port = os.Args[1] + } + + handler := func(ctx *fasthttp.RequestCtx) { + ctx.SetStatusCode(200) + switch string(ctx.Path()) { + case "/echo": + ctx.SetContentType("text/plain") + ctx.Request.Header.VisitAll(func(key, value []byte) { + ctx.WriteString(string(key) + ": " + string(value) + "\n") + }) + case "/cookie": + ctx.SetContentType("text/plain") + raw := string(ctx.Request.Header.Peek("Cookie")) + for _, pair := range strings.Split(raw, ";") { + pair = strings.TrimLeft(pair, " ") + if eq := strings.Index(pair, "="); eq > 0 { + ctx.WriteString(pair[:eq] + "=" + pair[eq+1:] + "\n") + } + } + default: + if string(ctx.Method()) == "POST" { + ctx.SetBody(ctx.Request.Body()) + return + } + ctx.SetBodyString("OK") + } + } + + fasthttp.ListenAndServe("0.0.0.0:"+port, handler) +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/flask.md b/docs/content/servers/flask.md index 6bbaa60..91b7586 100644 --- a/docs/content/servers/flask.md +++ b/docs/content/servers/flask.md @@ -1,90 +1,91 @@ ---- -title: "Flask" -toc: true -breadcrumbs: false ---- - -**Language:** Python · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/FlaskServer) - -## Dockerfile - -```dockerfile -FROM python:3.12-slim -WORKDIR /app -RUN pip install --no-cache-dir flask -COPY src/Servers/FlaskServer/app.py . -ENTRYPOINT ["python3", "app.py", "8080"] -``` - -## Source - -```python -import sys -from flask import Flask, request -from werkzeug.routing import Rule - -app = Flask(__name__) - -@app.route('/cookie', methods=['GET','POST','PUT','DELETE','PATCH','OPTIONS','HEAD']) -def cookie_endpoint(): - lines = [] - for name, value in request.cookies.items(): - lines.append(f"{name}={value}") - return '\n'.join(lines) + '\n', 200, {'Content-Type': 'text/plain'} - -@app.route('/echo', methods=['GET','POST','PUT','DELETE','PATCH','OPTIONS','HEAD']) -def echo(): - lines = [] - for name, value in request.headers: - lines.append(f"{name}: {value}") - return '\n'.join(lines) + '\n', 200, {'Content-Type': 'text/plain'} - -app.url_map.add(Rule('/', defaults={"path": ""}, endpoint='catch_all')) -app.url_map.add(Rule('/', endpoint='catch_all')) - -@app.endpoint('catch_all') -def catch_all(path): - if request.method == 'POST': - return request.get_data(as_text=True) - return "OK" - -if __name__ == "__main__": - port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 - app.run(host="0.0.0.0", port=port) -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Flask" +description: "Flask (Python) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Python · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/FlaskServer) + +## Dockerfile + +```dockerfile +FROM python:3.12-slim +WORKDIR /app +RUN pip install --no-cache-dir flask +COPY src/Servers/FlaskServer/app.py . +ENTRYPOINT ["python3", "app.py", "8080"] +``` + +## Source + +```python +import sys +from flask import Flask, request +from werkzeug.routing import Rule + +app = Flask(__name__) + +@app.route('/cookie', methods=['GET','POST','PUT','DELETE','PATCH','OPTIONS','HEAD']) +def cookie_endpoint(): + lines = [] + for name, value in request.cookies.items(): + lines.append(f"{name}={value}") + return '\n'.join(lines) + '\n', 200, {'Content-Type': 'text/plain'} + +@app.route('/echo', methods=['GET','POST','PUT','DELETE','PATCH','OPTIONS','HEAD']) +def echo(): + lines = [] + for name, value in request.headers: + lines.append(f"{name}: {value}") + return '\n'.join(lines) + '\n', 200, {'Content-Type': 'text/plain'} + +app.url_map.add(Rule('/', defaults={"path": ""}, endpoint='catch_all')) +app.url_map.add(Rule('/', endpoint='catch_all')) + +@app.endpoint('catch_all') +def catch_all(path): + if request.method == 'POST': + return request.get_data(as_text=True) + return "OK" + +if __name__ == "__main__": + port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 + app.run(host="0.0.0.0", port=port) +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/genhttp.md b/docs/content/servers/genhttp.md index 78a924c..7857284 100644 --- a/docs/content/servers/genhttp.md +++ b/docs/content/servers/genhttp.md @@ -1,118 +1,119 @@ ---- -title: "GenHTTP" -toc: true -breadcrumbs: false ---- - -**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/GenHttpServer) - -## Dockerfile - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY Directory.Build.props . -COPY src/Servers/GenHttpServer/ src/Servers/GenHttpServer/ -RUN dotnet restore src/Servers/GenHttpServer/GenHttpServer.csproj -RUN dotnet publish src/Servers/GenHttpServer/GenHttpServer.csproj -c Release -o /app --no-restore - -FROM mcr.microsoft.com/dotnet/runtime:10.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "GenHttpServer.dll", "8080"] -``` - -## Source - -```csharp -using GenHTTP.Api.Protocol; - -using GenHTTP.Engine.Internal; - -using GenHTTP.Modules.Functional; -using GenHTTP.Modules.Practices; - -var port = (args.Length > 0 && ushort.TryParse(args[0], out var p)) ? p : (ushort)8080; - -var app = Inline.Create() - .Get("/cookie", (IRequest request) => ParseCookies(request)) - .Post("/cookie", (IRequest request) => ParseCookies(request)) - .Get("/echo", (IRequest request) => Echo(request)) - .Post("/echo", (IRequest request) => Echo(request)) - .Post((Stream body) => RequestContent(body)) - .Any(() => StringContent()); - -return await Host.Create() - .Handler(app) - .Defaults() - .Port(port) - .RunAsync(); - -static string Echo(IRequest request) -{ - var headers = new System.Text.StringBuilder(); - - foreach (var h in request.Headers) - { - headers.AppendLine($"{h.Key}: {h.Value}"); - } - - return headers.ToString(); -} - -static string ParseCookies(IRequest request) -{ - var sb = new System.Text.StringBuilder(); - if (request.Headers.TryGetValue("Cookie", out var cookieHeader)) - { - foreach (var pair in cookieHeader.Split(';')) - { - var trimmed = pair.TrimStart(); - var eqIdx = trimmed.IndexOf('='); - if (eqIdx > 0) - sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); - } - } - return sb.ToString(); -} - -static string StringContent() => "OK"; - -static Stream RequestContent(Stream body) => body; -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "GenHTTP" +description: "GenHTTP (C#) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/GenHttpServer) + +## Dockerfile + +```dockerfile +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Directory.Build.props . +COPY src/Servers/GenHttpServer/ src/Servers/GenHttpServer/ +RUN dotnet restore src/Servers/GenHttpServer/GenHttpServer.csproj +RUN dotnet publish src/Servers/GenHttpServer/GenHttpServer.csproj -c Release -o /app --no-restore + +FROM mcr.microsoft.com/dotnet/runtime:10.0 +WORKDIR /app +COPY --from=build /app . +ENTRYPOINT ["dotnet", "GenHttpServer.dll", "8080"] +``` + +## Source + +```csharp +using GenHTTP.Api.Protocol; + +using GenHTTP.Engine.Internal; + +using GenHTTP.Modules.Functional; +using GenHTTP.Modules.Practices; + +var port = (args.Length > 0 && ushort.TryParse(args[0], out var p)) ? p : (ushort)8080; + +var app = Inline.Create() + .Get("/cookie", (IRequest request) => ParseCookies(request)) + .Post("/cookie", (IRequest request) => ParseCookies(request)) + .Get("/echo", (IRequest request) => Echo(request)) + .Post("/echo", (IRequest request) => Echo(request)) + .Post((Stream body) => RequestContent(body)) + .Any(() => StringContent()); + +return await Host.Create() + .Handler(app) + .Defaults() + .Port(port) + .RunAsync(); + +static string Echo(IRequest request) +{ + var headers = new System.Text.StringBuilder(); + + foreach (var h in request.Headers) + { + headers.AppendLine($"{h.Key}: {h.Value}"); + } + + return headers.ToString(); +} + +static string ParseCookies(IRequest request) +{ + var sb = new System.Text.StringBuilder(); + if (request.Headers.TryGetValue("Cookie", out var cookieHeader)) + { + foreach (var pair in cookieHeader.Split(';')) + { + var trimmed = pair.TrimStart(); + var eqIdx = trimmed.IndexOf('='); + if (eqIdx > 0) + sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); + } + } + return sb.ToString(); +} + +static string StringContent() => "OK"; + +static Stream RequestContent(Stream body) => body; +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/gin.md b/docs/content/servers/gin.md index 3828b0a..821c34d 100644 --- a/docs/content/servers/gin.md +++ b/docs/content/servers/gin.md @@ -1,110 +1,111 @@ ---- -title: "Gin" -toc: true -breadcrumbs: false ---- - -**Language:** Go · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/GinServer) - -## Dockerfile - -```dockerfile -FROM golang:1.23-alpine AS build -WORKDIR /src -COPY src/Servers/GinServer/go.mod . -COPY src/Servers/GinServer/main.go . -RUN go mod tidy && CGO_ENABLED=0 go build -o /gin-server . - -FROM alpine:3.20 -COPY --from=build /gin-server /usr/local/bin/ -ENTRYPOINT ["gin-server", "8080"] -``` - -## Source - -```go -package main - -import ( - "io" - "os" - "strings" - - "github.com/gin-gonic/gin" -) - -func main() { - port := "8080" - if len(os.Args) > 1 { - port = os.Args[1] - } - - gin.SetMode(gin.ReleaseMode) - r := gin.New() - r.Any("/cookie", func(c *gin.Context) { - var sb strings.Builder - raw := c.GetHeader("Cookie") - for _, pair := range strings.Split(raw, ";") { - pair = strings.TrimLeft(pair, " ") - if eq := strings.Index(pair, "="); eq > 0 { - sb.WriteString(pair[:eq] + "=" + pair[eq+1:] + "\n") - } - } - c.Data(200, "text/plain", []byte(sb.String())) - }) - r.Any("/echo", func(c *gin.Context) { - var sb strings.Builder - for name, values := range c.Request.Header { - for _, v := range values { - sb.WriteString(name + ": " + v + "\n") - } - } - c.Data(200, "text/plain", []byte(sb.String())) - }) - r.NoRoute(func(c *gin.Context) { - if c.Request.Method == "POST" { - body, _ := io.ReadAll(c.Request.Body) - c.Data(200, "text/plain", body) - return - } - c.String(200, "OK") - }) - r.Run("0.0.0.0:" + port) -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Gin" +description: "Gin (Go) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Go · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/GinServer) + +## Dockerfile + +```dockerfile +FROM golang:1.23-alpine AS build +WORKDIR /src +COPY src/Servers/GinServer/go.mod . +COPY src/Servers/GinServer/main.go . +RUN go mod tidy && CGO_ENABLED=0 go build -o /gin-server . + +FROM alpine:3.20 +COPY --from=build /gin-server /usr/local/bin/ +ENTRYPOINT ["gin-server", "8080"] +``` + +## Source + +```go +package main + +import ( + "io" + "os" + "strings" + + "github.com/gin-gonic/gin" +) + +func main() { + port := "8080" + if len(os.Args) > 1 { + port = os.Args[1] + } + + gin.SetMode(gin.ReleaseMode) + r := gin.New() + r.Any("/cookie", func(c *gin.Context) { + var sb strings.Builder + raw := c.GetHeader("Cookie") + for _, pair := range strings.Split(raw, ";") { + pair = strings.TrimLeft(pair, " ") + if eq := strings.Index(pair, "="); eq > 0 { + sb.WriteString(pair[:eq] + "=" + pair[eq+1:] + "\n") + } + } + c.Data(200, "text/plain", []byte(sb.String())) + }) + r.Any("/echo", func(c *gin.Context) { + var sb strings.Builder + for name, values := range c.Request.Header { + for _, v := range values { + sb.WriteString(name + ": " + v + "\n") + } + } + c.Data(200, "text/plain", []byte(sb.String())) + }) + r.NoRoute(func(c *gin.Context) { + if c.Request.Method == "POST" { + body, _ := io.ReadAll(c.Request.Body) + c.Data(200, "text/plain", body) + return + } + c.String(200, "OK") + }) + r.Run("0.0.0.0:" + port) +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/glyph.md b/docs/content/servers/glyph.md index 3ac6bc8..f40d9b8 100644 --- a/docs/content/servers/glyph.md +++ b/docs/content/servers/glyph.md @@ -1,388 +1,389 @@ ---- -title: "Glyph11" -toc: true -breadcrumbs: false ---- - -**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/GlyphServer) - -## Dockerfile - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY Directory.Build.props . -COPY src/Servers/GlyphServer/ src/Servers/GlyphServer/ -RUN dotnet restore src/Servers/GlyphServer/GlyphServer.csproj -RUN dotnet publish src/Servers/GlyphServer/GlyphServer.csproj -c Release -o /app --no-restore - -FROM mcr.microsoft.com/dotnet/runtime:10.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "GlyphServer.dll", "8080"] -``` - -## Source - -```csharp -using System.Buffers; -using System.IO.Pipelines; -using System.Net; -using System.Net.Sockets; -using System.Text; -using Glyph11; -using Glyph11.Parser.Hardened; -using Glyph11.Protocol; -using Glyph11.Validation; - -var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; - -var listener = new TcpListener(IPAddress.Any, port); -listener.Start(); - -Console.WriteLine($"GlyphServer listening on http://localhost:{port}"); - -using var cts = new CancellationTokenSource(); -Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; - -try -{ - while (!cts.Token.IsCancellationRequested) - { - var client = await listener.AcceptTcpClientAsync(cts.Token); - _ = HandleClientAsync(client, cts.Token); - } -} -catch (OperationCanceledException) { } - -listener.Stop(); -Console.WriteLine("Server stopped."); - -static async Task HandleClientAsync(TcpClient client, CancellationToken ct) -{ - using (client) - await using (var stream = client.GetStream()) - { - var limits = ParserLimits.Default; - var reader = PipeReader.Create(stream); - using var request = new BinaryRequest(); - - try - { - while (!ct.IsCancellationRequested) - { - // ── Phase 1: parse header ────────────────────────── - // Loop until we have a complete header. Do NOT advance - // the pipe yet — request holds ReadOnlyMemory slices - // into the pipe buffer. - ReadOnlySequence headerBuffer; - int headerByteCount; - while (true) - { - request.Clear(); - var result = await reader.ReadAsync(ct); - var buffer = result.Buffer; - - if (result.IsCompleted && buffer.IsEmpty) - { - await reader.CompleteAsync(); - return; - } - - var sequence = buffer; - try - { - // TODO FOR SINGLE SEQUENCE THERE ARE NO ALLOCATIONS, FOR MULTI SEGMENT THERE ARE, THAT INTERFERES THE BEHAVIOR - // TODO MEANING WE CANT ADVANCE FOR SINGLE SEGMENT CASE - - if (HardenedParser.TryExtractFullHeader(ref sequence, request, in limits, out var bytesRead)) - { - headerByteCount = bytesRead + 1; - headerBuffer = buffer; - break; - } - - if (buffer.Length > limits.MaxTotalHeaderBytes) - { - reader.AdvanceTo(buffer.End); - await stream.WriteAsync(MakeErrorResponse(431, "Request Header Fields Too Large"), ct); - await reader.CompleteAsync(); - return; - } - - reader.AdvanceTo(buffer.Start, buffer.End); - - if (result.IsCompleted) - { - await reader.CompleteAsync(); - return; - } - } - catch (HttpParseException ex) - { - var code = ex.StatusCode; - var reason = code switch - { - 431 => "Request Header Fields Too Large", - _ => "Bad Request" - }; - reader.AdvanceTo(buffer.End); - await stream.WriteAsync(MakeErrorResponse(code, reason), ct); - await reader.CompleteAsync(); - return; - } - } - - // ── Phase 2: semantic validation ─────────────────── - // request slices still point into the live pipe buffer. - if (RequestSemantics.HasTransferEncodingWithContentLength(request) || - RequestSemantics.HasConflictingContentLength(request) || - RequestSemantics.HasConflictingCommaSeparatedContentLength(request) || - RequestSemantics.HasInvalidContentLengthFormat(request) || - RequestSemantics.HasContentLengthWithLeadingZeros(request) || - RequestSemantics.HasInvalidHostHeaderCount(request) || - RequestSemantics.HasInvalidHostFormat(request) || - RequestSemantics.HasInvalidTransferEncoding(request) || - RequestSemantics.HasAsteriskFormWithoutOptions(request) || - RequestSemantics.HasInvalidConnectRequest(request) || - RequestSemantics.HasDotSegments(request) || - RequestSemantics.HasFragmentInRequestTarget(request) || - RequestSemantics.HasBackslashInPath(request) || - RequestSemantics.HasDoubleEncoding(request) || - RequestSemantics.HasEncodedNullByte(request) || - RequestSemantics.HasOverlongUtf8(request)) - { - reader.AdvanceTo(headerBuffer.End); - await stream.WriteAsync(MakeErrorResponse(400, "Bad Request"), ct); - await reader.CompleteAsync(); - return; - } - - // ── Phase 3: extract values & detect framing ─────── - // Copy what we need out of the pipe buffer, then release it. - var method = Encoding.ASCII.GetString(request.Method.Span); - var path = Encoding.ASCII.GetString(request.Path.Span); - var framing = HardenedParser.DetectBodyFraming(request); - - // Extract headers while slices are still valid (before advancing). - var headerPairs = new List>(); - for (int i = 0; i < request.Headers.Count; i++) - { - var kv = request.Headers[i]; - headerPairs.Add(new KeyValuePair( - Encoding.ASCII.GetString(kv.Key.Span), - Encoding.ASCII.GetString(kv.Value.Span))); - } - - // Now safe to advance past the header bytes. - reader.AdvanceTo(headerBuffer.GetPosition(headerByteCount)); - - // ── Phase 4: consume body ────────────────────────── - var bodyBytes = new MemoryStream(); - const int maxCapture = 4096; - - switch (framing.Framing) - { - case BodyFraming.ContentLength: - { - long remaining = framing.ContentLength; - while (remaining > 0) - { - var result = await reader.ReadAsync(ct); - var buffer = result.Buffer; - long available = Math.Min(buffer.Length, remaining); - - if (bodyBytes.Length < maxCapture) - { - var toCapture = (int)Math.Min(available, maxCapture - bodyBytes.Length); - foreach (var seg in buffer.Slice(0, toCapture)) - bodyBytes.Write(seg.Span); - } - - remaining -= available; - reader.AdvanceTo(buffer.GetPosition(available)); - - if (result.IsCompleted && remaining > 0) - { - await reader.CompleteAsync(); - return; - } - } - break; - } - - case BodyFraming.Chunked: - { - var chunked = new ChunkedBodyStream(); - while (true) - { - var result = await reader.ReadAsync(ct); - var buffer = result.Buffer; - - ReadOnlySpan span; - byte[]? linearized = null; - if (buffer.IsSingleSegment) - { - span = buffer.FirstSpan; - } - else - { - linearized = new byte[buffer.Length]; - buffer.CopyTo(linearized); - span = linearized; - } - - bool done = false; - int totalConsumed = 0; - while (true) - { - var localSpan = span[totalConsumed..]; - var cr = chunked.TryReadChunk(localSpan, out var consumed, out var dataOffset, out var dataLength); - totalConsumed += consumed; - - if (cr == ChunkResult.Chunk && dataLength > 0 && bodyBytes.Length < maxCapture) - { - var toCapture = Math.Min(dataLength, maxCapture - (int)bodyBytes.Length); - bodyBytes.Write(localSpan.Slice(dataOffset, toCapture)); - } - - if (cr == ChunkResult.Completed) - { - done = true; - break; - } - if (cr == ChunkResult.NeedMoreData) - break; - // ChunkResult.Chunk — loop to consume next chunk - } - - reader.AdvanceTo(buffer.GetPosition(totalConsumed)); - - if (done) - break; - - if (result.IsCompleted) - { - await reader.CompleteAsync(); - return; - } - } - break; - } - - case BodyFraming.None: - default: - break; - } - - // ── Phase 5: send response ───────────────────────── - var capturedBody = bodyBytes.Length > 0 ? Encoding.ASCII.GetString(bodyBytes.ToArray()) : null; - var responseBytes = BuildResponse(method, path, capturedBody, headerPairs); - await stream.WriteAsync(responseBytes, ct); - } - } - catch (OperationCanceledException) { } - catch (IOException) { } - catch (HttpParseException ex) - { - var code = ex.StatusCode; - var reason = code switch - { - 431 => "Request Header Fields Too Large", - _ => "Bad Request" - }; - try { await stream.WriteAsync(MakeErrorResponse(code, reason), ct); } catch { } - } - finally - { - await reader.CompleteAsync(); - } - } -} - -static byte[] BuildResponse(string method, string path, string? echoBody, List> headers) -{ - if (path == "/echo") - { - var sb = new StringBuilder(); - foreach (var h in headers) - sb.AppendLine($"{h.Key}: {h.Value}"); - return MakeResponse(200, "OK", sb.ToString()); - } - if (path == "/cookie") - { - var sb = new StringBuilder(); - foreach (var h in headers) - { - if (string.Equals(h.Key, "Cookie", StringComparison.OrdinalIgnoreCase)) - { - foreach (var pair in h.Value.Split(';')) - { - var trimmed = pair.TrimStart(); - var eqIdx = trimmed.IndexOf('='); - if (eqIdx > 0) - sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); - } - } - } - return MakeResponse(200, "OK", sb.ToString()); - } - var body = method == "POST" && echoBody is not null - ? echoBody - : $"Hello from GlyphServer\r\nMethod: {method}\r\nPath: {path}\r\n"; - return MakeResponse(200, "OK", body); -} - -static byte[] MakeResponse(int status, string reason, string body) -{ - var bodyBytes = Encoding.UTF8.GetBytes(body); - var header = $"HTTP/1.1 {status} {reason}\r\nContent-Type: text/plain\r\nContent-Length: {bodyBytes.Length}\r\nConnection: keep-alive\r\n\r\n"; - var headerBytes = Encoding.ASCII.GetBytes(header); - - var result = new byte[headerBytes.Length + bodyBytes.Length]; - Buffer.BlockCopy(headerBytes, 0, result, 0, headerBytes.Length); - Buffer.BlockCopy(bodyBytes, 0, result, headerBytes.Length, bodyBytes.Length); - return result; -} - -static byte[] MakeErrorResponse(int status, string reason) -{ - return MakeResponse(status, reason, $"{status} {reason}\r\n"); -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Glyph11" +description: "Glyph11 (C#) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/GlyphServer) + +## Dockerfile + +```dockerfile +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Directory.Build.props . +COPY src/Servers/GlyphServer/ src/Servers/GlyphServer/ +RUN dotnet restore src/Servers/GlyphServer/GlyphServer.csproj +RUN dotnet publish src/Servers/GlyphServer/GlyphServer.csproj -c Release -o /app --no-restore + +FROM mcr.microsoft.com/dotnet/runtime:10.0 +WORKDIR /app +COPY --from=build /app . +ENTRYPOINT ["dotnet", "GlyphServer.dll", "8080"] +``` + +## Source + +```csharp +using System.Buffers; +using System.IO.Pipelines; +using System.Net; +using System.Net.Sockets; +using System.Text; +using Glyph11; +using Glyph11.Parser.Hardened; +using Glyph11.Protocol; +using Glyph11.Validation; + +var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; + +var listener = new TcpListener(IPAddress.Any, port); +listener.Start(); + +Console.WriteLine($"GlyphServer listening on http://localhost:{port}"); + +using var cts = new CancellationTokenSource(); +Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; + +try +{ + while (!cts.Token.IsCancellationRequested) + { + var client = await listener.AcceptTcpClientAsync(cts.Token); + _ = HandleClientAsync(client, cts.Token); + } +} +catch (OperationCanceledException) { } + +listener.Stop(); +Console.WriteLine("Server stopped."); + +static async Task HandleClientAsync(TcpClient client, CancellationToken ct) +{ + using (client) + await using (var stream = client.GetStream()) + { + var limits = ParserLimits.Default; + var reader = PipeReader.Create(stream); + using var request = new BinaryRequest(); + + try + { + while (!ct.IsCancellationRequested) + { + // ── Phase 1: parse header ────────────────────────── + // Loop until we have a complete header. Do NOT advance + // the pipe yet — request holds ReadOnlyMemory slices + // into the pipe buffer. + ReadOnlySequence headerBuffer; + int headerByteCount; + while (true) + { + request.Clear(); + var result = await reader.ReadAsync(ct); + var buffer = result.Buffer; + + if (result.IsCompleted && buffer.IsEmpty) + { + await reader.CompleteAsync(); + return; + } + + var sequence = buffer; + try + { + // TODO FOR SINGLE SEQUENCE THERE ARE NO ALLOCATIONS, FOR MULTI SEGMENT THERE ARE, THAT INTERFERES THE BEHAVIOR + // TODO MEANING WE CANT ADVANCE FOR SINGLE SEGMENT CASE + + if (HardenedParser.TryExtractFullHeader(ref sequence, request, in limits, out var bytesRead)) + { + headerByteCount = bytesRead + 1; + headerBuffer = buffer; + break; + } + + if (buffer.Length > limits.MaxTotalHeaderBytes) + { + reader.AdvanceTo(buffer.End); + await stream.WriteAsync(MakeErrorResponse(431, "Request Header Fields Too Large"), ct); + await reader.CompleteAsync(); + return; + } + + reader.AdvanceTo(buffer.Start, buffer.End); + + if (result.IsCompleted) + { + await reader.CompleteAsync(); + return; + } + } + catch (HttpParseException ex) + { + var code = ex.StatusCode; + var reason = code switch + { + 431 => "Request Header Fields Too Large", + _ => "Bad Request" + }; + reader.AdvanceTo(buffer.End); + await stream.WriteAsync(MakeErrorResponse(code, reason), ct); + await reader.CompleteAsync(); + return; + } + } + + // ── Phase 2: semantic validation ─────────────────── + // request slices still point into the live pipe buffer. + if (RequestSemantics.HasTransferEncodingWithContentLength(request) || + RequestSemantics.HasConflictingContentLength(request) || + RequestSemantics.HasConflictingCommaSeparatedContentLength(request) || + RequestSemantics.HasInvalidContentLengthFormat(request) || + RequestSemantics.HasContentLengthWithLeadingZeros(request) || + RequestSemantics.HasInvalidHostHeaderCount(request) || + RequestSemantics.HasInvalidHostFormat(request) || + RequestSemantics.HasInvalidTransferEncoding(request) || + RequestSemantics.HasAsteriskFormWithoutOptions(request) || + RequestSemantics.HasInvalidConnectRequest(request) || + RequestSemantics.HasDotSegments(request) || + RequestSemantics.HasFragmentInRequestTarget(request) || + RequestSemantics.HasBackslashInPath(request) || + RequestSemantics.HasDoubleEncoding(request) || + RequestSemantics.HasEncodedNullByte(request) || + RequestSemantics.HasOverlongUtf8(request)) + { + reader.AdvanceTo(headerBuffer.End); + await stream.WriteAsync(MakeErrorResponse(400, "Bad Request"), ct); + await reader.CompleteAsync(); + return; + } + + // ── Phase 3: extract values & detect framing ─────── + // Copy what we need out of the pipe buffer, then release it. + var method = Encoding.ASCII.GetString(request.Method.Span); + var path = Encoding.ASCII.GetString(request.Path.Span); + var framing = HardenedParser.DetectBodyFraming(request); + + // Extract headers while slices are still valid (before advancing). + var headerPairs = new List>(); + for (int i = 0; i < request.Headers.Count; i++) + { + var kv = request.Headers[i]; + headerPairs.Add(new KeyValuePair( + Encoding.ASCII.GetString(kv.Key.Span), + Encoding.ASCII.GetString(kv.Value.Span))); + } + + // Now safe to advance past the header bytes. + reader.AdvanceTo(headerBuffer.GetPosition(headerByteCount)); + + // ── Phase 4: consume body ────────────────────────── + var bodyBytes = new MemoryStream(); + const int maxCapture = 4096; + + switch (framing.Framing) + { + case BodyFraming.ContentLength: + { + long remaining = framing.ContentLength; + while (remaining > 0) + { + var result = await reader.ReadAsync(ct); + var buffer = result.Buffer; + long available = Math.Min(buffer.Length, remaining); + + if (bodyBytes.Length < maxCapture) + { + var toCapture = (int)Math.Min(available, maxCapture - bodyBytes.Length); + foreach (var seg in buffer.Slice(0, toCapture)) + bodyBytes.Write(seg.Span); + } + + remaining -= available; + reader.AdvanceTo(buffer.GetPosition(available)); + + if (result.IsCompleted && remaining > 0) + { + await reader.CompleteAsync(); + return; + } + } + break; + } + + case BodyFraming.Chunked: + { + var chunked = new ChunkedBodyStream(); + while (true) + { + var result = await reader.ReadAsync(ct); + var buffer = result.Buffer; + + ReadOnlySpan span; + byte[]? linearized = null; + if (buffer.IsSingleSegment) + { + span = buffer.FirstSpan; + } + else + { + linearized = new byte[buffer.Length]; + buffer.CopyTo(linearized); + span = linearized; + } + + bool done = false; + int totalConsumed = 0; + while (true) + { + var localSpan = span[totalConsumed..]; + var cr = chunked.TryReadChunk(localSpan, out var consumed, out var dataOffset, out var dataLength); + totalConsumed += consumed; + + if (cr == ChunkResult.Chunk && dataLength > 0 && bodyBytes.Length < maxCapture) + { + var toCapture = Math.Min(dataLength, maxCapture - (int)bodyBytes.Length); + bodyBytes.Write(localSpan.Slice(dataOffset, toCapture)); + } + + if (cr == ChunkResult.Completed) + { + done = true; + break; + } + if (cr == ChunkResult.NeedMoreData) + break; + // ChunkResult.Chunk — loop to consume next chunk + } + + reader.AdvanceTo(buffer.GetPosition(totalConsumed)); + + if (done) + break; + + if (result.IsCompleted) + { + await reader.CompleteAsync(); + return; + } + } + break; + } + + case BodyFraming.None: + default: + break; + } + + // ── Phase 5: send response ───────────────────────── + var capturedBody = bodyBytes.Length > 0 ? Encoding.ASCII.GetString(bodyBytes.ToArray()) : null; + var responseBytes = BuildResponse(method, path, capturedBody, headerPairs); + await stream.WriteAsync(responseBytes, ct); + } + } + catch (OperationCanceledException) { } + catch (IOException) { } + catch (HttpParseException ex) + { + var code = ex.StatusCode; + var reason = code switch + { + 431 => "Request Header Fields Too Large", + _ => "Bad Request" + }; + try { await stream.WriteAsync(MakeErrorResponse(code, reason), ct); } catch { } + } + finally + { + await reader.CompleteAsync(); + } + } +} + +static byte[] BuildResponse(string method, string path, string? echoBody, List> headers) +{ + if (path == "/echo") + { + var sb = new StringBuilder(); + foreach (var h in headers) + sb.AppendLine($"{h.Key}: {h.Value}"); + return MakeResponse(200, "OK", sb.ToString()); + } + if (path == "/cookie") + { + var sb = new StringBuilder(); + foreach (var h in headers) + { + if (string.Equals(h.Key, "Cookie", StringComparison.OrdinalIgnoreCase)) + { + foreach (var pair in h.Value.Split(';')) + { + var trimmed = pair.TrimStart(); + var eqIdx = trimmed.IndexOf('='); + if (eqIdx > 0) + sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); + } + } + } + return MakeResponse(200, "OK", sb.ToString()); + } + var body = method == "POST" && echoBody is not null + ? echoBody + : $"Hello from GlyphServer\r\nMethod: {method}\r\nPath: {path}\r\n"; + return MakeResponse(200, "OK", body); +} + +static byte[] MakeResponse(int status, string reason, string body) +{ + var bodyBytes = Encoding.UTF8.GetBytes(body); + var header = $"HTTP/1.1 {status} {reason}\r\nContent-Type: text/plain\r\nContent-Length: {bodyBytes.Length}\r\nConnection: keep-alive\r\n\r\n"; + var headerBytes = Encoding.ASCII.GetBytes(header); + + var result = new byte[headerBytes.Length + bodyBytes.Length]; + Buffer.BlockCopy(headerBytes, 0, result, 0, headerBytes.Length); + Buffer.BlockCopy(bodyBytes, 0, result, headerBytes.Length, bodyBytes.Length); + return result; +} + +static byte[] MakeErrorResponse(int status, string reason) +{ + return MakeResponse(status, reason, $"{status} {reason}\r\n"); +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/gunicorn.md b/docs/content/servers/gunicorn.md index 0d211cd..8e7b86b 100644 --- a/docs/content/servers/gunicorn.md +++ b/docs/content/servers/gunicorn.md @@ -1,97 +1,98 @@ ---- -title: "Gunicorn" -toc: true -breadcrumbs: false ---- - -**Language:** Python · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/GunicornServer) - -## Dockerfile - -```dockerfile -FROM python:3.13-slim -RUN pip install --no-cache-dir gunicorn -COPY src/Servers/GunicornServer/app.py /app/app.py -WORKDIR /app -EXPOSE 8080 -CMD ["gunicorn", "-b", "0.0.0.0:8080", "app:app"] -``` - -## Source - -```python -def app(environ, start_response): - path = environ.get('PATH_INFO', '/') - - if path == '/cookie': - cookie_str = environ.get('HTTP_COOKIE', '') - lines = [] - for pair in cookie_str.split(';'): - pair = pair.strip() - eq = pair.find('=') - if eq > 0: - lines.append(f"{pair[:eq]}={pair[eq+1:]}") - body = ('\n'.join(lines) + '\n').encode('utf-8') if lines else b'' - start_response('200 OK', [('Content-Type', 'text/plain')]) - return [body] - - if path == '/echo': - lines = [] - for key, value in environ.items(): - if key.startswith('HTTP_'): - header_name = key[5:].replace('_', '-').title() - lines.append(f"{header_name}: {value}") - if environ.get('CONTENT_TYPE'): - lines.append(f"Content-Type: {environ['CONTENT_TYPE']}") - if environ.get('CONTENT_LENGTH'): - lines.append(f"Content-Length: {environ['CONTENT_LENGTH']}") - body = ('\n'.join(lines) + '\n').encode('utf-8') - start_response('200 OK', [('Content-Type', 'text/plain')]) - return [body] - - start_response('200 OK', [('Content-Type', 'text/plain')]) - if environ['REQUEST_METHOD'] == 'POST': - try: - length = int(environ.get('CONTENT_LENGTH', 0) or 0) - except ValueError: - length = 0 - body = environ['wsgi.input'].read(length) if length > 0 else b'' - return [body] - return [b'OK'] -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Gunicorn" +description: "Gunicorn (Python) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Python · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/GunicornServer) + +## Dockerfile + +```dockerfile +FROM python:3.13-slim +RUN pip install --no-cache-dir gunicorn +COPY src/Servers/GunicornServer/app.py /app/app.py +WORKDIR /app +EXPOSE 8080 +CMD ["gunicorn", "-b", "0.0.0.0:8080", "app:app"] +``` + +## Source + +```python +def app(environ, start_response): + path = environ.get('PATH_INFO', '/') + + if path == '/cookie': + cookie_str = environ.get('HTTP_COOKIE', '') + lines = [] + for pair in cookie_str.split(';'): + pair = pair.strip() + eq = pair.find('=') + if eq > 0: + lines.append(f"{pair[:eq]}={pair[eq+1:]}") + body = ('\n'.join(lines) + '\n').encode('utf-8') if lines else b'' + start_response('200 OK', [('Content-Type', 'text/plain')]) + return [body] + + if path == '/echo': + lines = [] + for key, value in environ.items(): + if key.startswith('HTTP_'): + header_name = key[5:].replace('_', '-').title() + lines.append(f"{header_name}: {value}") + if environ.get('CONTENT_TYPE'): + lines.append(f"Content-Type: {environ['CONTENT_TYPE']}") + if environ.get('CONTENT_LENGTH'): + lines.append(f"Content-Length: {environ['CONTENT_LENGTH']}") + body = ('\n'.join(lines) + '\n').encode('utf-8') + start_response('200 OK', [('Content-Type', 'text/plain')]) + return [body] + + start_response('200 OK', [('Content-Type', 'text/plain')]) + if environ['REQUEST_METHOD'] == 'POST': + try: + length = int(environ.get('CONTENT_LENGTH', 0) or 0) + except ValueError: + length = 0 + body = environ['wsgi.input'].read(length) if length > 0 else b'' + return [body] + return [b'OK'] +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/h2o.md b/docs/content/servers/h2o.md index 1523d80..0fade3f 100644 --- a/docs/content/servers/h2o.md +++ b/docs/content/servers/h2o.md @@ -1,103 +1,104 @@ ---- -title: "H2O" -toc: true -breadcrumbs: false ---- - -**Language:** C · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/H2OServer) - -## Dockerfile - -```dockerfile -FROM ubuntu:24.04 AS build -RUN apt-get update && apt-get install -y cmake gcc g++ pkg-config libssl-dev zlib1g-dev git ruby bison && rm -rf /var/lib/apt/lists/* -RUN git clone --recurse-submodules --depth 1 https://github.com/h2o/h2o.git /src/h2o -WORKDIR /src/h2o/build -RUN cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local -DWITH_MRUBY=ON && make -j$(nproc) && make install - -FROM ubuntu:24.04 -RUN apt-get update && apt-get install -y libssl3t64 && rm -rf /var/lib/apt/lists/* -COPY --from=build /usr/local/bin/h2o /usr/local/bin/ -COPY --from=build /usr/local/share/h2o/ /usr/local/share/h2o/ -COPY src/Servers/H2OServer/h2o.conf /etc/h2o/h2o.conf -RUN mkdir -p /var/www && echo "OK" > /var/www/index.html -ENTRYPOINT ["h2o", "-c", "/etc/h2o/h2o.conf"] -``` - -## Source - -```yaml -listen: 8080 -hosts: - default: - paths: - /: - mruby.handler: | - proc {|env| - if env["PATH_INFO"] == "/echo" - body = "" - env.each do |k, v| - if k.start_with?("HTTP_") - name = k.sub("HTTP_", "").split("_").map(&:capitalize).join("-") - body += "#{name}: #{v}\n" - end - end - body += "Content-Type: #{env['CONTENT_TYPE']}\n" if env['CONTENT_TYPE'] && !env['CONTENT_TYPE'].empty? - body += "Content-Length: #{env['CONTENT_LENGTH']}\n" if env['CONTENT_LENGTH'] && !env['CONTENT_LENGTH'].empty? - [200, {"content-type" => "text/plain"}, [body]] - elsif env["PATH_INFO"] == "/cookie" - body = "" - if env["HTTP_COOKIE"] - env["HTTP_COOKIE"].split(";").each do |pair| - trimmed = pair.lstrip - eq = trimmed.index("=") - if eq && eq > 0 - body += "#{trimmed[0...eq]}=#{trimmed[(eq+1)..]}\n" - end - end - end - [200, {"content-type" => "text/plain"}, [body]] - elsif env["REQUEST_METHOD"] == "POST" - body = env["rack.input"] ? env["rack.input"].read : "" - [200, {"content-type" => "text/plain"}, [body]] - else - [200, {"content-type" => "text/plain"}, ["OK"]] - end - } -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "H2O" +description: "H2O (C) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/H2OServer) + +## Dockerfile + +```dockerfile +FROM ubuntu:24.04 AS build +RUN apt-get update && apt-get install -y cmake gcc g++ pkg-config libssl-dev zlib1g-dev git ruby bison && rm -rf /var/lib/apt/lists/* +RUN git clone --recurse-submodules --depth 1 https://github.com/h2o/h2o.git /src/h2o +WORKDIR /src/h2o/build +RUN cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local -DWITH_MRUBY=ON && make -j$(nproc) && make install + +FROM ubuntu:24.04 +RUN apt-get update && apt-get install -y libssl3t64 && rm -rf /var/lib/apt/lists/* +COPY --from=build /usr/local/bin/h2o /usr/local/bin/ +COPY --from=build /usr/local/share/h2o/ /usr/local/share/h2o/ +COPY src/Servers/H2OServer/h2o.conf /etc/h2o/h2o.conf +RUN mkdir -p /var/www && echo "OK" > /var/www/index.html +ENTRYPOINT ["h2o", "-c", "/etc/h2o/h2o.conf"] +``` + +## Source + +```yaml +listen: 8080 +hosts: + default: + paths: + /: + mruby.handler: | + proc {|env| + if env["PATH_INFO"] == "/echo" + body = "" + env.each do |k, v| + if k.start_with?("HTTP_") + name = k.sub("HTTP_", "").split("_").map(&:capitalize).join("-") + body += "#{name}: #{v}\n" + end + end + body += "Content-Type: #{env['CONTENT_TYPE']}\n" if env['CONTENT_TYPE'] && !env['CONTENT_TYPE'].empty? + body += "Content-Length: #{env['CONTENT_LENGTH']}\n" if env['CONTENT_LENGTH'] && !env['CONTENT_LENGTH'].empty? + [200, {"content-type" => "text/plain"}, [body]] + elsif env["PATH_INFO"] == "/cookie" + body = "" + if env["HTTP_COOKIE"] + env["HTTP_COOKIE"].split(";").each do |pair| + trimmed = pair.lstrip + eq = trimmed.index("=") + if eq && eq > 0 + body += "#{trimmed[0...eq]}=#{trimmed[(eq+1)..]}\n" + end + end + end + [200, {"content-type" => "text/plain"}, [body]] + elsif env["REQUEST_METHOD"] == "POST" + body = env["rack.input"] ? env["rack.input"].read : "" + [200, {"content-type" => "text/plain"}, [body]] + else + [200, {"content-type" => "text/plain"}, ["OK"]] + end + } +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/haproxy.md b/docs/content/servers/haproxy.md index 7532d06..b0a4165 100644 --- a/docs/content/servers/haproxy.md +++ b/docs/content/servers/haproxy.md @@ -1,133 +1,134 @@ ---- -title: "HAProxy" -toc: true -breadcrumbs: false ---- - -**Language:** C · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/HAProxyServer) - -## Dockerfile - -```dockerfile -FROM haproxy:3.0-alpine -COPY src/Servers/HAProxyServer/haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg -COPY src/Servers/HAProxyServer/echo.lua /usr/local/etc/haproxy/echo.lua -``` - -## Source - -**`haproxy.cfg`** - -```text -global - log stdout format raw local0 - lua-load /usr/local/etc/haproxy/echo.lua - -defaults - mode http - timeout client 10s - timeout connect 5s - timeout server 10s - -frontend http_in - bind *:8080 - use_backend echo_backend if { path /echo } - use_backend cookie_backend if { path /cookie } - use_backend post_echo_backend if { method POST } - http-request return status 200 content-type "text/plain" string "OK" - -backend echo_backend - http-request use-service lua.echo - -backend cookie_backend - http-request use-service lua.cookie - -backend post_echo_backend - http-request use-service lua.echo_body -``` - -**`echo.lua`** - -```lua -core.register_service("echo", "http", function(applet) - local body = "" - local hdrs = applet.headers - for name, values in pairs(hdrs) do - for _, v in ipairs(values) do - body = body .. name .. ": " .. v .. "\n" - end - end - applet:set_status(200) - applet:add_header("Content-Type", "text/plain") - applet:add_header("Content-Length", tostring(#body)) - applet:start_response() - applet:send(body) -end) - -core.register_service("cookie", "http", function(applet) - local body = "" - local hdrs = applet.headers - if hdrs["cookie"] then - for _, raw in ipairs(hdrs["cookie"]) do - for pair in raw:gmatch("[^;]+") do - local trimmed = pair:match("^%s*(.*)") - local eq = trimmed:find("=") - if eq and eq > 1 then - body = body .. trimmed:sub(1, eq-1) .. "=" .. trimmed:sub(eq+1) .. "\n" - end - end - end - end - applet:set_status(200) - applet:add_header("Content-Type", "text/plain") - applet:add_header("Content-Length", tostring(#body)) - applet:start_response() - applet:send(body) -end) - -core.register_service("echo_body", "http", function(applet) - local body = applet:receive() - if body == nil then body = "" end - applet:set_status(200) - applet:add_header("Content-Type", "text/plain") - applet:add_header("Content-Length", tostring(#body)) - applet:start_response() - applet:send(body) -end) -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "HAProxy" +description: "HAProxy (C) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/HAProxyServer) + +## Dockerfile + +```dockerfile +FROM haproxy:3.0-alpine +COPY src/Servers/HAProxyServer/haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg +COPY src/Servers/HAProxyServer/echo.lua /usr/local/etc/haproxy/echo.lua +``` + +## Source + +**`haproxy.cfg`** + +```text +global + log stdout format raw local0 + lua-load /usr/local/etc/haproxy/echo.lua + +defaults + mode http + timeout client 10s + timeout connect 5s + timeout server 10s + +frontend http_in + bind *:8080 + use_backend echo_backend if { path /echo } + use_backend cookie_backend if { path /cookie } + use_backend post_echo_backend if { method POST } + http-request return status 200 content-type "text/plain" string "OK" + +backend echo_backend + http-request use-service lua.echo + +backend cookie_backend + http-request use-service lua.cookie + +backend post_echo_backend + http-request use-service lua.echo_body +``` + +**`echo.lua`** + +```lua +core.register_service("echo", "http", function(applet) + local body = "" + local hdrs = applet.headers + for name, values in pairs(hdrs) do + for _, v in ipairs(values) do + body = body .. name .. ": " .. v .. "\n" + end + end + applet:set_status(200) + applet:add_header("Content-Type", "text/plain") + applet:add_header("Content-Length", tostring(#body)) + applet:start_response() + applet:send(body) +end) + +core.register_service("cookie", "http", function(applet) + local body = "" + local hdrs = applet.headers + if hdrs["cookie"] then + for _, raw in ipairs(hdrs["cookie"]) do + for pair in raw:gmatch("[^;]+") do + local trimmed = pair:match("^%s*(.*)") + local eq = trimmed:find("=") + if eq and eq > 1 then + body = body .. trimmed:sub(1, eq-1) .. "=" .. trimmed:sub(eq+1) .. "\n" + end + end + end + end + applet:set_status(200) + applet:add_header("Content-Type", "text/plain") + applet:add_header("Content-Length", tostring(#body)) + applet:start_response() + applet:send(body) +end) + +core.register_service("echo_body", "http", function(applet) + local body = applet:receive() + if body == nil then body = "" end + applet:set_status(200) + applet:add_header("Content-Type", "text/plain") + applet:add_header("Content-Length", tostring(#body)) + applet:start_response() + applet:send(body) +end) +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/hyper.md b/docs/content/servers/hyper.md index afcf322..995aada 100644 --- a/docs/content/servers/hyper.md +++ b/docs/content/servers/hyper.md @@ -1,139 +1,140 @@ ---- -title: "Hyper" -toc: true -breadcrumbs: false ---- - -**Language:** Rust · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/HyperServer) - -## Dockerfile - -```dockerfile -FROM rust:1-slim AS build -WORKDIR /src - -# Cache dependencies with dummy main -COPY src/Servers/HyperServer/Cargo.toml . -RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src target/release/.fingerprint/hyper-server-* - -COPY src/Servers/HyperServer/src/ src/ -RUN cargo build --release - -FROM debian:bookworm-slim -COPY --from=build /src/target/release/hyper-server /usr/local/bin/ -ENTRYPOINT ["hyper-server", "8080"] -``` - -## Source - -```rust -use std::convert::Infallible; -use std::net::SocketAddr; - -use http_body_util::Full; -use hyper::body::Bytes; -use hyper::server::conn::http1; -use hyper::service::service_fn; -use hyper::{Request, Response}; -use hyper_util::rt::TokioIo; -use tokio::net::TcpListener; - -async fn handle(req: Request) -> Result>, Infallible> { - if req.uri().path() == "/echo" { - let mut body = String::new(); - for (name, value) in req.headers() { - body.push_str(&format!("{}: {}\n", name, value.to_str().unwrap_or(""))); - } - return Ok(Response::builder() - .status(200) - .header("Content-Type", "text/plain") - .body(Full::new(Bytes::from(body))) - .unwrap()); - } - if req.uri().path() == "/cookie" { - let mut body = String::new(); - if let Some(raw) = req.headers().get("cookie").and_then(|v| v.to_str().ok()) { - for pair in raw.split(';') { - let trimmed = pair.trim_start(); - if let Some(eq) = trimmed.find('=') { - body.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); - } - } - } - return Ok(Response::builder() - .status(200) - .header("Content-Type", "text/plain") - .body(Full::new(Bytes::from(body))) - .unwrap()); - } - if req.method() == hyper::Method::POST { - let body = match http_body_util::BodyExt::collect(req.into_body()).await { - Ok(collected) => collected.to_bytes(), - Err(_) => Bytes::new(), - }; - return Ok(Response::new(Full::new(body))); - } - Ok(Response::new(Full::new(Bytes::from("OK")))) -} - -#[tokio::main] -async fn main() { - let port: u16 = std::env::args() - .nth(1) - .and_then(|s| s.parse().ok()) - .unwrap_or(8080); - - let addr = SocketAddr::from(([0, 0, 0, 0], port)); - let listener = TcpListener::bind(addr).await.unwrap(); - - loop { - let (stream, _) = listener.accept().await.unwrap(); - let io = TokioIo::new(stream); - - tokio::task::spawn(async move { - if let Err(err) = http1::Builder::new() - .serve_connection(io, service_fn(handle)) - .await - { - eprintln!("Error serving connection: {err:?}"); - } - }); - } -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Hyper" +description: "Hyper (Rust) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Rust · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/HyperServer) + +## Dockerfile + +```dockerfile +FROM rust:1-slim AS build +WORKDIR /src + +# Cache dependencies with dummy main +COPY src/Servers/HyperServer/Cargo.toml . +RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src target/release/.fingerprint/hyper-server-* + +COPY src/Servers/HyperServer/src/ src/ +RUN cargo build --release + +FROM debian:bookworm-slim +COPY --from=build /src/target/release/hyper-server /usr/local/bin/ +ENTRYPOINT ["hyper-server", "8080"] +``` + +## Source + +```rust +use std::convert::Infallible; +use std::net::SocketAddr; + +use http_body_util::Full; +use hyper::body::Bytes; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{Request, Response}; +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; + +async fn handle(req: Request) -> Result>, Infallible> { + if req.uri().path() == "/echo" { + let mut body = String::new(); + for (name, value) in req.headers() { + body.push_str(&format!("{}: {}\n", name, value.to_str().unwrap_or(""))); + } + return Ok(Response::builder() + .status(200) + .header("Content-Type", "text/plain") + .body(Full::new(Bytes::from(body))) + .unwrap()); + } + if req.uri().path() == "/cookie" { + let mut body = String::new(); + if let Some(raw) = req.headers().get("cookie").and_then(|v| v.to_str().ok()) { + for pair in raw.split(';') { + let trimmed = pair.trim_start(); + if let Some(eq) = trimmed.find('=') { + body.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); + } + } + } + return Ok(Response::builder() + .status(200) + .header("Content-Type", "text/plain") + .body(Full::new(Bytes::from(body))) + .unwrap()); + } + if req.method() == hyper::Method::POST { + let body = match http_body_util::BodyExt::collect(req.into_body()).await { + Ok(collected) => collected.to_bytes(), + Err(_) => Bytes::new(), + }; + return Ok(Response::new(Full::new(body))); + } + Ok(Response::new(Full::new(Bytes::from("OK")))) +} + +#[tokio::main] +async fn main() { + let port: u16 = std::env::args() + .nth(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(8080); + + let addr = SocketAddr::from(([0, 0, 0, 0], port)); + let listener = TcpListener::bind(addr).await.unwrap(); + + loop { + let (stream, _) = listener.accept().await.unwrap(); + let io = TokioIo::new(stream); + + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new() + .serve_connection(io, service_fn(handle)) + .await + { + eprintln!("Error serving connection: {err:?}"); + } + }); + } +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/jetty.md b/docs/content/servers/jetty.md index a77246b..61182d5 100644 --- a/docs/content/servers/jetty.md +++ b/docs/content/servers/jetty.md @@ -1,131 +1,132 @@ ---- -title: "Jetty" -toc: true -breadcrumbs: false ---- - -**Language:** Java · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/JettyServer) - -## Dockerfile - -```dockerfile -FROM maven:3.9-eclipse-temurin-21 AS build -WORKDIR /src -COPY src/Servers/JettyServer/pom.xml . -RUN mvn dependency:go-offline -q -COPY src/Servers/JettyServer/src/ src/ -RUN mvn package -q -DskipTests - -FROM eclipse-temurin:21-jre -WORKDIR /app -COPY --from=build /src/target/jetty-server-1.0.0.jar app.jar -ENTRYPOINT ["java", "-jar", "app.jar", "8080"] -``` - -## Source - -```java -package server; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; - -import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.util.Callback; - -public class Application extends Handler.Abstract { - - private static final ByteBuffer OK_BODY = - ByteBuffer.wrap("OK".getBytes(StandardCharsets.UTF_8)).asReadOnlyBuffer(); - - @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception { - response.setStatus(200); - response.getHeaders().put("Content-Type", "text/plain"); - - if ("/cookie".equals(request.getHttpURI().getPath())) { - StringBuilder sb = new StringBuilder(); - for (HttpField field : request.getHeaders()) { - if ("Cookie".equalsIgnoreCase(field.getName())) { - for (String pair : field.getValue().split(";")) { - String trimmed = pair.stripLeading(); - int eq = trimmed.indexOf('='); - if (eq > 0) { - sb.append(trimmed, 0, eq).append("=").append(trimmed.substring(eq + 1)).append("\n"); - } - } - } - } - byte[] cookieBody = sb.toString().getBytes(StandardCharsets.UTF_8); - response.write(true, ByteBuffer.wrap(cookieBody), callback); - } else if ("/echo".equals(request.getHttpURI().getPath())) { - StringBuilder sb = new StringBuilder(); - for (HttpField field : request.getHeaders()) { - sb.append(field.getName()).append(": ").append(field.getValue()).append("\n"); - } - byte[] echoBody = sb.toString().getBytes(StandardCharsets.UTF_8); - response.write(true, ByteBuffer.wrap(echoBody), callback); - } else if ("POST".equals(request.getMethod())) { - byte[] body = Request.asInputStream(request).readAllBytes(); - response.write(true, ByteBuffer.wrap(body), callback); - } else { - response.write(true, OK_BODY.slice(), callback); - } - return true; - } - - public static void main(String[] args) throws Exception { - int port = args.length > 0 ? Integer.parseInt(args[0]) : 9007; - - Server server = new Server(); - ServerConnector connector = new ServerConnector(server); - connector.setHost("127.0.0.1"); - connector.setPort(port); - server.addConnector(connector); - server.setHandler(new Application()); - server.start(); - server.join(); - } -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Jetty" +description: "Jetty (Java) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Java · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/JettyServer) + +## Dockerfile + +```dockerfile +FROM maven:3.9-eclipse-temurin-21 AS build +WORKDIR /src +COPY src/Servers/JettyServer/pom.xml . +RUN mvn dependency:go-offline -q +COPY src/Servers/JettyServer/src/ src/ +RUN mvn package -q -DskipTests + +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=build /src/target/jetty-server-1.0.0.jar app.jar +ENTRYPOINT ["java", "-jar", "app.jar", "8080"] +``` + +## Source + +```java +package server; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.util.Callback; + +public class Application extends Handler.Abstract { + + private static final ByteBuffer OK_BODY = + ByteBuffer.wrap("OK".getBytes(StandardCharsets.UTF_8)).asReadOnlyBuffer(); + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + response.setStatus(200); + response.getHeaders().put("Content-Type", "text/plain"); + + if ("/cookie".equals(request.getHttpURI().getPath())) { + StringBuilder sb = new StringBuilder(); + for (HttpField field : request.getHeaders()) { + if ("Cookie".equalsIgnoreCase(field.getName())) { + for (String pair : field.getValue().split(";")) { + String trimmed = pair.stripLeading(); + int eq = trimmed.indexOf('='); + if (eq > 0) { + sb.append(trimmed, 0, eq).append("=").append(trimmed.substring(eq + 1)).append("\n"); + } + } + } + } + byte[] cookieBody = sb.toString().getBytes(StandardCharsets.UTF_8); + response.write(true, ByteBuffer.wrap(cookieBody), callback); + } else if ("/echo".equals(request.getHttpURI().getPath())) { + StringBuilder sb = new StringBuilder(); + for (HttpField field : request.getHeaders()) { + sb.append(field.getName()).append(": ").append(field.getValue()).append("\n"); + } + byte[] echoBody = sb.toString().getBytes(StandardCharsets.UTF_8); + response.write(true, ByteBuffer.wrap(echoBody), callback); + } else if ("POST".equals(request.getMethod())) { + byte[] body = Request.asInputStream(request).readAllBytes(); + response.write(true, ByteBuffer.wrap(body), callback); + } else { + response.write(true, OK_BODY.slice(), callback); + } + return true; + } + + public static void main(String[] args) throws Exception { + int port = args.length > 0 ? Integer.parseInt(args[0]) : 9007; + + Server server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setHost("127.0.0.1"); + connector.setPort(port); + server.addConnector(connector); + server.setHandler(new Application()); + server.start(); + server.join(); + } +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/lighttpd.md b/docs/content/servers/lighttpd.md index 7aa9d1e..2c9de2a 100644 --- a/docs/content/servers/lighttpd.md +++ b/docs/content/servers/lighttpd.md @@ -1,113 +1,114 @@ ---- -title: "Lighttpd" -toc: true -breadcrumbs: false ---- - -**Language:** C · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/LighttpdServer) - -## Dockerfile - -```dockerfile -FROM alpine:3.20 -RUN apk add --no-cache lighttpd -COPY src/Servers/LighttpdServer/lighttpd.conf /etc/lighttpd/lighttpd.conf -COPY src/Servers/LighttpdServer/index.cgi /var/www/index.cgi -COPY src/Servers/LighttpdServer/echo.cgi /var/www/echo.cgi -COPY src/Servers/LighttpdServer/cookie.cgi /var/www/cookie.cgi -RUN chmod +x /var/www/index.cgi /var/www/echo.cgi /var/www/cookie.cgi -EXPOSE 8080 -CMD ["lighttpd", "-D", "-f", "/etc/lighttpd/lighttpd.conf"] -``` - -## Source - -**`lighttpd.conf`** - -```text -server.document-root = "/var/www" -server.port = 8080 -index-file.names = ("index.cgi") -server.modules += ("mod_cgi", "mod_alias") -cgi.assign = (".cgi" => "") -server.error-handler = "/index.cgi" -alias.url = ("/echo" => "/var/www/echo.cgi", "/cookie" => "/var/www/cookie.cgi") -``` - -**`index.cgi`** - -```bash -#!/bin/sh -printf 'Content-Type: text/plain\r\n\r\n' -if [ "$REQUEST_METHOD" = "POST" ] && [ "${CONTENT_LENGTH:-0}" -gt 0 ] 2>/dev/null; then - head -c "$CONTENT_LENGTH" -else - printf 'OK' -fi -``` - -**`echo.cgi`** - -```bash -#!/bin/sh -printf 'Content-Type: text/plain\r\n\r\n' -env | grep '^HTTP_' | while IFS='=' read -r key value; do - name=$(echo "$key" | sed 's/^HTTP_//;s/_/-/g') - printf '%s: %s\n' "$name" "$value" -done -if [ -n "$CONTENT_TYPE" ]; then - printf 'Content-Type: %s\n' "$CONTENT_TYPE" -fi -if [ -n "$CONTENT_LENGTH" ]; then - printf 'Content-Length: %s\n' "$CONTENT_LENGTH" -fi -``` - -**`cookie.cgi`** - -```bash -#!/bin/sh -printf 'Content-Type: text/plain\r\n\r\n' -if [ -n "$HTTP_COOKIE" ]; then - echo "$HTTP_COOKIE" | tr ';' '\n' | while read -r pair; do - trimmed=$(echo "$pair" | sed 's/^ *//') - printf '%s\n' "$trimmed" - done -fi -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Lighttpd" +description: "Lighttpd (C) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/LighttpdServer) + +## Dockerfile + +```dockerfile +FROM alpine:3.20 +RUN apk add --no-cache lighttpd +COPY src/Servers/LighttpdServer/lighttpd.conf /etc/lighttpd/lighttpd.conf +COPY src/Servers/LighttpdServer/index.cgi /var/www/index.cgi +COPY src/Servers/LighttpdServer/echo.cgi /var/www/echo.cgi +COPY src/Servers/LighttpdServer/cookie.cgi /var/www/cookie.cgi +RUN chmod +x /var/www/index.cgi /var/www/echo.cgi /var/www/cookie.cgi +EXPOSE 8080 +CMD ["lighttpd", "-D", "-f", "/etc/lighttpd/lighttpd.conf"] +``` + +## Source + +**`lighttpd.conf`** + +```text +server.document-root = "/var/www" +server.port = 8080 +index-file.names = ("index.cgi") +server.modules += ("mod_cgi", "mod_alias") +cgi.assign = (".cgi" => "") +server.error-handler = "/index.cgi" +alias.url = ("/echo" => "/var/www/echo.cgi", "/cookie" => "/var/www/cookie.cgi") +``` + +**`index.cgi`** + +```bash +#!/bin/sh +printf 'Content-Type: text/plain\r\n\r\n' +if [ "$REQUEST_METHOD" = "POST" ] && [ "${CONTENT_LENGTH:-0}" -gt 0 ] 2>/dev/null; then + head -c "$CONTENT_LENGTH" +else + printf 'OK' +fi +``` + +**`echo.cgi`** + +```bash +#!/bin/sh +printf 'Content-Type: text/plain\r\n\r\n' +env | grep '^HTTP_' | while IFS='=' read -r key value; do + name=$(echo "$key" | sed 's/^HTTP_//;s/_/-/g') + printf '%s: %s\n' "$name" "$value" +done +if [ -n "$CONTENT_TYPE" ]; then + printf 'Content-Type: %s\n' "$CONTENT_TYPE" +fi +if [ -n "$CONTENT_LENGTH" ]; then + printf 'Content-Length: %s\n' "$CONTENT_LENGTH" +fi +``` + +**`cookie.cgi`** + +```bash +#!/bin/sh +printf 'Content-Type: text/plain\r\n\r\n' +if [ -n "$HTTP_COOKIE" ]; then + echo "$HTTP_COOKIE" | tr ';' '\n' | while read -r pair; do + trimmed=$(echo "$pair" | sed 's/^ *//') + printf '%s\n' "$trimmed" + done +fi +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/netcoreserver.md b/docs/content/servers/netcoreserver.md index d8dbd50..5f34a79 100644 --- a/docs/content/servers/netcoreserver.md +++ b/docs/content/servers/netcoreserver.md @@ -1,138 +1,139 @@ ---- -title: "NetCoreServer" -toc: true -breadcrumbs: false ---- - -**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/NetCoreServerFramework) - -## Dockerfile - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY Directory.Build.props . -COPY src/Servers/NetCoreServerFramework/ src/Servers/NetCoreServerFramework/ -RUN dotnet restore src/Servers/NetCoreServerFramework/NetCoreServerFramework.csproj -RUN dotnet publish src/Servers/NetCoreServerFramework/NetCoreServerFramework.csproj -c Release -o /app --no-restore - -FROM mcr.microsoft.com/dotnet/runtime:10.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "NetCoreServerFramework.dll", "8080"] -``` - -## Source - -```csharp -using System.Net; -using System.Net.Sockets; -using NetCoreServer; - -var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; - -var server = new OkHttpServer(IPAddress.Any, port); -server.Start(); - -Console.WriteLine($"NetCoreServer listening on http://localhost:{port}"); - -var waitHandle = new ManualResetEvent(false); -Console.CancelKeyPress += (_, e) => { e.Cancel = true; waitHandle.Set(); }; -waitHandle.WaitOne(); - -server.Stop(); - -class OkHttpSession : HttpSession -{ - public OkHttpSession(NetCoreServer.HttpServer server) : base(server) { } - - protected override void OnReceivedRequest(HttpRequest request) - { - if (request.Url == "/echo") - { - var sb = new System.Text.StringBuilder(); - for (int i = 0; i < request.Headers; i++) - { - var (name, value) = request.Header(i); - sb.AppendLine($"{name}: {value}"); - } - SendResponseAsync(Response.MakeOkResponse(200).SetBody(sb.ToString())); - } - else if (request.Url == "/cookie") - { - var sb = new System.Text.StringBuilder(); - for (int i = 0; i < request.Headers; i++) - { - var (name, value) = request.Header(i); - if (string.Equals(name, "Cookie", StringComparison.OrdinalIgnoreCase)) - { - foreach (var pair in value.Split(';')) - { - var trimmed = pair.TrimStart(); - var eqIdx = trimmed.IndexOf('='); - if (eqIdx > 0) - sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); - } - } - } - SendResponseAsync(Response.MakeOkResponse(200).SetBody(sb.ToString())); - } - else if (request.Method == "POST" && request.Body.Length > 0) - SendResponseAsync(Response.MakeOkResponse(200).SetBody(request.Body)); - else - SendResponseAsync(Response.MakeOkResponse(200).SetBody("OK")); - } - - protected override void OnReceivedRequestError(HttpRequest request, string error) - { - SendResponseAsync(Response.MakeErrorResponse(400)); - } - - protected override void OnError(SocketError error) { } -} - -class OkHttpServer : NetCoreServer.HttpServer -{ - public OkHttpServer(IPAddress address, int port) : base(address, port) { } - - protected override TcpSession CreateSession() => new OkHttpSession(this); - - protected override void OnError(SocketError error) { } -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "NetCoreServer" +description: "NetCoreServer (C#) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/NetCoreServerFramework) + +## Dockerfile + +```dockerfile +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Directory.Build.props . +COPY src/Servers/NetCoreServerFramework/ src/Servers/NetCoreServerFramework/ +RUN dotnet restore src/Servers/NetCoreServerFramework/NetCoreServerFramework.csproj +RUN dotnet publish src/Servers/NetCoreServerFramework/NetCoreServerFramework.csproj -c Release -o /app --no-restore + +FROM mcr.microsoft.com/dotnet/runtime:10.0 +WORKDIR /app +COPY --from=build /app . +ENTRYPOINT ["dotnet", "NetCoreServerFramework.dll", "8080"] +``` + +## Source + +```csharp +using System.Net; +using System.Net.Sockets; +using NetCoreServer; + +var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; + +var server = new OkHttpServer(IPAddress.Any, port); +server.Start(); + +Console.WriteLine($"NetCoreServer listening on http://localhost:{port}"); + +var waitHandle = new ManualResetEvent(false); +Console.CancelKeyPress += (_, e) => { e.Cancel = true; waitHandle.Set(); }; +waitHandle.WaitOne(); + +server.Stop(); + +class OkHttpSession : HttpSession +{ + public OkHttpSession(NetCoreServer.HttpServer server) : base(server) { } + + protected override void OnReceivedRequest(HttpRequest request) + { + if (request.Url == "/echo") + { + var sb = new System.Text.StringBuilder(); + for (int i = 0; i < request.Headers; i++) + { + var (name, value) = request.Header(i); + sb.AppendLine($"{name}: {value}"); + } + SendResponseAsync(Response.MakeOkResponse(200).SetBody(sb.ToString())); + } + else if (request.Url == "/cookie") + { + var sb = new System.Text.StringBuilder(); + for (int i = 0; i < request.Headers; i++) + { + var (name, value) = request.Header(i); + if (string.Equals(name, "Cookie", StringComparison.OrdinalIgnoreCase)) + { + foreach (var pair in value.Split(';')) + { + var trimmed = pair.TrimStart(); + var eqIdx = trimmed.IndexOf('='); + if (eqIdx > 0) + sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); + } + } + } + SendResponseAsync(Response.MakeOkResponse(200).SetBody(sb.ToString())); + } + else if (request.Method == "POST" && request.Body.Length > 0) + SendResponseAsync(Response.MakeOkResponse(200).SetBody(request.Body)); + else + SendResponseAsync(Response.MakeOkResponse(200).SetBody("OK")); + } + + protected override void OnReceivedRequestError(HttpRequest request, string error) + { + SendResponseAsync(Response.MakeErrorResponse(400)); + } + + protected override void OnError(SocketError error) { } +} + +class OkHttpServer : NetCoreServer.HttpServer +{ + public OkHttpServer(IPAddress address, int port) : base(address, port) { } + + protected override TcpSession CreateSession() => new OkHttpSession(this); + + protected override void OnError(SocketError error) { } +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/nginx.md b/docs/content/servers/nginx.md index fc722af..9e0c832 100644 --- a/docs/content/servers/nginx.md +++ b/docs/content/servers/nginx.md @@ -1,134 +1,135 @@ ---- -title: "Nginx" -toc: true -breadcrumbs: false ---- - -**Language:** C · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/NginxServer) - -## Dockerfile - -```dockerfile -FROM nginx:1.27 -COPY src/Servers/NginxServer/nginx.conf /etc/nginx/nginx.conf -COPY src/Servers/NginxServer/echo.js /etc/nginx/echo.js -``` - -## Source - -**`nginx.conf`** - -```nginx -load_module modules/ngx_http_js_module.so; - -worker_processes 1; -pid /tmp/nginx.pid; -error_log /tmp/nginx_error.log; - -events { - worker_connections 64; -} - -http { - access_log /tmp/nginx_access.log; - client_body_temp_path /tmp/nginx_client_body; - proxy_temp_path /tmp/nginx_proxy; - fastcgi_temp_path /tmp/nginx_fastcgi; - uwsgi_temp_path /tmp/nginx_uwsgi; - scgi_temp_path /tmp/nginx_scgi; - - js_import echo from /etc/nginx/echo.js; - - server { - listen 8080; - server_name localhost; - - location /echo { - js_content echo.echo; - } - - location /cookie { - js_content echo.cookie; - } - - location / { - js_content echo.handler; - } - } -} -``` - -**`echo.js`** - -```javascript -function echo(r) { - var body = ''; - var headers = r.headersIn; - for (var name in headers) { - body += name + ': ' + headers[name] + '\n'; - } - r.return(200, body); -} - -function cookie(r) { - var body = ''; - var raw = r.headersIn['Cookie']; - if (raw) { - var pairs = raw.split(';'); - for (var i = 0; i < pairs.length; i++) { - var trimmed = pairs[i].replace(/^\s+/, ''); - var eq = trimmed.indexOf('='); - if (eq > 0) { - body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; - } - } - } - r.return(200, body); -} - -function handler(r) { - if (r.method === 'POST') { - r.return(200, r.requestText || ''); - } else { - r.return(200, 'OK'); - } -} - -export default { echo, cookie, handler }; -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Nginx" +description: "Nginx (C) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/NginxServer) + +## Dockerfile + +```dockerfile +FROM nginx:1.27 +COPY src/Servers/NginxServer/nginx.conf /etc/nginx/nginx.conf +COPY src/Servers/NginxServer/echo.js /etc/nginx/echo.js +``` + +## Source + +**`nginx.conf`** + +```nginx +load_module modules/ngx_http_js_module.so; + +worker_processes 1; +pid /tmp/nginx.pid; +error_log /tmp/nginx_error.log; + +events { + worker_connections 64; +} + +http { + access_log /tmp/nginx_access.log; + client_body_temp_path /tmp/nginx_client_body; + proxy_temp_path /tmp/nginx_proxy; + fastcgi_temp_path /tmp/nginx_fastcgi; + uwsgi_temp_path /tmp/nginx_uwsgi; + scgi_temp_path /tmp/nginx_scgi; + + js_import echo from /etc/nginx/echo.js; + + server { + listen 8080; + server_name localhost; + + location /echo { + js_content echo.echo; + } + + location /cookie { + js_content echo.cookie; + } + + location / { + js_content echo.handler; + } + } +} +``` + +**`echo.js`** + +```javascript +function echo(r) { + var body = ''; + var headers = r.headersIn; + for (var name in headers) { + body += name + ': ' + headers[name] + '\n'; + } + r.return(200, body); +} + +function cookie(r) { + var body = ''; + var raw = r.headersIn['Cookie']; + if (raw) { + var pairs = raw.split(';'); + for (var i = 0; i < pairs.length; i++) { + var trimmed = pairs[i].replace(/^\s+/, ''); + var eq = trimmed.indexOf('='); + if (eq > 0) { + body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; + } + } + } + r.return(200, body); +} + +function handler(r) { + if (r.method === 'POST') { + r.return(200, r.requestText || ''); + } else { + r.return(200, 'OK'); + } +} + +export default { echo, cookie, handler }; +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/node.md b/docs/content/servers/node.md index be6ec6e..a4d7120 100644 --- a/docs/content/servers/node.md +++ b/docs/content/servers/node.md @@ -1,100 +1,101 @@ ---- -title: "Node.js" -toc: true -breadcrumbs: false ---- - -**Language:** JavaScript · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/NodeServer) - -## Dockerfile - -```dockerfile -FROM node:22-slim -WORKDIR /app -COPY src/Servers/NodeServer/server.js . -ENTRYPOINT ["node", "server.js", "8080"] -``` - -## Source - -```javascript -const http = require('http'); - -const port = parseInt(process.argv[2] || '8080', 10); - -const server = http.createServer((req, res) => { - let pathname; - try { - pathname = new URL(req.url, `http://${req.headers.host || 'localhost'}`).pathname; - } catch { - pathname = req.url; - } - if (pathname === '/cookie') { - let body = ''; - const raw = req.headers.cookie || ''; - for (const pair of raw.split(';')) { - const trimmed = pair.trimStart(); - const eq = trimmed.indexOf('='); - if (eq > 0) body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; - } - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end(body); - } else if (pathname === '/echo') { - let body = ''; - for (const [name, value] of Object.entries(req.headers)) { - if (Array.isArray(value)) value.forEach(v => body += name + ': ' + v + '\n'); - else body += name + ': ' + value + '\n'; - } - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end(body); - } else if (req.method === 'POST') { - const chunks = []; - req.on('data', (chunk) => chunks.push(chunk)); - req.on('end', () => { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end(Buffer.concat(chunks)); - }); - } else { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('OK'); - } -}); - -server.listen(port, '0.0.0.0'); -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Node.js" +description: "Node.js (JavaScript) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** JavaScript · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/NodeServer) + +## Dockerfile + +```dockerfile +FROM node:22-slim +WORKDIR /app +COPY src/Servers/NodeServer/server.js . +ENTRYPOINT ["node", "server.js", "8080"] +``` + +## Source + +```javascript +const http = require('http'); + +const port = parseInt(process.argv[2] || '8080', 10); + +const server = http.createServer((req, res) => { + let pathname; + try { + pathname = new URL(req.url, `http://${req.headers.host || 'localhost'}`).pathname; + } catch { + pathname = req.url; + } + if (pathname === '/cookie') { + let body = ''; + const raw = req.headers.cookie || ''; + for (const pair of raw.split(';')) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf('='); + if (eq > 0) body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; + } + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(body); + } else if (pathname === '/echo') { + let body = ''; + for (const [name, value] of Object.entries(req.headers)) { + if (Array.isArray(value)) value.forEach(v => body += name + ': ' + v + '\n'); + else body += name + ': ' + value + '\n'; + } + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(body); + } else if (req.method === 'POST') { + const chunks = []; + req.on('data', (chunk) => chunks.push(chunk)); + req.on('end', () => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(Buffer.concat(chunks)); + }); + } else { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('OK'); + } +}); + +server.listen(port, '0.0.0.0'); +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/ntex.md b/docs/content/servers/ntex.md index b22a6c3..22697a1 100644 --- a/docs/content/servers/ntex.md +++ b/docs/content/servers/ntex.md @@ -1,121 +1,122 @@ ---- -title: "Ntex" -toc: true -breadcrumbs: false ---- - -**Language:** Rust · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/NtexServer) - -## Dockerfile - -```dockerfile -FROM rust:1-slim AS build -WORKDIR /src - -# Cache dependencies with dummy main -COPY src/Servers/NtexServer/Cargo.toml . -RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src target/release/.fingerprint/ntex-server-* - -COPY src/Servers/NtexServer/src/ src/ -RUN cargo build --release - -FROM debian:bookworm-slim -COPY --from=build /src/target/release/ntex-server /usr/local/bin/ -ENTRYPOINT ["ntex-server", "8080"] -``` - -## Source - -```rust -use ntex::web; -use ntex::util::Bytes; - -async fn echo(req: web::HttpRequest) -> impl web::Responder { - let mut body = String::new(); - for (name, value) in req.headers() { - body.push_str(&format!("{}: {}\n", name, value.to_str().unwrap_or(""))); - } - web::HttpResponse::Ok().content_type("text/plain").body(body) -} - -async fn cookie(req: web::HttpRequest) -> impl web::Responder { - let mut body = String::new(); - if let Some(raw) = req.headers().get("cookie").and_then(|v| v.to_str().ok()) { - for pair in raw.split(';') { - let trimmed = pair.trim_start(); - if let Some(eq) = trimmed.find('=') { - body.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); - } - } - } - web::HttpResponse::Ok().content_type("text/plain").body(body) -} - -async fn handler(req: web::HttpRequest, body: Bytes) -> web::HttpResponse { - if req.method() == ntex::http::Method::POST { - web::HttpResponse::Ok() - .content_type("text/plain") - .body(body) - } else { - web::HttpResponse::Ok() - .content_type("text/plain") - .body("OK") - } -} - -#[ntex::main] -async fn main() -> std::io::Result<()> { - let port: u16 = std::env::args() - .nth(1) - .and_then(|s| s.parse().ok()) - .unwrap_or(8080); - - web::server(|| { - web::App::new() - .route("/echo", web::to(echo)) - .route("/cookie", web::to(cookie)) - .default_service(web::to(handler)) - }) - .bind(("0.0.0.0", port))? - .run() - .await?; - - Ok(()) -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Ntex" +description: "Ntex (Rust) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Rust · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/NtexServer) + +## Dockerfile + +```dockerfile +FROM rust:1-slim AS build +WORKDIR /src + +# Cache dependencies with dummy main +COPY src/Servers/NtexServer/Cargo.toml . +RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src target/release/.fingerprint/ntex-server-* + +COPY src/Servers/NtexServer/src/ src/ +RUN cargo build --release + +FROM debian:bookworm-slim +COPY --from=build /src/target/release/ntex-server /usr/local/bin/ +ENTRYPOINT ["ntex-server", "8080"] +``` + +## Source + +```rust +use ntex::web; +use ntex::util::Bytes; + +async fn echo(req: web::HttpRequest) -> impl web::Responder { + let mut body = String::new(); + for (name, value) in req.headers() { + body.push_str(&format!("{}: {}\n", name, value.to_str().unwrap_or(""))); + } + web::HttpResponse::Ok().content_type("text/plain").body(body) +} + +async fn cookie(req: web::HttpRequest) -> impl web::Responder { + let mut body = String::new(); + if let Some(raw) = req.headers().get("cookie").and_then(|v| v.to_str().ok()) { + for pair in raw.split(';') { + let trimmed = pair.trim_start(); + if let Some(eq) = trimmed.find('=') { + body.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); + } + } + } + web::HttpResponse::Ok().content_type("text/plain").body(body) +} + +async fn handler(req: web::HttpRequest, body: Bytes) -> web::HttpResponse { + if req.method() == ntex::http::Method::POST { + web::HttpResponse::Ok() + .content_type("text/plain") + .body(body) + } else { + web::HttpResponse::Ok() + .content_type("text/plain") + .body("OK") + } +} + +#[ntex::main] +async fn main() -> std::io::Result<()> { + let port: u16 = std::env::args() + .nth(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(8080); + + web::server(|| { + web::App::new() + .route("/echo", web::to(echo)) + .route("/cookie", web::to(cookie)) + .default_service(web::to(handler)) + }) + .bind(("0.0.0.0", port))? + .run() + .await?; + + Ok(()) +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/php.md b/docs/content/servers/php.md index 35c0265..5b48b0e 100644 --- a/docs/content/servers/php.md +++ b/docs/content/servers/php.md @@ -1,81 +1,82 @@ ---- -title: "PHP" -toc: true -breadcrumbs: false ---- - -**Language:** PHP · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/PhpServer) - -## Dockerfile - -```dockerfile -FROM php:8.3-cli -COPY src/Servers/PhpServer/index.php /app/index.php -WORKDIR /app -EXPOSE 8080 -CMD ["php", "-S", "0.0.0.0:8080", "index.php"] -``` - -## Source - -```php - $value) { - echo "$name: $value\n"; - } - exit; -} - -if ($_SERVER['REQUEST_URI'] === '/cookie') { - header('Content-Type: text/plain'); - foreach ($_COOKIE as $name => $value) { - echo "$name=$value\n"; - } - exit; -} - -header('Content-Type: text/plain'); -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - echo file_get_contents('php://input'); -} else { - echo 'OK'; -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "PHP" +description: "PHP (PHP) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** PHP · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/PhpServer) + +## Dockerfile + +```dockerfile +FROM php:8.3-cli +COPY src/Servers/PhpServer/index.php /app/index.php +WORKDIR /app +EXPOSE 8080 +CMD ["php", "-S", "0.0.0.0:8080", "index.php"] +``` + +## Source + +```php + $value) { + echo "$name: $value\n"; + } + exit; +} + +if ($_SERVER['REQUEST_URI'] === '/cookie') { + header('Content-Type: text/plain'); + foreach ($_COOKIE as $name => $value) { + echo "$name=$value\n"; + } + exit; +} + +header('Content-Type: text/plain'); +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + echo file_get_contents('php://input'); +} else { + echo 'OK'; +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/pingora.md b/docs/content/servers/pingora.md index 5b845bf..baee8fb 100644 --- a/docs/content/servers/pingora.md +++ b/docs/content/servers/pingora.md @@ -1,177 +1,178 @@ ---- -title: "Pingora" -toc: true -breadcrumbs: false ---- - -**Language:** Rust · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/PingoraServer) - -## Dockerfile - -```dockerfile -FROM rust:1-slim AS build -RUN apt-get update && apt-get install -y pkg-config libssl-dev cmake g++ && rm -rf /var/lib/apt/lists/* -WORKDIR /src - -# Cache dependencies with dummy main -COPY src/Servers/PingoraServer/Cargo.toml . -RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src target/release/.fingerprint/pingora-server-* - -COPY src/Servers/PingoraServer/src/ src/ -RUN cargo build --release - -FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y libssl3 && rm -rf /var/lib/apt/lists/* -COPY --from=build /src/target/release/pingora-server /usr/local/bin/ -ENTRYPOINT ["pingora-server", "8080"] -``` - -## Source - -```rust -use async_trait::async_trait; -use bytes::Bytes; -use pingora::http::ResponseHeader; -use pingora::prelude::*; -use pingora::proxy::{http_proxy_service, ProxyHttp, Session}; - -struct OkProxy; - -#[async_trait] -impl ProxyHttp for OkProxy { - type CTX = (); - - fn new_ctx(&self) -> Self::CTX {} - - async fn request_filter( - &self, - session: &mut Session, - _ctx: &mut Self::CTX, - ) -> Result { - let is_cookie = session.req_header().uri.path() == "/cookie"; - if is_cookie { - let mut body_str = String::new(); - if let Some(raw) = session.req_header().headers.get("cookie").and_then(|v| v.to_str().ok()) { - for pair in raw.split(';') { - let trimmed = pair.trim_start(); - if let Some(eq) = trimmed.find('=') { - body_str.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); - } - } - } - let body = Bytes::from(body_str); - let mut header = ResponseHeader::build(200, None)?; - header.insert_header("Content-Type", "text/plain")?; - header.insert_header("Content-Length", &body.len().to_string())?; - session - .write_response_header(Box::new(header), false) - .await?; - session - .write_response_body(Some(body), true) - .await?; - return Ok(true); - } - - let is_echo = session.req_header().uri.path() == "/echo"; - if is_echo { - let mut body_str = String::new(); - for (name, value) in session.req_header().headers.iter() { - body_str.push_str(&format!("{}: {}\n", name, value.to_str().unwrap_or(""))); - } - let body = Bytes::from(body_str); - let mut header = ResponseHeader::build(200, None)?; - header.insert_header("Content-Type", "text/plain")?; - header.insert_header("Content-Length", &body.len().to_string())?; - session - .write_response_header(Box::new(header), false) - .await?; - session - .write_response_body(Some(body), true) - .await?; - return Ok(true); - } - - let is_post = session.req_header().method == pingora::http::Method::POST; - let body = if is_post { - let mut buf = Vec::new(); - while let Some(chunk) = session.read_request_body().await? { - buf.extend_from_slice(&chunk); - } - Bytes::from(buf) - } else { - Bytes::from_static(b"OK") - }; - let mut header = ResponseHeader::build(200, None)?; - header.insert_header("Content-Type", "text/plain")?; - header.insert_header("Content-Length", &body.len().to_string())?; - session - .write_response_header(Box::new(header), false) - .await?; - session - .write_response_body(Some(body), true) - .await?; - Ok(true) - } - - async fn upstream_peer( - &self, - _session: &mut Session, - _ctx: &mut Self::CTX, - ) -> Result> { - // Never reached — request_filter always handles the request - unreachable!() - } -} - -fn main() { - let port: u16 = std::env::args() - .nth(1) - .and_then(|s| s.parse().ok()) - .unwrap_or(9011); - - let mut server = Server::new(None).unwrap(); - server.bootstrap(); - - let mut proxy = http_proxy_service(&server.configuration, OkProxy); - proxy.add_tcp(&format!("0.0.0.0:{port}")); - server.add_service(proxy); - - server.run_forever(); -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Pingora" +description: "Pingora (Rust) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Rust · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/PingoraServer) + +## Dockerfile + +```dockerfile +FROM rust:1-slim AS build +RUN apt-get update && apt-get install -y pkg-config libssl-dev cmake g++ && rm -rf /var/lib/apt/lists/* +WORKDIR /src + +# Cache dependencies with dummy main +COPY src/Servers/PingoraServer/Cargo.toml . +RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src target/release/.fingerprint/pingora-server-* + +COPY src/Servers/PingoraServer/src/ src/ +RUN cargo build --release + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y libssl3 && rm -rf /var/lib/apt/lists/* +COPY --from=build /src/target/release/pingora-server /usr/local/bin/ +ENTRYPOINT ["pingora-server", "8080"] +``` + +## Source + +```rust +use async_trait::async_trait; +use bytes::Bytes; +use pingora::http::ResponseHeader; +use pingora::prelude::*; +use pingora::proxy::{http_proxy_service, ProxyHttp, Session}; + +struct OkProxy; + +#[async_trait] +impl ProxyHttp for OkProxy { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX {} + + async fn request_filter( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result { + let is_cookie = session.req_header().uri.path() == "/cookie"; + if is_cookie { + let mut body_str = String::new(); + if let Some(raw) = session.req_header().headers.get("cookie").and_then(|v| v.to_str().ok()) { + for pair in raw.split(';') { + let trimmed = pair.trim_start(); + if let Some(eq) = trimmed.find('=') { + body_str.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); + } + } + } + let body = Bytes::from(body_str); + let mut header = ResponseHeader::build(200, None)?; + header.insert_header("Content-Type", "text/plain")?; + header.insert_header("Content-Length", &body.len().to_string())?; + session + .write_response_header(Box::new(header), false) + .await?; + session + .write_response_body(Some(body), true) + .await?; + return Ok(true); + } + + let is_echo = session.req_header().uri.path() == "/echo"; + if is_echo { + let mut body_str = String::new(); + for (name, value) in session.req_header().headers.iter() { + body_str.push_str(&format!("{}: {}\n", name, value.to_str().unwrap_or(""))); + } + let body = Bytes::from(body_str); + let mut header = ResponseHeader::build(200, None)?; + header.insert_header("Content-Type", "text/plain")?; + header.insert_header("Content-Length", &body.len().to_string())?; + session + .write_response_header(Box::new(header), false) + .await?; + session + .write_response_body(Some(body), true) + .await?; + return Ok(true); + } + + let is_post = session.req_header().method == pingora::http::Method::POST; + let body = if is_post { + let mut buf = Vec::new(); + while let Some(chunk) = session.read_request_body().await? { + buf.extend_from_slice(&chunk); + } + Bytes::from(buf) + } else { + Bytes::from_static(b"OK") + }; + let mut header = ResponseHeader::build(200, None)?; + header.insert_header("Content-Type", "text/plain")?; + header.insert_header("Content-Length", &body.len().to_string())?; + session + .write_response_header(Box::new(header), false) + .await?; + session + .write_response_body(Some(body), true) + .await?; + Ok(true) + } + + async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + // Never reached — request_filter always handles the request + unreachable!() + } +} + +fn main() { + let port: u16 = std::env::args() + .nth(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(9011); + + let mut server = Server::new(None).unwrap(); + server.bootstrap(); + + let mut proxy = http_proxy_service(&server.configuration, OkProxy); + proxy.add_tcp(&format!("0.0.0.0:{port}")); + server.add_service(proxy); + + server.run_forever(); +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/puma.md b/docs/content/servers/puma.md index e7b04f5..8d17145 100644 --- a/docs/content/servers/puma.md +++ b/docs/content/servers/puma.md @@ -1,89 +1,90 @@ ---- -title: "Puma" -toc: true -breadcrumbs: false ---- - -**Language:** Ruby · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/PumaServer) - -## Dockerfile - -```dockerfile -FROM ruby:3.3-slim -RUN apt-get update && apt-get install -y --no-install-recommends build-essential && \ - gem install puma --no-doc && \ - apt-get purge -y build-essential && apt-get autoremove -y && \ - rm -rf /var/lib/apt/lists/* -COPY src/Servers/PumaServer/config.ru /app/config.ru -WORKDIR /app -EXPOSE 8080 -CMD ["puma", "-b", "tcp://0.0.0.0:8080"] -``` - -## Source - -```ruby -app = proc { |env| - if env['PATH_INFO'] == '/echo' - headers = env.select { |k, _| k.start_with?('HTTP_') } - body = headers.map { |k, v| "#{k.sub('HTTP_', '').split('_').map(&:capitalize).join('-')}: #{v}" }.join("\n") + "\n" - body += "Content-Type: #{env['CONTENT_TYPE']}\n" if env['CONTENT_TYPE'] - body += "Content-Length: #{env['CONTENT_LENGTH']}\n" if env['CONTENT_LENGTH'] - [200, { 'Content-Type' => 'text/plain' }, [body]] - elsif env['PATH_INFO'] == '/cookie' - body = "" - if env['HTTP_COOKIE'] - env['HTTP_COOKIE'].split(';').each do |pair| - trimmed = pair.lstrip - eq = trimmed.index('=') - if eq && eq > 0 - body += "#{trimmed[0...eq]}=#{trimmed[(eq+1)..]}\n" - end - end - end - [200, { 'Content-Type' => 'text/plain' }, [body]] - elsif env['REQUEST_METHOD'] == 'POST' - body = env['rack.input'].read - [200, { 'content-type' => 'text/plain' }, [body]] - else - [200, { 'content-type' => 'text/plain' }, ['OK']] - end -} -run app -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Puma" +description: "Puma (Ruby) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Ruby · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/PumaServer) + +## Dockerfile + +```dockerfile +FROM ruby:3.3-slim +RUN apt-get update && apt-get install -y --no-install-recommends build-essential && \ + gem install puma --no-doc && \ + apt-get purge -y build-essential && apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* +COPY src/Servers/PumaServer/config.ru /app/config.ru +WORKDIR /app +EXPOSE 8080 +CMD ["puma", "-b", "tcp://0.0.0.0:8080"] +``` + +## Source + +```ruby +app = proc { |env| + if env['PATH_INFO'] == '/echo' + headers = env.select { |k, _| k.start_with?('HTTP_') } + body = headers.map { |k, v| "#{k.sub('HTTP_', '').split('_').map(&:capitalize).join('-')}: #{v}" }.join("\n") + "\n" + body += "Content-Type: #{env['CONTENT_TYPE']}\n" if env['CONTENT_TYPE'] + body += "Content-Length: #{env['CONTENT_LENGTH']}\n" if env['CONTENT_LENGTH'] + [200, { 'Content-Type' => 'text/plain' }, [body]] + elsif env['PATH_INFO'] == '/cookie' + body = "" + if env['HTTP_COOKIE'] + env['HTTP_COOKIE'].split(';').each do |pair| + trimmed = pair.lstrip + eq = trimmed.index('=') + if eq && eq > 0 + body += "#{trimmed[0...eq]}=#{trimmed[(eq+1)..]}\n" + end + end + end + [200, { 'Content-Type' => 'text/plain' }, [body]] + elsif env['REQUEST_METHOD'] == 'POST' + body = env['rack.input'].read + [200, { 'content-type' => 'text/plain' }, [body]] + else + [200, { 'content-type' => 'text/plain' }, ['OK']] + end +} +run app +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/quarkus.md b/docs/content/servers/quarkus.md index ccec88e..c642d03 100644 --- a/docs/content/servers/quarkus.md +++ b/docs/content/servers/quarkus.md @@ -1,152 +1,153 @@ ---- -title: "Quarkus" -toc: true -breadcrumbs: false ---- - -**Language:** Java · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/QuarkusServer) - -## Dockerfile - -```dockerfile -FROM maven:3.9-eclipse-temurin-21 AS build -WORKDIR /src -COPY src/Servers/QuarkusServer/pom.xml . -RUN mvn dependency:go-offline -q -COPY src/Servers/QuarkusServer/src/ src/ -RUN mvn package -q -DskipTests - -FROM eclipse-temurin:21-jre -WORKDIR /app -COPY --from=build /src/target/quarkus-app/ quarkus-app/ -ENTRYPOINT ["java", "-Dquarkus.http.port=8080", "-jar", "quarkus-app/quarkus-run.jar"] -``` - -## Source - -```java -package server; - -import java.io.InputStream; -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -@Path("/") -public class Application { - - @GET - @Path("{path:.*}") - @Produces(MediaType.TEXT_PLAIN) - public String catchAll() { - return "OK"; - } - - @POST - @Path("{path:.*}") - @Produces(MediaType.TEXT_PLAIN) - public byte[] catchAllPost(InputStream body) throws IOException { - return body.readAllBytes(); - } - - @GET - @Path("/cookie") - @Produces(MediaType.TEXT_PLAIN) - public Response cookieGet(@Context HttpHeaders headers) { - return parseCookies(headers); - } - - @POST - @Path("/cookie") - @Produces(MediaType.TEXT_PLAIN) - public Response cookiePost(@Context HttpHeaders headers) { - return parseCookies(headers); - } - - @GET - @Path("/echo") - @Produces(MediaType.TEXT_PLAIN) - public Response echoGet(@Context HttpHeaders headers) { - return echoHeaders(headers); - } - - @POST - @Path("/echo") - @Produces(MediaType.TEXT_PLAIN) - public Response echoPost(@Context HttpHeaders headers) { - return echoHeaders(headers); - } - - private Response parseCookies(HttpHeaders headers) { - StringBuilder sb = new StringBuilder(); - List cookieHeaders = headers.getRequestHeader("Cookie"); - if (cookieHeaders != null) { - for (String raw : cookieHeaders) { - for (String pair : raw.split(";")) { - String trimmed = pair.stripLeading(); - int eq = trimmed.indexOf('='); - if (eq > 0) { - sb.append(trimmed, 0, eq).append("=").append(trimmed.substring(eq + 1)).append("\n"); - } - } - } - } - return Response.ok(sb.toString(), MediaType.TEXT_PLAIN).build(); - } - - private Response echoHeaders(HttpHeaders headers) { - StringBuilder sb = new StringBuilder(); - for (Map.Entry> entry : headers.getRequestHeaders().entrySet()) { - for (String value : entry.getValue()) { - sb.append(entry.getKey()).append(": ").append(value).append("\n"); - } - } - return Response.ok(sb.toString(), MediaType.TEXT_PLAIN).build(); - } -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Quarkus" +description: "Quarkus (Java) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Java · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/QuarkusServer) + +## Dockerfile + +```dockerfile +FROM maven:3.9-eclipse-temurin-21 AS build +WORKDIR /src +COPY src/Servers/QuarkusServer/pom.xml . +RUN mvn dependency:go-offline -q +COPY src/Servers/QuarkusServer/src/ src/ +RUN mvn package -q -DskipTests + +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=build /src/target/quarkus-app/ quarkus-app/ +ENTRYPOINT ["java", "-Dquarkus.http.port=8080", "-jar", "quarkus-app/quarkus-run.jar"] +``` + +## Source + +```java +package server; + +import java.io.InputStream; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path("/") +public class Application { + + @GET + @Path("{path:.*}") + @Produces(MediaType.TEXT_PLAIN) + public String catchAll() { + return "OK"; + } + + @POST + @Path("{path:.*}") + @Produces(MediaType.TEXT_PLAIN) + public byte[] catchAllPost(InputStream body) throws IOException { + return body.readAllBytes(); + } + + @GET + @Path("/cookie") + @Produces(MediaType.TEXT_PLAIN) + public Response cookieGet(@Context HttpHeaders headers) { + return parseCookies(headers); + } + + @POST + @Path("/cookie") + @Produces(MediaType.TEXT_PLAIN) + public Response cookiePost(@Context HttpHeaders headers) { + return parseCookies(headers); + } + + @GET + @Path("/echo") + @Produces(MediaType.TEXT_PLAIN) + public Response echoGet(@Context HttpHeaders headers) { + return echoHeaders(headers); + } + + @POST + @Path("/echo") + @Produces(MediaType.TEXT_PLAIN) + public Response echoPost(@Context HttpHeaders headers) { + return echoHeaders(headers); + } + + private Response parseCookies(HttpHeaders headers) { + StringBuilder sb = new StringBuilder(); + List cookieHeaders = headers.getRequestHeader("Cookie"); + if (cookieHeaders != null) { + for (String raw : cookieHeaders) { + for (String pair : raw.split(";")) { + String trimmed = pair.stripLeading(); + int eq = trimmed.indexOf('='); + if (eq > 0) { + sb.append(trimmed, 0, eq).append("=").append(trimmed.substring(eq + 1)).append("\n"); + } + } + } + } + return Response.ok(sb.toString(), MediaType.TEXT_PLAIN).build(); + } + + private Response echoHeaders(HttpHeaders headers) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry> entry : headers.getRequestHeaders().entrySet()) { + for (String value : entry.getValue()) { + sb.append(entry.getKey()).append(": ").append(value).append("\n"); + } + } + return Response.ok(sb.toString(), MediaType.TEXT_PLAIN).build(); + } +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/servicestack.md b/docs/content/servers/servicestack.md index 0275b91..008c704 100644 --- a/docs/content/servers/servicestack.md +++ b/docs/content/servers/servicestack.md @@ -1,102 +1,103 @@ ---- -title: "ServiceStack" -toc: true -breadcrumbs: false ---- - -**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/ServiceStackServer) - -## Dockerfile - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY Directory.Build.props . -COPY src/Servers/ServiceStackServer/ src/Servers/ServiceStackServer/ -RUN dotnet restore src/Servers/ServiceStackServer/ServiceStackServer.csproj -RUN dotnet publish src/Servers/ServiceStackServer/ServiceStackServer.csproj -c Release -o /app --no-restore - -FROM mcr.microsoft.com/dotnet/aspnet:10.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "ServiceStackServer.dll"] -``` - -## Source - -```csharp -using ServiceStack; - -var builder = WebApplication.CreateBuilder(args); -var app = builder.Build(); - -app.UseServiceStack(new AppHost()); -app.Map("/echo", (HttpContext ctx) => -{ - var sb = new System.Text.StringBuilder(); - foreach (var h in ctx.Request.Headers) - foreach (var v in h.Value) - sb.AppendLine($"{h.Key}: {v}"); - return Results.Text(sb.ToString()); -}); -app.Map("/cookie", (HttpContext ctx) => -{ - var sb = new System.Text.StringBuilder(); - foreach (var cookie in ctx.Request.Cookies) - sb.AppendLine($"{cookie.Key}={cookie.Value}"); - return Results.Text(sb.ToString()); -}); -app.MapFallback(async (HttpContext ctx) => -{ - if (ctx.Request.Method == "POST") - { - using var reader = new StreamReader(ctx.Request.Body); - var body = await reader.ReadToEndAsync(); - return Results.Text(body); - } - return Results.Ok("OK"); -}); -app.Run("http://0.0.0.0:8080"); - -class AppHost : AppHostBase -{ - public AppHost() : base("Probe", typeof(AppHost).Assembly) { } - public override void Configure() { } -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "ServiceStack" +description: "ServiceStack (C#) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/ServiceStackServer) + +## Dockerfile + +```dockerfile +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Directory.Build.props . +COPY src/Servers/ServiceStackServer/ src/Servers/ServiceStackServer/ +RUN dotnet restore src/Servers/ServiceStackServer/ServiceStackServer.csproj +RUN dotnet publish src/Servers/ServiceStackServer/ServiceStackServer.csproj -c Release -o /app --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app . +ENTRYPOINT ["dotnet", "ServiceStackServer.dll"] +``` + +## Source + +```csharp +using ServiceStack; + +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +app.UseServiceStack(new AppHost()); +app.Map("/echo", (HttpContext ctx) => +{ + var sb = new System.Text.StringBuilder(); + foreach (var h in ctx.Request.Headers) + foreach (var v in h.Value) + sb.AppendLine($"{h.Key}: {v}"); + return Results.Text(sb.ToString()); +}); +app.Map("/cookie", (HttpContext ctx) => +{ + var sb = new System.Text.StringBuilder(); + foreach (var cookie in ctx.Request.Cookies) + sb.AppendLine($"{cookie.Key}={cookie.Value}"); + return Results.Text(sb.ToString()); +}); +app.MapFallback(async (HttpContext ctx) => +{ + if (ctx.Request.Method == "POST") + { + using var reader = new StreamReader(ctx.Request.Body); + var body = await reader.ReadToEndAsync(); + return Results.Text(body); + } + return Results.Ok("OK"); +}); +app.Run("http://0.0.0.0:8080"); + +class AppHost : AppHostBase +{ + public AppHost() : base("Probe", typeof(AppHost).Assembly) { } + public override void Configure() { } +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/simplew.md b/docs/content/servers/simplew.md index 2384120..24cc803 100644 --- a/docs/content/servers/simplew.md +++ b/docs/content/servers/simplew.md @@ -1,115 +1,116 @@ ---- -title: "SimpleW" -toc: true -breadcrumbs: false ---- - -**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/SimpleWServer) - -## Dockerfile - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY Directory.Build.props . -COPY src/Servers/SimpleWServer/ src/Servers/SimpleWServer/ -RUN dotnet restore src/Servers/SimpleWServer/SimpleWServer.csproj -RUN dotnet publish src/Servers/SimpleWServer/SimpleWServer.csproj -c Release -o /app --no-restore - -FROM mcr.microsoft.com/dotnet/runtime:10.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "SimpleWServer.dll", "8080"] -``` - -## Source - -```csharp -using System.Net; -using SimpleW; - -var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; - -var server = new SimpleWServer(IPAddress.Any, port); - - -server.MapGet("/cookie", (HttpSession session) => ParseCookies(session)); -server.MapPost("/cookie", (HttpSession session) => ParseCookies(session)); -server.MapGet("/echo", (HttpSession session) => -{ - var sb = new System.Text.StringBuilder(); - foreach (var h in session.Request.Headers.EnumerateAll()) - sb.AppendLine($"{h.Key}: {h.Value}"); - return sb.ToString(); -}); -server.MapPost("/echo", (HttpSession session) => -{ - var sb = new System.Text.StringBuilder(); - foreach (var h in session.Request.Headers.EnumerateAll()) - sb.AppendLine($"{h.Key}: {h.Value}"); - return sb.ToString(); -}); -server.MapGet("/", () => "OK"); -server.MapGet("/{path}", () => "OK"); -server.MapPost("/", (HttpSession session) => session.Request.BodyString); -server.MapPost("/{path}", (HttpSession session) => session.Request.BodyString); - -static string ParseCookies(HttpSession session) -{ - var sb = new System.Text.StringBuilder(); - foreach (var h in session.Request.Headers.EnumerateAll()) - { - if (string.Equals(h.Key, "Cookie", StringComparison.OrdinalIgnoreCase)) - { - foreach (var pair in h.Value.Split(';')) - { - var trimmed = pair.TrimStart(); - var eqIdx = trimmed.IndexOf('='); - if (eqIdx > 0) - sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); - } - } - } - return sb.ToString(); -} - -Console.WriteLine($"SimpleW listening on http://localhost:{port}"); - -await server.RunAsync(); -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "SimpleW" +description: "SimpleW (C#) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/SimpleWServer) + +## Dockerfile + +```dockerfile +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Directory.Build.props . +COPY src/Servers/SimpleWServer/ src/Servers/SimpleWServer/ +RUN dotnet restore src/Servers/SimpleWServer/SimpleWServer.csproj +RUN dotnet publish src/Servers/SimpleWServer/SimpleWServer.csproj -c Release -o /app --no-restore + +FROM mcr.microsoft.com/dotnet/runtime:10.0 +WORKDIR /app +COPY --from=build /app . +ENTRYPOINT ["dotnet", "SimpleWServer.dll", "8080"] +``` + +## Source + +```csharp +using System.Net; +using SimpleW; + +var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; + +var server = new SimpleWServer(IPAddress.Any, port); + + +server.MapGet("/cookie", (HttpSession session) => ParseCookies(session)); +server.MapPost("/cookie", (HttpSession session) => ParseCookies(session)); +server.MapGet("/echo", (HttpSession session) => +{ + var sb = new System.Text.StringBuilder(); + foreach (var h in session.Request.Headers.EnumerateAll()) + sb.AppendLine($"{h.Key}: {h.Value}"); + return sb.ToString(); +}); +server.MapPost("/echo", (HttpSession session) => +{ + var sb = new System.Text.StringBuilder(); + foreach (var h in session.Request.Headers.EnumerateAll()) + sb.AppendLine($"{h.Key}: {h.Value}"); + return sb.ToString(); +}); +server.MapGet("/", () => "OK"); +server.MapGet("/{path}", () => "OK"); +server.MapPost("/", (HttpSession session) => session.Request.BodyString); +server.MapPost("/{path}", (HttpSession session) => session.Request.BodyString); + +static string ParseCookies(HttpSession session) +{ + var sb = new System.Text.StringBuilder(); + foreach (var h in session.Request.Headers.EnumerateAll()) + { + if (string.Equals(h.Key, "Cookie", StringComparison.OrdinalIgnoreCase)) + { + foreach (var pair in h.Value.Split(';')) + { + var trimmed = pair.TrimStart(); + var eqIdx = trimmed.IndexOf('='); + if (eqIdx > 0) + sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); + } + } + } + return sb.ToString(); +} + +Console.WriteLine($"SimpleW listening on http://localhost:{port}"); + +await server.RunAsync(); +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/sisk.md b/docs/content/servers/sisk.md index 343afbf..2435cae 100644 --- a/docs/content/servers/sisk.md +++ b/docs/content/servers/sisk.md @@ -1,113 +1,114 @@ ---- -title: "Sisk" -toc: true -breadcrumbs: false ---- - -**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/SiskServer) - -## Dockerfile - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY Directory.Build.props . -COPY src/Servers/SiskServer/ src/Servers/SiskServer/ -RUN dotnet restore src/Servers/SiskServer/SiskServer.csproj -RUN dotnet publish src/Servers/SiskServer/SiskServer.csproj -c Release -o /app --no-restore - -FROM mcr.microsoft.com/dotnet/runtime:10.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "SiskServer.dll", "8080"] -``` - -## Source - -```csharp -using Sisk.Core.Http; -using Sisk.Core.Routing; - -var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; - -using var app = HttpServer.CreateBuilder() - .UseListeningPort($"http://+:{port}/") - .Build(); - -app.Router.SetRoute(RouteMethod.Any, Route.AnyPath, request => -{ - if (request.Path == "/echo") - { - var sb = new System.Text.StringBuilder(); - foreach (var h in request.Headers) - foreach (var val in h.Value) - sb.AppendLine($"{h.Key}: {val}"); - return new HttpResponse(200).WithContent(sb.ToString()); - } - if (request.Path == "/cookie") - { - var sb = new System.Text.StringBuilder(); - foreach (var h in request.Headers) - { - if (string.Equals(h.Key, "Cookie", StringComparison.OrdinalIgnoreCase)) - { - foreach (var rawVal in h.Value) - { - foreach (var pair in rawVal.Split(';')) - { - var trimmed = pair.TrimStart(); - var eqIdx = trimmed.IndexOf('='); - if (eqIdx > 0) - sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); - } - } - } - } - return new HttpResponse(200).WithContent(sb.ToString()); - } - if (request.Method == HttpMethod.Post && request.Body is not null) - { - var body = request.Body; - return new HttpResponse(200).WithContent(body); - } - return new HttpResponse(200).WithContent("OK"); -}); - -await app.StartAsync(); -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Sisk" +description: "Sisk (C#) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/SiskServer) + +## Dockerfile + +```dockerfile +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Directory.Build.props . +COPY src/Servers/SiskServer/ src/Servers/SiskServer/ +RUN dotnet restore src/Servers/SiskServer/SiskServer.csproj +RUN dotnet publish src/Servers/SiskServer/SiskServer.csproj -c Release -o /app --no-restore + +FROM mcr.microsoft.com/dotnet/runtime:10.0 +WORKDIR /app +COPY --from=build /app . +ENTRYPOINT ["dotnet", "SiskServer.dll", "8080"] +``` + +## Source + +```csharp +using Sisk.Core.Http; +using Sisk.Core.Routing; + +var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; + +using var app = HttpServer.CreateBuilder() + .UseListeningPort($"http://+:{port}/") + .Build(); + +app.Router.SetRoute(RouteMethod.Any, Route.AnyPath, request => +{ + if (request.Path == "/echo") + { + var sb = new System.Text.StringBuilder(); + foreach (var h in request.Headers) + foreach (var val in h.Value) + sb.AppendLine($"{h.Key}: {val}"); + return new HttpResponse(200).WithContent(sb.ToString()); + } + if (request.Path == "/cookie") + { + var sb = new System.Text.StringBuilder(); + foreach (var h in request.Headers) + { + if (string.Equals(h.Key, "Cookie", StringComparison.OrdinalIgnoreCase)) + { + foreach (var rawVal in h.Value) + { + foreach (var pair in rawVal.Split(';')) + { + var trimmed = pair.TrimStart(); + var eqIdx = trimmed.IndexOf('='); + if (eqIdx > 0) + sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); + } + } + } + } + return new HttpResponse(200).WithContent(sb.ToString()); + } + if (request.Method == HttpMethod.Post && request.Body is not null) + { + var body = request.Body; + return new HttpResponse(200).WithContent(body); + } + return new HttpResponse(200).WithContent("OK"); +}); + +await app.StartAsync(); +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/spring-boot.md b/docs/content/servers/spring-boot.md index 5668719..df172ae 100644 --- a/docs/content/servers/spring-boot.md +++ b/docs/content/servers/spring-boot.md @@ -1,123 +1,124 @@ ---- -title: "Spring Boot" -toc: true -breadcrumbs: false ---- - -**Language:** Java · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/SpringBootServer) - -## Dockerfile - -```dockerfile -FROM maven:3.9-eclipse-temurin-21 AS build -WORKDIR /src -COPY src/Servers/SpringBootServer/pom.xml . -RUN mvn dependency:go-offline -q -COPY src/Servers/SpringBootServer/src/ src/ -RUN mvn package -q -DskipTests - -FROM eclipse-temurin:21-jre -WORKDIR /app -COPY --from=build /src/target/*.jar app.jar -ENTRYPOINT ["java", "-jar", "app.jar", "--server.port=8080", "--server.address=127.0.0.1"] -``` - -## Source - -```java -package server; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -import jakarta.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.util.Enumeration; - -@SpringBootApplication -@RestController -public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - @RequestMapping(value = "/", method = RequestMethod.GET) - public String indexGet() { - return "OK"; - } - - @RequestMapping(value = "/", method = RequestMethod.POST) - public byte[] indexPost(HttpServletRequest request) throws IOException { - return request.getInputStream().readAllBytes(); - } - - @RequestMapping("/cookie") - public ResponseEntity cookieEndpoint(HttpServletRequest request) { - StringBuilder sb = new StringBuilder(); - jakarta.servlet.http.Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (jakarta.servlet.http.Cookie c : cookies) { - sb.append(c.getName()).append("=").append(c.getValue()).append("\n"); - } - } - return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(sb.toString()); - } - - @RequestMapping("/echo") - public ResponseEntity echo(HttpServletRequest request) { - StringBuilder sb = new StringBuilder(); - Enumeration names = request.getHeaderNames(); - while (names.hasMoreElements()) { - String name = names.nextElement(); - Enumeration values = request.getHeaders(name); - while (values.hasMoreElements()) { - sb.append(name).append(": ").append(values.nextElement()).append("\n"); - } - } - return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(sb.toString()); - } -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Spring Boot" +description: "Spring Boot (Java) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Java · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/SpringBootServer) + +## Dockerfile + +```dockerfile +FROM maven:3.9-eclipse-temurin-21 AS build +WORKDIR /src +COPY src/Servers/SpringBootServer/pom.xml . +RUN mvn dependency:go-offline -q +COPY src/Servers/SpringBootServer/src/ src/ +RUN mvn package -q -DskipTests + +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=build /src/target/*.jar app.jar +ENTRYPOINT ["java", "-jar", "app.jar", "--server.port=8080", "--server.address=127.0.0.1"] +``` + +## Source + +```java +package server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Enumeration; + +@SpringBootApplication +@RestController +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @RequestMapping(value = "/", method = RequestMethod.GET) + public String indexGet() { + return "OK"; + } + + @RequestMapping(value = "/", method = RequestMethod.POST) + public byte[] indexPost(HttpServletRequest request) throws IOException { + return request.getInputStream().readAllBytes(); + } + + @RequestMapping("/cookie") + public ResponseEntity cookieEndpoint(HttpServletRequest request) { + StringBuilder sb = new StringBuilder(); + jakarta.servlet.http.Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (jakarta.servlet.http.Cookie c : cookies) { + sb.append(c.getName()).append("=").append(c.getValue()).append("\n"); + } + } + return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(sb.toString()); + } + + @RequestMapping("/echo") + public ResponseEntity echo(HttpServletRequest request) { + StringBuilder sb = new StringBuilder(); + Enumeration names = request.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + Enumeration values = request.getHeaders(name); + while (values.hasMoreElements()) { + sb.append(name).append(": ").append(values.nextElement()).append("\n"); + } + } + return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(sb.toString()); + } +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/tomcat.md b/docs/content/servers/tomcat.md index 9fe9fd8..9a7ff6c 100644 --- a/docs/content/servers/tomcat.md +++ b/docs/content/servers/tomcat.md @@ -1,131 +1,132 @@ ---- -title: "Tomcat" -toc: true -breadcrumbs: false ---- - -**Language:** Java · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/TomcatServer) - -## Dockerfile - -```dockerfile -FROM tomcat:11-jre21-temurin -RUN rm -rf /usr/local/tomcat/webapps/* -COPY src/Servers/TomcatServer/webapp/ /usr/local/tomcat/webapps/ROOT/ -EXPOSE 8080 -CMD ["catalina.sh", "run"] -``` - -## Source - -**`webapp/WEB-INF/web.xml`** - -```xml - - - - echo - /echo.jsp - - - echo - /echo - - - - cookie - /cookie.jsp - - - cookie - /cookie - - - - ok - /ok.jsp - - - ok - /* - - -``` - -**`webapp/ok.jsp`** - -```jsp -<%@page contentType="text/plain" import="java.io.*"%><% -if ("POST".equals(request.getMethod())) { - InputStream in = request.getInputStream(); - byte[] buf = in.readAllBytes(); - out.print(new String(buf, "UTF-8")); -} else { - out.print("OK"); -} -%> -``` - -**`webapp/echo.jsp`** - -```jsp -<%@page contentType="text/plain" import="java.util.*"%><% -Enumeration names = request.getHeaderNames(); -while (names.hasMoreElements()) { - String name = names.nextElement(); - Enumeration values = request.getHeaders(name); - while (values.hasMoreElements()) { - out.print(name + ": " + values.nextElement() + "\n"); - } -} -%> -``` - -**`webapp/cookie.jsp`** - -```jsp -<%@page contentType="text/plain"%><% -jakarta.servlet.http.Cookie[] cookies = request.getCookies(); -if (cookies != null) { - for (jakarta.servlet.http.Cookie c : cookies) { - out.print(c.getName() + "=" + c.getValue() + "\n"); - } -} -%> -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Tomcat" +description: "Tomcat (Java) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Java · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/TomcatServer) + +## Dockerfile + +```dockerfile +FROM tomcat:11-jre21-temurin +RUN rm -rf /usr/local/tomcat/webapps/* +COPY src/Servers/TomcatServer/webapp/ /usr/local/tomcat/webapps/ROOT/ +EXPOSE 8080 +CMD ["catalina.sh", "run"] +``` + +## Source + +**`webapp/WEB-INF/web.xml`** + +```xml + + + + echo + /echo.jsp + + + echo + /echo + + + + cookie + /cookie.jsp + + + cookie + /cookie + + + + ok + /ok.jsp + + + ok + /* + + +``` + +**`webapp/ok.jsp`** + +```jsp +<%@page contentType="text/plain" import="java.io.*"%><% +if ("POST".equals(request.getMethod())) { + InputStream in = request.getInputStream(); + byte[] buf = in.readAllBytes(); + out.print(new String(buf, "UTF-8")); +} else { + out.print("OK"); +} +%> +``` + +**`webapp/echo.jsp`** + +```jsp +<%@page contentType="text/plain" import="java.util.*"%><% +Enumeration names = request.getHeaderNames(); +while (names.hasMoreElements()) { + String name = names.nextElement(); + Enumeration values = request.getHeaders(name); + while (values.hasMoreElements()) { + out.print(name + ": " + values.nextElement() + "\n"); + } +} +%> +``` + +**`webapp/cookie.jsp`** + +```jsp +<%@page contentType="text/plain"%><% +jakarta.servlet.http.Cookie[] cookies = request.getCookies(); +if (cookies != null) { + for (jakarta.servlet.http.Cookie c : cookies) { + out.print(c.getName() + "=" + c.getValue() + "\n"); + } +} +%> +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/traefik.md b/docs/content/servers/traefik.md index d7c3de8..6d82e1f 100644 --- a/docs/content/servers/traefik.md +++ b/docs/content/servers/traefik.md @@ -1,176 +1,177 @@ ---- -title: "Traefik" -toc: true -breadcrumbs: false ---- - -**Language:** Go · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/TraefikServer) - -## Dockerfile - -```dockerfile -FROM alpine:3.20 AS plugin -RUN apk add --no-cache git -RUN git clone https://github.com/jdel/staticresponse.git /plugin - -FROM golang:1.23-alpine AS echo-build -WORKDIR /build -COPY src/Servers/TraefikServer/echo/main.go main.go -RUN go build -o /echo-server main.go - -FROM traefik:v3.2 -COPY --from=plugin /plugin /plugins-local/src/github.com/jdel/staticresponse/ -COPY --from=echo-build /echo-server /usr/local/bin/echo-server -COPY src/Servers/TraefikServer/traefik.yml /etc/traefik/traefik.yml -COPY src/Servers/TraefikServer/dynamic.yml /etc/traefik/dynamic.yml -COPY src/Servers/TraefikServer/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh -ENTRYPOINT ["/entrypoint.sh"] -``` - -## Source - -**`traefik.yml`** - -```yaml -entryPoints: - web: - address: ":8080" - -providers: - file: - filename: /etc/traefik/dynamic.yml - -experimental: - localPlugins: - staticresponse: - moduleName: github.com/jdel/staticresponse -``` - -**`dynamic.yml`** - -```yaml -http: - routers: - echo: - rule: "Path(`/echo`)" - entryPoints: - - web - service: echo-svc - - cookie: - rule: "Path(`/cookie`)" - entryPoints: - - web - service: echo-svc - - catchall: - rule: "PathPrefix(`/`)" - entryPoints: - - web - middlewares: - - static-ok - service: noop@internal - - services: - echo-svc: - loadBalancer: - servers: - - url: "http://127.0.0.1:9090" - - middlewares: - static-ok: - plugin: - staticresponse: - statusCode: 200 - body: "OK" -``` - -**`echo/main.go`** - -```go -package main - -import ( - "io" - "net/http" - "strings" -) - -func main() { - http.HandleFunc("/cookie", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - raw := r.Header.Get("Cookie") - for _, pair := range strings.Split(raw, ";") { - pair = strings.TrimLeft(pair, " ") - if eq := strings.Index(pair, "="); eq > 0 { - w.Write([]byte(pair[:eq] + "=" + pair[eq+1:] + "\n")) - } - } - }) - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return - } - - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "Failed to read body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - w.Write(body) - }) - - http.ListenAndServe(":9090", nil) -} -``` - -**`entrypoint.sh`** - -```bash -#!/bin/sh -/usr/local/bin/echo-server & -exec traefik "$@" -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Traefik" +description: "Traefik (Go) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Go · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/TraefikServer) + +## Dockerfile + +```dockerfile +FROM alpine:3.20 AS plugin +RUN apk add --no-cache git +RUN git clone https://github.com/jdel/staticresponse.git /plugin + +FROM golang:1.23-alpine AS echo-build +WORKDIR /build +COPY src/Servers/TraefikServer/echo/main.go main.go +RUN go build -o /echo-server main.go + +FROM traefik:v3.2 +COPY --from=plugin /plugin /plugins-local/src/github.com/jdel/staticresponse/ +COPY --from=echo-build /echo-server /usr/local/bin/echo-server +COPY src/Servers/TraefikServer/traefik.yml /etc/traefik/traefik.yml +COPY src/Servers/TraefikServer/dynamic.yml /etc/traefik/dynamic.yml +COPY src/Servers/TraefikServer/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] +``` + +## Source + +**`traefik.yml`** + +```yaml +entryPoints: + web: + address: ":8080" + +providers: + file: + filename: /etc/traefik/dynamic.yml + +experimental: + localPlugins: + staticresponse: + moduleName: github.com/jdel/staticresponse +``` + +**`dynamic.yml`** + +```yaml +http: + routers: + echo: + rule: "Path(`/echo`)" + entryPoints: + - web + service: echo-svc + + cookie: + rule: "Path(`/cookie`)" + entryPoints: + - web + service: echo-svc + + catchall: + rule: "PathPrefix(`/`)" + entryPoints: + - web + middlewares: + - static-ok + service: noop@internal + + services: + echo-svc: + loadBalancer: + servers: + - url: "http://127.0.0.1:9090" + + middlewares: + static-ok: + plugin: + staticresponse: + statusCode: 200 + body: "OK" +``` + +**`echo/main.go`** + +```go +package main + +import ( + "io" + "net/http" + "strings" +) + +func main() { + http.HandleFunc("/cookie", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + raw := r.Header.Get("Cookie") + for _, pair := range strings.Split(raw, ";") { + pair = strings.TrimLeft(pair, " ") + if eq := strings.Index(pair, "="); eq > 0 { + w.Write([]byte(pair[:eq] + "=" + pair[eq+1:] + "\n")) + } + } + }) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write(body) + }) + + http.ListenAndServe(":9090", nil) +} +``` + +**`entrypoint.sh`** + +```bash +#!/bin/sh +/usr/local/bin/echo-server & +exec traefik "$@" +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/trillium.md b/docs/content/servers/trillium.md index 2514c99..0a0b575 100644 --- a/docs/content/servers/trillium.md +++ b/docs/content/servers/trillium.md @@ -1,111 +1,112 @@ ---- -title: "Trillium" -toc: true -breadcrumbs: false ---- - -**Language:** Rust · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/TrilliumServer) - -## Dockerfile - -```dockerfile -FROM rust:1-slim AS build -WORKDIR /src - -# Cache dependencies with dummy main -COPY src/Servers/TrilliumServer/Cargo.toml . -RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src target/release/.fingerprint/trillium-server-* - -COPY src/Servers/TrilliumServer/src/ src/ -RUN cargo build --release - -FROM debian:bookworm-slim -COPY --from=build /src/target/release/trillium-server /usr/local/bin/ -ENV HOST=0.0.0.0 PORT=8080 -ENTRYPOINT ["trillium-server"] -``` - -## Source - -```rust -use std::fmt::Write; -use trillium::{Conn, Handler, Method}; -use trillium_caching_headers::caching_headers; -use trillium_cookies::{CookiesConnExt, cookies}; -use trillium_head::head; -use trillium_router::router; - -async fn echo_body(mut conn: Conn) -> Conn { - match conn.request_body().read_bytes().await { - Ok(bytes) => conn.ok(bytes), - Err(_) => conn.with_status(400), - } -} - -async fn echo_headers(conn: Conn) -> Conn { - let body = conn.request_headers().to_string(); - conn.ok(body) -} - -async fn echo_cookies(conn: Conn) -> Conn { - let mut body = String::new(); - for cookie in conn.cookies().iter() { - let _ = writeln!(body, "{}={}", cookie.name(), cookie.value()); - } - conn.ok(body) -} - -fn app() -> impl Handler { - ( - head(), - caching_headers(), - cookies(), - router() - .get("/", "OK") - .post("/", echo_body) - .any(&[Method::Get, Method::Post], "/echo", echo_headers) - .any(&[Method::Get, Method::Post], "/cookie", echo_cookies) - .with_method_not_allowed(), - ) -} - -fn main() { - trillium_smol::run(app()); -} -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Trillium" +description: "Trillium (Rust) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Rust · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/TrilliumServer) + +## Dockerfile + +```dockerfile +FROM rust:1-slim AS build +WORKDIR /src + +# Cache dependencies with dummy main +COPY src/Servers/TrilliumServer/Cargo.toml . +RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src target/release/.fingerprint/trillium-server-* + +COPY src/Servers/TrilliumServer/src/ src/ +RUN cargo build --release + +FROM debian:bookworm-slim +COPY --from=build /src/target/release/trillium-server /usr/local/bin/ +ENV HOST=0.0.0.0 PORT=8080 +ENTRYPOINT ["trillium-server"] +``` + +## Source + +```rust +use std::fmt::Write; +use trillium::{Conn, Handler, Method}; +use trillium_caching_headers::caching_headers; +use trillium_cookies::{CookiesConnExt, cookies}; +use trillium_head::head; +use trillium_router::router; + +async fn echo_body(mut conn: Conn) -> Conn { + match conn.request_body().read_bytes().await { + Ok(bytes) => conn.ok(bytes), + Err(_) => conn.with_status(400), + } +} + +async fn echo_headers(conn: Conn) -> Conn { + let body = conn.request_headers().to_string(); + conn.ok(body) +} + +async fn echo_cookies(conn: Conn) -> Conn { + let mut body = String::new(); + for cookie in conn.cookies().iter() { + let _ = writeln!(body, "{}={}", cookie.name(), cookie.value()); + } + conn.ok(body) +} + +fn app() -> impl Handler { + ( + head(), + caching_headers(), + cookies(), + router() + .get("/", "OK") + .post("/", echo_body) + .any(&[Method::Get, Method::Post], "/echo", echo_headers) + .any(&[Method::Get, Method::Post], "/cookie", echo_cookies) + .with_method_not_allowed(), + ) +} + +fn main() { + trillium_smol::run(app()); +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/uvicorn.md b/docs/content/servers/uvicorn.md index cc9dc28..b583bcf 100644 --- a/docs/content/servers/uvicorn.md +++ b/docs/content/servers/uvicorn.md @@ -1,120 +1,121 @@ ---- -title: "Uvicorn" -toc: true -breadcrumbs: false ---- - -**Language:** Python · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/UvicornServer) - -## Dockerfile - -```dockerfile -FROM python:3.13-slim -RUN pip install --no-cache-dir 'uvicorn[standard]' -COPY src/Servers/UvicornServer/app.py /app/app.py -WORKDIR /app -EXPOSE 8080 -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"] -``` - -## Source - -```python -async def app(scope, receive, send): - path = scope.get('path', '/') - - if path == '/cookie': - cookie_val = '' - for name, value in scope.get('headers', []): - if name.lower() == b'cookie': - cookie_val = value.decode('latin-1') - break - lines = [] - for pair in cookie_val.split(';'): - pair = pair.strip() - eq = pair.find('=') - if eq > 0: - lines.append(f"{pair[:eq]}={pair[eq+1:]}") - body = ('\n'.join(lines) + '\n').encode('utf-8') if lines else b'' - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [(b'content-type', b'text/plain')], - }) - await send({ - 'type': 'http.response.body', - 'body': body, - }) - return - - if path == '/echo': - lines = [] - for name, value in scope.get('headers', []): - lines.append(f"{name.decode('latin-1')}: {value.decode('latin-1')}") - body = ('\n'.join(lines) + '\n').encode('utf-8') - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [(b'content-type', b'text/plain')], - }) - await send({ - 'type': 'http.response.body', - 'body': body, - }) - return - - body = b'OK' - if scope.get('method') == 'POST': - chunks = [] - while True: - msg = await receive() - chunks.append(msg.get('body', b'')) - if not msg.get('more_body', False): - break - body = b''.join(chunks) - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [(b'content-type', b'text/plain')], - }) - await send({ - 'type': 'http.response.body', - 'body': body, - }) -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Uvicorn" +description: "Uvicorn (Python) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** Python · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/UvicornServer) + +## Dockerfile + +```dockerfile +FROM python:3.13-slim +RUN pip install --no-cache-dir 'uvicorn[standard]' +COPY src/Servers/UvicornServer/app.py /app/app.py +WORKDIR /app +EXPOSE 8080 +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"] +``` + +## Source + +```python +async def app(scope, receive, send): + path = scope.get('path', '/') + + if path == '/cookie': + cookie_val = '' + for name, value in scope.get('headers', []): + if name.lower() == b'cookie': + cookie_val = value.decode('latin-1') + break + lines = [] + for pair in cookie_val.split(';'): + pair = pair.strip() + eq = pair.find('=') + if eq > 0: + lines.append(f"{pair[:eq]}={pair[eq+1:]}") + body = ('\n'.join(lines) + '\n').encode('utf-8') if lines else b'' + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [(b'content-type', b'text/plain')], + }) + await send({ + 'type': 'http.response.body', + 'body': body, + }) + return + + if path == '/echo': + lines = [] + for name, value in scope.get('headers', []): + lines.append(f"{name.decode('latin-1')}: {value.decode('latin-1')}") + body = ('\n'.join(lines) + '\n').encode('utf-8') + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [(b'content-type', b'text/plain')], + }) + await send({ + 'type': 'http.response.body', + 'body': body, + }) + return + + body = b'OK' + if scope.get('method') == 'POST': + chunks = [] + while True: + msg = await receive() + chunks.append(msg.get('body', b'')) + if not msg.get('more_body', False): + break + body = b''.join(chunks) + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [(b'content-type', b'text/plain')], + }) + await send({ + 'type': 'http.response.body', + 'body': body, + }) +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/workerman.md b/docs/content/servers/workerman.md index 6489626..6aca3ee 100644 --- a/docs/content/servers/workerman.md +++ b/docs/content/servers/workerman.md @@ -1,112 +1,113 @@ ---- -title: "Workerman" -toc: true -breadcrumbs: false ---- - -**Language:** PHP · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/WorkermanServer) - -## Dockerfile - -```dockerfile -FROM php:8.5-cli - -COPY --from=composer/composer:latest-bin --link /composer /usr/local/bin/composer - -RUN docker-php-ext-install pcntl > /dev/null -RUN apt-get update -yqq && apt-get install -yqq git unzip > /dev/null && rm -rf /var/lib/apt/lists/* - -WORKDIR /workerman -COPY src/Servers/WorkermanServer/composer.json . -RUN composer install --optimize-autoloader --classmap-authoritative --no-dev --quiet -COPY src/Servers/WorkermanServer/php.ini /etc/php/8.5/cli/php.ini - -COPY src/Servers/WorkermanServer/start.php . - -EXPOSE 8080 - -CMD ["php", "/workerman/start.php", "start"] -``` - -## Source - -```php -reusePort = true; -$http_worker->count = (int) shell_exec('nproc'); -$http_worker->name = 'bench'; - -// Data received -$http_worker->onMessage = static function ($connection, $request) { - - return match($request->path()) { - - '/echo' => $connection->send( new Response( - 200, - ['Content-Type' => 'text/plain'], - implode("\n", array_map(fn($name, $value) => "$name: $value", $request->header(), $request->header()))) - ), - - '/cookie' => $connection->send( new Response( - 200, - ['Content-Type' => 'text/plain'], - implode("\n", array_map(fn($name, $value) => "$name=$value", $request->cookie(), $request->cookie()))) - ), - - '/' => $connection->send( new Response( - 200, - ['Content-Type' => 'text/plain'], - $request->method() === 'POST' ? $request->rawBody() : 'OK') - ), - - default => null, - }; -}; - -// Run all workers -Worker::runAll(); -``` - -## Test Results - -

Loading results...

- -### Compliance - -
- -### Smuggling - -
- -### Malformed Input - -
- -### Caching - -
- -### Cookies - -
- - - - +--- +title: "Workerman" +description: "Workerman (PHP) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** PHP · [View source on GitHub](https://github.com/MDA2AV/tree/main/src/Servers/WorkermanServer) + +## Dockerfile + +```dockerfile +FROM php:8.5-cli + +COPY --from=composer/composer:latest-bin --link /composer /usr/local/bin/composer + +RUN docker-php-ext-install pcntl > /dev/null +RUN apt-get update -yqq && apt-get install -yqq git unzip > /dev/null && rm -rf /var/lib/apt/lists/* + +WORKDIR /workerman +COPY src/Servers/WorkermanServer/composer.json . +RUN composer install --optimize-autoloader --classmap-authoritative --no-dev --quiet +COPY src/Servers/WorkermanServer/php.ini /etc/php/8.5/cli/php.ini + +COPY src/Servers/WorkermanServer/start.php . + +EXPOSE 8080 + +CMD ["php", "/workerman/start.php", "start"] +``` + +## Source + +```php +reusePort = true; +$http_worker->count = (int) shell_exec('nproc'); +$http_worker->name = 'bench'; + +// Data received +$http_worker->onMessage = static function ($connection, $request) { + + return match($request->path()) { + + '/echo' => $connection->send( new Response( + 200, + ['Content-Type' => 'text/plain'], + implode("\n", array_map(fn($name, $value) => "$name: $value", $request->header(), $request->header()))) + ), + + '/cookie' => $connection->send( new Response( + 200, + ['Content-Type' => 'text/plain'], + implode("\n", array_map(fn($name, $value) => "$name=$value", $request->cookie(), $request->cookie()))) + ), + + '/' => $connection->send( new Response( + 200, + ['Content-Type' => 'text/plain'], + $request->method() === 'POST' ? $request->rawBody() : 'OK') + ), + + default => null, + }; +}; + +// Run all workers +Worker::runAll(); +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/smuggling/_index.md b/docs/content/smuggling/_index.md index c841aac..2892399 100644 --- a/docs/content/smuggling/_index.md +++ b/docs/content/smuggling/_index.md @@ -1,5 +1,6 @@ --- title: Smuggling +description: "HTTP request smuggling test results across servers, covering Content-Length/Transfer-Encoding conflicts, obfuscated framing, and pipeline injection vectors." layout: wide toc: false --- diff --git a/docs/content/websockets/_index.md b/docs/content/websockets/_index.md index 97454c1..fa32c99 100644 --- a/docs/content/websockets/_index.md +++ b/docs/content/websockets/_index.md @@ -1,5 +1,6 @@ --- title: WebSockets +description: "How HTTP/1.1 servers validate the WebSocket Upgrade handshake, per RFC 9110 Section 7.8 and RFC 6455, including malformed and unsupported upgrade requests." layout: wide toc: false ---