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]