From d1345074584118aa7c56e6dde171502300b89ad7 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 4 Aug 2026 16:09:22 +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 | 39 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/lib/net/http.rb b/lib/net/http.rb index 6c43e62..4254cd9 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 Net::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 0b5b326..31b35f9 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 e5028d4..bbce847 100644 --- a/test/net/http/test_http.rb +++ b/test/net/http/test_http.rb @@ -1171,6 +1171,45 @@ 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 logfile + @debug = StringIO.new('') + end + + def mount_proc(count) + @server.mount('/info', proc {|req, res| + socket = req.instance_variable_get(:@socket) + count.times { socket << "HTTP/1.1 100 Continue\r\n\r\n" } + res.body = 'BODY' + }) + end + + def test_informational_responses + mount_proc 3 + start {|http| + res = http.get('/info') + assert_equal('BODY', res.body) + } + end + + def test_too_many_informational_responses + mount_proc 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',