From 39cf5f648a9e5e25312909a2dc26681edaa2402c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 26 Aug 2026 09:21:40 +0900 Subject: [PATCH] Limit the number of informational responses per request A peer that keeps sending 1xx responses keeps the client in the read loop of transport_request indefinitely, since the per-response header limit resets for each response. Cap the count at 100, the same value CPython's http.client uses. Co-Authored-By: Claude Opus 5 --- lib/net/http.rb | 6 ++++++ lib/net/http/response.rb | 4 ++++ test/net/http/test_http.rb | 42 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/lib/net/http.rb b/lib/net/http.rb index 6c43e62..d1e9b2b 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -2497,11 +2497,17 @@ def transport_request(req) # still read the received response. end + informational_count = 0 begin res = HTTPResponse.read_new(@socket) res.decode_content = req.decode_content res.body_encoding = @response_body_encoding res.ignore_eof = @ignore_eof + if res.kind_of?(HTTPInformation) + informational_count += 1 + raise HTTPBadResponse, 'too many informational responses' if + informational_count > HTTPResponse::MAX_INFORMATIONAL_RESPONSES + end end while res.kind_of?(HTTPInformation) res.uri = req.uri diff --git a/lib/net/http/response.rb b/lib/net/http/response.rb index 4d8b938..fb48490 100644 --- a/lib/net/http/response.rb +++ b/lib/net/http/response.rb @@ -136,6 +136,10 @@ class Net::HTTPResponse # The maximum total size in bytes of the response header. MAX_RESPONSE_HEADER_LENGTH = 1024 * 1024 # 1 MiB + # The maximum number of informational (1xx) responses accepted before the + # final response. + MAX_INFORMATIONAL_RESPONSES = 100 + class << self # true if the response has a body. def body_permitted? diff --git a/test/net/http/test_http.rb b/test/net/http/test_http.rb index 097fda6..590f8a2 100644 --- a/test/net/http/test_http.rb +++ b/test/net/http/test_http.rb @@ -1171,6 +1171,48 @@ def test_info end end +class TestNetHTTPInformationalResponses < Test::Unit::TestCase + CONFIG = { + 'host' => '127.0.0.1', + 'proxy_host' => nil, + 'proxy_port' => nil, + } + + include TestNetHTTPUtils + + def mount_informational(count) + @server.mount('/info', proc {|req, res| + count.times { req.continue } + res.body = 'BODY' + }) + end + + def test_informational_responses + mount_informational 3 + start {|http| + res = http.get('/info') + assert_equal('BODY', res.body) + } + end + + def test_max_informational_responses + mount_informational Net::HTTPResponse::MAX_INFORMATIONAL_RESPONSES + start {|http| + res = http.get('/info') + assert_equal('BODY', res.body) + } + end + + def test_too_many_informational_responses + mount_informational Net::HTTPResponse::MAX_INFORMATIONAL_RESPONSES + 1 + start {|http| + assert_raise(Net::HTTPBadResponse) { + http.get('/info') + } + } + end +end + class TestNetHTTPKeepAlive < Test::Unit::TestCase CONFIG = { 'host' => '127.0.0.1',