Skip to content

Commit 95e58df

Browse files
MDA2AVclaude
andcommitted
Downgrade FRAGMENT-IN-TARGET from MUST to SHOULD (warn_on_2xx)
Fragment (#) is absent from origin-form grammar but RFC 9112 §3 only says SHOULD reject invalid request-lines. Servers that strip the fragment and return 2xx are not violating a mandatory requirement. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1de8543 commit 95e58df

2 files changed

Lines changed: 32 additions & 9 deletions

File tree

docs/content/docs/request-line/fragment-in-target.md

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,43 @@ weight: 3
1010
| **Category** | Compliance |
1111
| **RFC** | [RFC 9112 Section 3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) |
1212
| **Requirement** | SHOULD |
13-
| **Expected** | `400` or close |
13+
| **Expected** | `400` = Pass; `2xx` = Warn |
1414

1515
## What it sends
1616

17-
A request with a fragment identifier in the URI: `GET /path#fragment HTTP/1.1`.
17+
A request with a fragment identifier in the URI: `GET /path#frag HTTP/1.1`.
1818

1919
```http
2020
GET /path#frag HTTP/1.1\r\n
2121
Host: localhost:8080\r\n
2222
\r\n
2323
```
2424

25-
2625
## What the RFC says
2726

28-
The origin-form of request-target is `absolute-path [ "?" query ]`. There is no fragment component. A fragment identifier (`#...`) does not appear in any valid request-target form.
27+
The origin-form of request-target is defined as:
28+
29+
```
30+
origin-form = absolute-path [ "?" query ]
31+
```
32+
33+
There is no fragment component in this grammar. The `#` character and anything after it are not part of any valid request-target form (origin-form, absolute-form, authority-form, or asterisk-form).
34+
35+
Since the request-line doesn't match any valid form, it is an invalid request-line:
2936

30-
> "Recipients of an invalid request-line **SHOULD** respond with either a 400 (Bad Request) error..." — RFC 9112 Section 3
37+
> "Recipients of an invalid request-line **SHOULD** respond with either a 400 (Bad Request) error or a 301 (Moved Permanently) redirect..." — RFC 9112 Section 3
38+
39+
This is a **SHOULD**, not a MUST — servers that strip the fragment and process the path are not violating a mandatory requirement.
3140

3241
## Why it matters
3342

34-
Fragments are a client-side concept (they 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.
43+
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.
44+
45+
**Pass:** Server rejects with `400` (strict parsing).
46+
**Warn:** Server returns `2xx` (likely strips the fragment and processes `/path`).
3547

3648
## Sources
3749

3850
- [RFC 9112 Section 3.2 — origin-form](https://www.rfc-editor.org/rfc/rfc9112#section-3.2)
51+
- [RFC 9112 Section 3 — Request Line](https://www.rfc-editor.org/rfc/rfc9112#section-3)
3952
- [RFC 9110 Section 4.1 — URI References](https://www.rfc-editor.org/rfc/rfc9110#section-4.1)

src/Http11Probe/TestCases/Suites/ComplianceSuite.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,24 @@ public static IEnumerable<TestCase> GetTestCases()
175175
yield return new TestCase
176176
{
177177
Id = "RFC9112-3.2-FRAGMENT-IN-TARGET",
178-
Description = "Fragment (#) in request-target must be rejected",
178+
Description = "Fragment (#) in request-target — not part of origin-form grammar",
179179
Category = TestCategory.Compliance,
180180
RfcReference = "RFC 9112 §3.2",
181181
PayloadFactory = ctx => MakeRequest($"GET /path#frag HTTP/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"),
182182
Expected = new ExpectedBehavior
183183
{
184-
ExpectedStatus = StatusCodeRange.Exact(400),
185-
AllowConnectionClose = true
184+
Description = "400 or 2xx",
185+
CustomValidator = (response, state) =>
186+
{
187+
if (response is null)
188+
return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail;
189+
if (response.StatusCode == 400)
190+
return TestVerdict.Pass;
191+
// Fragment not in origin-form grammar, but RFC only says SHOULD reject invalid request-line
192+
if (response.StatusCode is >= 200 and < 300)
193+
return TestVerdict.Warn;
194+
return TestVerdict.Fail;
195+
}
186196
}
187197
};
188198

0 commit comments

Comments
 (0)