CHUNK_VALID_ENDING = Const::LINE_END
CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
+ # The maximum number of bytes we'll buffer looking for a valid
+ # chunk header.
+ MAX_CHUNK_HEADER_SIZE = 4096
+
+ # The maximum amount of excess data the client sends
+ # using chunk size extensions before we abort the connection.
+ MAX_CHUNK_EXCESS = 16 * 1024
+
# Content-Length header value validation
CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
@chunked_body = true
@partial_part_left = 0
@prev_chunk = ""
+ @excess_cr = 0
@body = Tempfile.new(Const::PUMA_TMP_BASE)
@body.unlink
end
end
+ # Track the excess as a function of the size of the
+ # header vs the size of the actual data. Excess can
+ # go negative (and is expected to) when the body is
+ # significant.
+ # The additional of chunk_hex.size and 2 compensates
+ # for a client sending 1 byte in a chunked body over
+ # a long period of time, making sure that that client
+ # isn't accidentally eventually punished.
+ @excess_cr += (line.size - len - chunk_hex.size - 2)
+
+ if @excess_cr >= MAX_CHUNK_EXCESS
+ raise HttpParserError, "Maximum chunk excess detected"
+ end
+
len += 2
part = io.read(len)
@partial_part_left = len - part.size
end
else
+ if @prev_chunk.size + chunk.size >= MAX_CHUNK_HEADER_SIZE
+ raise HttpParserError, "maximum size of chunk header exceeded"
+ end
+
@prev_chunk = line
return false
end
end
end
+ def test_large_chunked_request_header
+ server_run(environment: :production) { |env|
+ [200, {}, [""]]
+ }
+
+ max_chunk_header_size = Puma::Client::MAX_CHUNK_HEADER_SIZE
+ header = "GET / HTTP/1.1\r\nConnection: close\r\nContent-Length: 200\r\nTransfer-Encoding: chunked\r\n\r\n"
+ socket = send_http "#{header}1;t#{'x' * (max_chunk_header_size + 2)}"
+
+ data = socket.read
+
+ assert_match "HTTP/1.1 400 Bad Request\r\n\r\n", data
+ end
+
def test_chunked_request_pause_before_value
body = nil
content_length = nil