test: ensure HTTP server enforces limits on headers and body size

This commit is contained in:
Matthew Zipkin
2026-03-18 12:04:48 -04:00
parent 0c1a07e890
commit 422ca211ec

View File

@@ -15,6 +15,8 @@ import urllib.parse
RPCSERVERTIMEOUT = 2
# Set in httpserver.cpp and passed to libevent evhttp_set_max_headers_size()
MAX_HEADERS_SIZE = 8192
# Set in serialize.h and passed to libevent evhttp_set_max_body_size()
MAX_SIZE = 0x02000000
class BitcoinHTTPConnection:
@@ -45,6 +47,9 @@ class BitcoinHTTPConnection:
def set_timeout(self, seconds):
self.conn.sock.settimeout(seconds)
def add_header(self, key, value):
self.headers.update({key: value})
def _request(self, method, path, data, connection_header, **kwargs):
headers = self.headers.copy()
if connection_header is not None:
@@ -166,6 +171,57 @@ class HTTPBasicsTest (BitcoinTestFramework):
response2 = conn.get(f'/{"x" * MAX_HEADERS_SIZE}')
assert_equal(response2.status, http.client.BAD_REQUEST)
# Compute how many short header lines need to be added to http.client
# default headers to make / break the total limit in a single request.
header_line_length = len("header_0000: foo\r\n")
headers_below_limit = (MAX_HEADERS_SIZE - 1000) // header_line_length
headers_above_limit = MAX_HEADERS_SIZE // header_line_length
# This is a libevent mystery:
# libevent does not reject the request until it is more than
# 1,000 bytes above the configured limit.
headers_above_limit += 1000 // header_line_length
# Many small header lines is ok
conn = BitcoinHTTPConnection(self.nodes[2])
for i in range(headers_below_limit):
conn.add_header(f"header_{i:04}", "foo")
response3 = conn.get('/x')
assert_equal(response3.status, http.client.NOT_FOUND)
# Too many small header lines exceeds total headers size allowed
conn = BitcoinHTTPConnection(self.nodes[2])
for i in range(headers_above_limit):
conn.add_header(f"header_{i:04}", "foo")
response3 = conn.get('/x')
assert_equal(response3.status, http.client.BAD_REQUEST)
# Compute how much data we can add to a request message body
# to make / break the limit.
base_request_body_size = len('{"jsonrpc": "2.0", "id": "0", "method": "submitblock", "params": [""]}}')
bytes_below_limit = MAX_SIZE - base_request_body_size
bytes_above_limit = MAX_SIZE - base_request_body_size + 2
# Large request body size is ok
conn = BitcoinHTTPConnection(self.nodes[0])
response4 = conn.post('/', f'{{"jsonrpc": "2.0", "id": "0", "method": "submitblock", "params": ["{"0" * bytes_below_limit}"]}}')
assert_equal(response4.status, http.client.OK)
conn = BitcoinHTTPConnection(self.nodes[1])
try:
# Excessive body size is invalid
response5 = conn.post('/', f'{{"jsonrpc": "2.0", "id": "0", "method": "submitblock", "params": ["{"0" * bytes_above_limit}"]}}')
# The server will send a 400 response and disconnect but
# due to a race condition, the python client may or may not
# receive the response before detecting the broken socket.
response5.read()
assert_equal(response5.status, http.client.BAD_REQUEST)
assert conn.sock_closed()
self.log.debug("Server sent response before terminating connection")
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
self.log.debug("Server terminated connection immediately")
def check_pipelining(self):
"""
@@ -228,6 +284,38 @@ class HTTPBasicsTest (BitcoinTestFramework):
response1 = conn.recv_raw()
assert b'{"result":"high-hash","error":null}\n' in response1
self.log.info("Check excessive size HTTP request encoded with chunked transfer")
conn = BitcoinHTTPConnection(self.nodes[0])
headers_chunked = conn.headers.copy()
headers_chunked.update({"Transfer-encoding": "chunked"})
body_chunked = [
b'{"method": "submitblock", "params": ["',
b'0' * 10000000,
b'1' * 10000000,
b'2' * 10000000,
b'3' * 10000000,
b'"]}'
]
try:
conn.conn.request(
method='POST',
url='/',
body=iter(body_chunked),
headers=headers_chunked,
encode_chunked=True)
# The server will send a 400 response and disconnect but
# due to a race condition, the python client may or may not
# receive the response before detecting the broken socket.
response2 = conn.conn.getresponse()
response2.read()
assert_equal(response2.status, http.client.BAD_REQUEST)
assert conn.sock_closed()
self.log.debug("Server sent response before terminating connection")
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
# ...or just immediately disconnect
self.log.debug("Server terminated connection immediately")
def check_idle_timeout(self):
self.log.info("Check -rpcservertimeout")