From c5d23e0df3963b778612b0bc8d485642a5764015 Mon Sep 17 00:00:00 2001 From: mohammadmseet-hue Date: Thu, 30 Apr 2026 00:52:02 -0700 Subject: [PATCH] fix: HTTP request smuggling via Transfer-Encoding token whitespace Closes https://github.com/dart-lang/sdk/pull/63250 GitOrigin-RevId: 7c6b7bf7d033dd31dc23a7cc3a30b3dc4db41d16 Change-Id: I573328660bbef7ec9cd829ec376fcee894a6bc34 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/498420 Reviewed-by: Slava Egorov Commit-Queue: Slava Egorov --- sdk/lib/_http/http_parser.dart | 27 ++++++++++++++++------- tests/standalone/io/http_parser_test.dart | 21 ++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/sdk/lib/_http/http_parser.dart b/sdk/lib/_http/http_parser.dart index f960feb2b63..9b6a1c0f35b 100644 --- a/sdk/lib/_http/http_parser.dart +++ b/sdk/lib/_http/http_parser.dart @@ -1112,20 +1112,31 @@ class _HttpParser extends Stream<_HttpIncoming> { value.length = length; } + static bool _isOWS(int c) { + return c == _CharCode.SP || c == _CharCode.HT; + } + static List _tokenizeFieldValue(String headerValue) { + int length = headerValue.length; List tokens = []; + // First non-OWS character of the token. Note that headerValue is already + // trimmed from start and end so we don't need to skip characters at start. int start = 0; - int index = 0; - while (index < headerValue.length) { - if (headerValue[index] == ",") { - tokens.add(headerValue.substring(start, index)); - start = index + 1; - } else if (headerValue[index] == " " || headerValue[index] == "\t") { + while (start < length) { + int end = start + 1; + while (end < length && headerValue.codeUnitAt(end) != _CharCode.COMMA) { + end++; + } + int comma = end; + while (start < end && _isOWS(headerValue.codeUnitAt(end - 1))) { + end--; + } + tokens.add(headerValue.substring(start, end)); + start = comma + 1; + while (start < length && _isOWS(headerValue.codeUnitAt(start))) { start++; } - index++; } - tokens.add(headerValue.substring(start, index)); return tokens; } diff --git a/tests/standalone/io/http_parser_test.dart b/tests/standalone/io/http_parser_test.dart index 2f3ad86eae1..a9c25b0d90e 100644 --- a/tests/standalone/io/http_parser_test.dart +++ b/tests/standalone/io/http_parser_test.dart @@ -718,6 +718,27 @@ Content-Length: 38\r GET /admin HTTP/1.1\r\nHost: backend\r\n\r\n"""; _testParseInvalidRequest(request); + // RFC 7230 section 3.2.6 forbids SP/HT inside a token. With the tokenizer + // treating "x chunked" as a single token (not "chunked"), the request has + // a Transfer-Encoding header whose final coding is not chunked, so it + // MUST be rejected (RFC 7230 section 3.3.3 rule 3) rather than falling + // back to Content-Length. + request = """ +POST /test HTTP/1.1\r +Content-Length: 10\r +Transfer-Encoding: x chunked\r +\r +0123456789"""; + _testParseInvalidRequest(request); + + request = """ +POST /test HTTP/1.1\r +Content-Length: 10\r +Transfer-Encoding: x\tchunked\r +\r +0123456789"""; + _testParseInvalidRequest(request); + // Content-Length and "Transfer-Encoding: chunked" are specified. request = """ POST /test HTTP/1.1\r