Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -305,23 +305,29 @@ private[http] trait HttpMessageParser[Output >: MessageOutput <: ParserOutput] {
} else failEntityStream(
s"HTTP chunk extension length exceeds configured limit of ${settings.maxChunkExtLength} characters")

@tailrec def parseSize(cursor: Int, size: Long): StateResult =
// `sawWhitespace` records that the size digits are over and we are in trailing whitespace. Whitespace after the
// size is tolerated (illegal per the spec but seen in the wild, see issue #1812), but whitespace *between* size
// digits is not: without this a chunk size such as `5 0` would be read here as 0x50 = 80 bytes while a proxy that
// stops at the space reads 5, a request-smuggling discrepancy. Once whitespace is seen only more whitespace or a
// terminator may follow; a further hex digit falls through to the illegal-character case.
@tailrec def parseSize(cursor: Int, size: Long, sawWhitespace: Boolean): StateResult =
if (size <= Int.MaxValue) {
byteChar(input, cursor) match {
case c if CharacterClasses.HEXDIG(c) => parseSize(cursor + 1, size * 16 + CharUtils.hexValue(c))
case c if CharacterClasses.HEXDIG(c) && !sawWhitespace =>
parseSize(cursor + 1, size * 16 + CharUtils.hexValue(c), sawWhitespace = false)
case c if size > settings.maxChunkSize =>
failEntityStream(
s"HTTP chunk of $size bytes exceeds the configured limit of ${settings.maxChunkSize} bytes")
case ';' if cursor > offset => parseChunkExtensions(size.toInt, cursor + 1)()
case '\r' if cursor > offset && byteAt(input, cursor + 1) == LF_BYTE =>
parseChunkBody(size.toInt, "", cursor + 2)
case '\n' if cursor > offset => parseChunkBody(size.toInt, "", cursor + 1)
case c if CharacterClasses.WSP(c) => parseSize(cursor + 1, size) // illegal according to the spec but can happen, see issue #1812
case c => failEntityStream(s"Illegal character '${escape(c)}' in chunk start")
case '\n' if cursor > offset => parseChunkBody(size.toInt, "", cursor + 1)
case c if CharacterClasses.WSP(c) && cursor > offset => parseSize(cursor + 1, size, sawWhitespace = true)
case c => failEntityStream(s"Illegal character '${escape(c)}' in chunk start")
}
} else failEntityStream(s"HTTP chunk size exceeds Integer.MAX_VALUE (${Int.MaxValue}) bytes")

try parseSize(offset, 0)
try parseSize(offset, 0, sawWhitespace = false)
catch {
case NotEnoughDataException =>
continue(input, offset)(parseChunk(_, _, isLastMessage, totalBytesRead, chunkCount))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,28 @@ abstract class RequestParserSpec(mode: String, newLine: String) extends AnyFreeS
closeAfterResponseCompletion shouldEqual Seq(false)
}

"whitespace between chunk size digits" in new Test {
// `5 0` must not be read as 0x50 = 80 bytes; a hex digit after whitespace is rejected so that this parser and
// a fronting proxy cannot disagree on the chunk size (request smuggling)
Seq(
start,
"""5 0
|""") should generalMultiParseTo(
Right(baseRequest),
Left(EntityStreamError(ErrorInfo("Illegal character '0' in chunk start"))))
closeAfterResponseCompletion shouldEqual Seq(false)
}

"leading whitespace before the chunk size" in new Test {
Seq(
start,
""" 5
|""") should generalMultiParseTo(
Right(baseRequest),
Left(EntityStreamError(ErrorInfo("Illegal character ' ' in chunk start"))))
closeAfterResponseCompletion shouldEqual Seq(false)
}

"an illegal char in chunk size" in new Test {
Seq(start, "bla") should generalMultiParseTo(
Right(baseRequest),
Expand Down