Harden chunked encoding parsing - #68465
Conversation
e128d12 to
462697e
Compare
4615157 to
6be36e9
Compare
6be36e9 to
92da644
Compare
There was a problem hiding this comment.
Pull request overview
This PR hardens Kestrel’s HTTP/1.1 chunked transfer-encoding parsing to strictly reject invalid chunk extensions (notably CR/LF inside chunk-ext, including quoted-string cases) per RFC 9110/9112, and updates tests accordingly. It also removes the legacy insecure chunked parsing AppContext switch from the mainline implementation.
Changes:
- Tighten
Http1ChunkedEncodingMessageBodychunk-size and chunk-extension parsing (including stricter token/quoted-string validation and CRLF handling). - Expand/adjust chunked request test coverage for invalid/valid extension forms and boundary splitting.
- Update existing tests’ chunk-extension strings to avoid now-invalid characters (e.g., spaces in tokens).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs | Reworks chunk-size and chunk-ext parsing to be stricter and RFC-aligned (including quoted-string rules). |
| src/Servers/Kestrel/Core/test/MessageBodyTests.cs | Adds new tests for incomplete chunk-extension/value parsing and max hex digit chunk-size parsing. |
| src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs | Expands valid/invalid chunk extension theory cases; updates extension examples; adjusts RemoteExecutor options. |
| src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs | Updates chunk-extension strings in payloads to remain valid under stricter parsing. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
92da644 to
ff7097f
Compare
cincuranet
left a comment
There was a problem hiding this comment.
With MaxRequestBodySize = 10 and
Transfer-Encoding: chunked\r\n
\r\n
5;a="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
the 413 is not returned.
With 5;a="aaaaaaaaaaaa... ← 1 MB of 'a', no closing quote, no CRLF, written in 64-byte segments, because the code never advances consumed until a whole ;name[=value] unit parses, every arrival re-scans the entire pending extension. Serious CPU burn. The quoted variant is worst because it's parsed byte-at-a-time through a Func<byte,bool> delegate.
Finally, I'm missing tests for stuff like 2;a="x\r\ny"\r\n and 2;a="x\<CR>"\r\n.
| [InlineData("2;novalue\r\nxy\r\n0")] // Name only chunk extension | ||
| //[InlineData("2 ;\r\nxy\r\n0")] // Technically allowed per spec, but we never supported it, and no one should be sending it |
There was a problem hiding this comment.
Do we want to allow this now?
There was a problem hiding this comment.
I'm implementing the spec completely and trying to be 100% compliant. I think that's best.
Let me see what's the best way to handle this. I guess we will need more "internal" modes to allow us to track where we were at in parsing extensions and then we can consume things more frequently. |
DeagleGross
left a comment
There was a problem hiding this comment.
Jiri raised a great point about code re-reading the whole payload on the next iteration; but I think we can do in the follow-up PR - this PR can focus on char rejection only. If you decide to do in follow-up - lets create a separate issue to track this?
If i would be doing this, i would create a struct that holds mode, readOffset (which keeps track of how much data we already parsed from the input buffer), and chunkSize which does not require us re-reading the first byte to determine size. Then you can continue from last not read byte.
| @@ -28,8 +38,6 @@ internal sealed class Http1ChunkedEncodingMessageBody : Http1MessageBody | |||
| private readonly Pipe _requestBodyPipe; | |||
| private ReadResult _readResult; | |||
|
|
|||
| private static readonly bool InsecureChunkedParsing = AppContext.TryGetSwitch("Microsoft.AspNetCore.Server.Kestrel.EnableInsecureChunkedRequestParsing", out var value) && value; | |||
There was a problem hiding this comment.
I think we cant remove this context switch - @BrennanConroy documented it in the security doc.
There was a problem hiding this comment.
Doc getting updated in dotnet/AspNetCore.Docs#37484
| // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" | ||
| // / DIGIT / ALPHA | ||
| // ; any VCHAR, except delimiters | ||
| private static ReadOnlySpan<byte> s_tchar => "!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"u8; |
There was a problem hiding this comment.
you can reference
IndexOfInvalidTokenChar() already gives you vectorized lookup.
Fixes #66794