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 3b2977217b..0e0cec6f09 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 1671d62492..2e28d92e45 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 f7467a49e1..a0fb3dc5bf 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