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 883cf8ffe..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,21 +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 (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 recurse(ix + 1, result * 10 + digit) - } else if (WSP(c)) recurse(ix + 1, result) - else if (c == '\r' && byteAt(input, ix + 1) == LF_BYTE) (`Content-Length`(result), ix + 2) + 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/RequestParserSpec.scala b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/RequestParserSpec.scala index 53ff54e85..d7e903e03 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 @@ -670,6 +670,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