This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch 1.4.x
in repository https://gitbox.apache.org/repos/asf/pekko-http.git


The following commit(s) were added to refs/heads/1.4.x by this push:
     new 54f50c329 parse Content-Length as digits with surrounding whitespace 
only (#1221) (#1243)
54f50c329 is described below

commit 54f50c329d6278de99f3f29caac3a87b8c297963
Author: PJ Fanning <[email protected]>
AuthorDate: Thu Aug 27 18:59:43 2026 +0100

    parse Content-Length as digits with surrounding whitespace only (#1221) 
(#1243)
    
    * parse Content-Length as digits with surrounding whitespace only (#1221)
    
    Motivation:
    `ContentLengthParser` skipped whitespace anywhere in the field value, so
    `Content-Length: 1 2` was read as 12 and `Content-Length: 5 5` as 55, and
    an empty value was read as 0. The field value is `1*DIGIT` surrounded by
    optional whitespace (RFC 9110, section 8.6 and RFC 9112, section 5). Every
    other length ambiguity is rejected by this parser already: two differing
    Content-Length headers, a Transfer-Encoding other than a single chunked,
    and chunked together with a Content-Length. This was the remaining spot
    where pekko-http could read a body length that another implementation in
    the request path reads differently or rejects.
    
    Modification:
    Skip whitespace before and after the digits, but require at least one digit
    and stop the value at the first non-digit.
    
    Result:
    A Content-Length value with whitespace between digits, or without any
    digit, is rejected with the "Illegal `Content-Length` header value" error
    that other malformed values already produce. Values with leading or
    trailing whitespace keep parsing as before.
    
    Note this rejects two inputs that were accepted before: whitespace inside
    the digits, and an empty value that was read as 0.
    
    Tests:
    - sbt "http-core/testOnly 
org.apache.pekko.http.impl.engine.parsing.RequestParserCRLFSpec 
org.apache.pekko.http.impl.engine.parsing.RequestParserLFSpec 
org.apache.pekko.http.impl.engine.parsing.ResponseParserSpec" - pass, 2 new 
tests that both fail without the change; leading whitespace stays covered by 
the existing "Content-length:    17" tests
    - sbt http-core/test - pass
    - sbt http-tests/test - pass
    - sbt http-core/mimaReportBinaryIssues - pass
    - sbt http-core/scalafmt http-core/Test/scalafmt - clean
    
    References:
    None - tightens Content-Length parsing to the grammar
    
    * backport overflow test from #1049 for the Content-Length parser
    
    #1221 rewrites the same lines that #1049 changed, so cherry picking it
    onto 1.4.x brings the pre-multiply overflow check along with it. #1049
    was never backported, so its test came too: a value that wraps back to a
    small positive Long, which the old `result < 0` check did not catch.
    
    SpecializedHeaderValueParsers.scala and ContentLengthHeaderParserSpec.scala
    are now identical to main.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../parsing/SpecializedHeaderValueParsers.scala    | 26 +++++++++++++++++-----
 .../parsing/ContentLengthHeaderParserSpec.scala    |  2 ++
 .../impl/engine/parsing/RequestParserSpec.scala    | 14 ++++++++++++
 3 files changed, 36 insertions(+), 6 deletions(-)

diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/SpecializedHeaderValueParsers.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/SpecializedHeaderValueParsers.scala
index 3b2977217..0e0cec6f0 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/SpecializedHeaderValueParsers.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/SpecializedHeaderValueParsers.scala
@@ -33,18 +33,32 @@ private[parsing] object SpecializedHeaderValueParsers {
   def specializedHeaderValueParsers = Seq(ContentLengthParser)
 
   object ContentLengthParser extends HeaderValueParser("Content-Length", 
maxValueCount = 1) {
+    // The field value is `1*DIGIT`, surrounded by optional whitespace (RFC 
9110, section 8.6 and RFC 9112,
+    // section 5). Whitespace within the digits must not be skipped: a value 
like `1 2` would then be read as `12`
+    // here while another implementation in the request path rejects it or 
reads it as `1`.
     def apply(hhp: HttpHeaderParser, input: ByteString, valueStart: Int, 
onIllegalHeader: ErrorInfo => Unit)
         : (HttpHeader, Int) = {
-      @tailrec def recurse(ix: Int = valueStart, result: Long = 0): 
(HttpHeader, Int) = {
+      @tailrec def skipWhitespace(ix: Int): Int = if (WSP(byteChar(input, 
ix))) skipWhitespace(ix + 1) else ix
+
+      @tailrec def digits(ix: Int, result: Long, seenDigit: Boolean): 
(HttpHeader, Int) = {
         val c = byteChar(input, ix)
-        if (result < 0) fail("`Content-Length` header value must not exceed 
63-bit integer range")
-        else if (DIGIT(c)) recurse(ix + 1, result * 10 + c - '0')
-        else if (WSP(c)) recurse(ix + 1, result)
-        else if (c == '\r' && byteAt(input, ix + 1) == LF_BYTE) 
(`Content-Length`(result), ix + 2)
+        if (DIGIT(c)) {
+          val digit = c - '0'
+          if (result > (Long.MaxValue - digit) / 10)
+            fail("`Content-Length` header value must not exceed 63-bit integer 
range")
+          else digits(ix + 1, result * 10 + digit, seenDigit = true)
+        } else if (!seenDigit) fail("Illegal `Content-Length` header value")
+        else lineEnd(skipWhitespace(ix), result)
+      }
+
+      def lineEnd(ix: Int, result: Long): (HttpHeader, Int) = {
+        val c = byteChar(input, ix)
+        if (c == '\r' && byteAt(input, ix + 1) == LF_BYTE) 
(`Content-Length`(result), ix + 2)
         else if (c == '\n') (`Content-Length`(result), ix + 1)
         else fail("Illegal `Content-Length` header value")
       }
-      recurse()
+
+      digits(skipWhitespace(valueStart), 0, seenDigit = false)
     }
   }
 }
diff --git 
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/ContentLengthHeaderParserSpec.scala
 
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/ContentLengthHeaderParserSpec.scala
index 1671d6249..2e28d92e4 100644
--- 
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/ContentLengthHeaderParserSpec.scala
+++ 
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/ContentLengthHeaderParserSpec.scala
@@ -37,6 +37,8 @@ abstract class ContentLengthHeaderParserSpec(mode: String, 
newLine: String) exte
       a[ParsingException] should be thrownBy parse("9223372036854775808") // 
Long.MaxValue + 1
       a[ParsingException] should be thrownBy parse("92233720368547758070") // 
Long.MaxValue * 10 which is 0 taken overflow into account
       a[ParsingException] should be thrownBy parse("92233720368547758080") // 
(Long.MaxValue + 1) * 10 which is 0 taken overflow into account
+      // overflow that wraps to a small positive value (was not caught by the 
old `result < 0` check)
+      a[ParsingException] should be thrownBy parse("18446744073709551634") // 
~2^64+18, wraps to 18 in signed Long
     }
   }
 
diff --git 
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/RequestParserSpec.scala
 
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/RequestParserSpec.scala
index f7467a49e..a0fb3dc5b 100644
--- 
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/RequestParserSpec.scala
+++ 
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/RequestParserSpec.scala
@@ -647,6 +647,20 @@ abstract class RequestParserSpec(mode: String, newLine: 
String) extends AnyFreeS
           |abc""" should parseToError(BadRequest, ErrorInfo("Illegal 
`Content-Length` header value"))
       }
 
+      "with whitespace inside the Content-Length header value" in new Test {
+        """GET / HTTP/1.0
+          |Content-Length: 1 2
+          |
+          |abc""" should parseToError(BadRequest, ErrorInfo("Illegal 
`Content-Length` header value"))
+      }
+
+      "with an empty Content-Length header value" in new Test {
+        """GET / HTTP/1.0
+          |Content-Length:
+          |
+          |abc""" should parseToError(BadRequest, ErrorInfo("Illegal 
`Content-Length` header value"))
+      }
+
       "with Content-Length > Long.MaxSize" in new Test {
         // content-length = (Long.MaxValue + 1) * 10, which is 0 when 
calculated overflow
         """PUT /resource/yes HTTP/1.1


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to