Skip to content

Commit ebe8f38

Browse files
hsbtmamenobuclaude
committed
Limit the total size of response headers
each_response_header read header lines until the blank separator with no bound on their total size, so a server could exhaust client memory by sending a large header block. Cap the cumulative size at 1 MiB and raise Net::HTTPBadResponse once it is exceeded. Co-authored-by: Yusuke Endoh <mame@ruby-lang.org> Co-authored-by: Nobuyoshi Nakada <nobu@ruby-lang.org> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent ab084fd commit ebe8f38

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

lib/net/http/response.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@
133133
# there is a protocol error.
134134
#
135135
class Net::HTTPResponse
136+
# The maximum total size in bytes of the response header.
137+
MAX_RESPONSE_HEADER_LENGTH = 1024 * 1024 # 1 MiB
138+
136139
class << self
137140
# true if the response has a body.
138141
def body_permitted?
@@ -170,8 +173,12 @@ def response_class(code)
170173

171174
def each_response_header(sock)
172175
key = value = nil
176+
remaining = MAX_RESPONSE_HEADER_LENGTH
173177
while true
174-
line = sock.readuntil("\n", true).sub(/\s+\z/, '')
178+
line = sock.readuntil("\n", true)
179+
remaining -= line.bytesize
180+
raise Net::HTTPBadResponse, 'response header too large' if remaining < 0
181+
line = line.sub(/\s+\z/, '')
175182
break if line.empty?
176183
if line[0] == ?\s or line[0] == ?\t and value
177184
value << ' ' unless value.empty?

test/net/http/test_httpresponse.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,30 @@ def test_singleline_header
1818
assert_equal('close', res['connection'])
1919
end
2020

21+
def test_response_header_too_large
22+
big_value = 'a' * (Net::HTTPHeader::MAX_FIELD_LENGTH - 100)
23+
count = (Net::HTTPResponse::MAX_RESPONSE_HEADER_LENGTH / big_value.bytesize) + 2
24+
headers = +"HTTP/1.1 200 OK\n"
25+
count.times { |i| headers << "X-Pad-#{i}: #{big_value}\n" }
26+
headers << "\nhello\n"
27+
io = dummy_io(headers)
28+
assert_raise(Net::HTTPBadResponse) do
29+
Net::HTTPResponse.read_new(io)
30+
end
31+
end
32+
33+
def test_response_header_within_limit
34+
big_value = 'a' * (Net::HTTPHeader::MAX_FIELD_LENGTH - 100)
35+
count = (Net::HTTPResponse::MAX_RESPONSE_HEADER_LENGTH / big_value.bytesize) - 1
36+
headers = +"HTTP/1.1 200 OK\n"
37+
count.times { |i| headers << "X-Pad-#{i}: #{big_value}\n" }
38+
headers << "\nhello\n"
39+
io = dummy_io(headers)
40+
assert_nothing_raised do
41+
Net::HTTPResponse.read_new(io)
42+
end
43+
end
44+
2145
def test_multiline_header
2246
io = dummy_io(<<EOS)
2347
HTTP/1.1 200 OK

0 commit comments

Comments
 (0)