Skip to content

Commit f705e6b

Browse files
authored
Merge pull request #219 from ruby/set-header-limit
Added extra header validation for total size of headers
2 parents 0b23c70 + ebe8f38 commit f705e6b

4 files changed

Lines changed: 64 additions & 9 deletions

File tree

lib/net/http/header.rb

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,9 @@ def initialize_http_header(initheader) #:nodoc:
193193
warn "net/http: nil HTTP header: #{key}", uplevel: 3 if $VERBOSE
194194
else
195195
value = value.strip # raise error for invalid byte sequences
196-
if key.to_s.bytesize > MAX_KEY_LENGTH
197-
raise ArgumentError, "too long (#{key.bytesize} bytes) header: #{key[0, 30].inspect}..."
198-
end
199196
validate_field_name(key)
200197
if value.to_s.bytesize > MAX_FIELD_LENGTH
201-
raise ArgumentError, "header #{key} has too long field value: #{value.bytesize}"
198+
raise ArgumentError, "header #{key} has too long field value: #{value.bytesize} bytes (limit #{MAX_FIELD_LENGTH})"
202199
end
203200
if value.count("\r\n") > 0
204201
raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF"
@@ -264,14 +261,17 @@ def []=(key, val)
264261
def add_field(key, val)
265262
stringified_downcased_key = key.downcase.to_s
266263
if @header.key?(stringified_downcased_key)
267-
append_field_value(@header[stringified_downcased_key], val)
264+
append_field_value(@header[stringified_downcased_key], val, key)
268265
else
269266
set_field(key, val)
270267
end
271268
end
272269

273270
# :stopdoc:
274271
private def validate_field_name(key)
272+
if key.to_s.bytesize > MAX_KEY_LENGTH
273+
raise ArgumentError, "too long (#{key.to_s.bytesize} bytes) header: #{key.to_s[0, 30].inspect}..."
274+
end
275275
if /[\x00-\x1f\x7f:]/n.match?(key.to_s.b)
276276
raise ArgumentError, "header field name cannot include control characters or colon: #{key.to_s[0, 30].inspect}"
277277
end
@@ -282,23 +282,29 @@ def add_field(key, val)
282282
case val
283283
when Enumerable
284284
ary = []
285-
append_field_value(ary, val)
285+
append_field_value(ary, val, key)
286286
@header[key.downcase.to_s] = ary
287287
else
288288
val = val.to_s # for compatibility use to_s instead of to_str
289+
if val.bytesize > MAX_FIELD_LENGTH
290+
raise ArgumentError, "header #{key} has too long field value: #{val.bytesize} bytes (limit #{MAX_FIELD_LENGTH})"
291+
end
289292
if val.b.count("\r\n") > 0
290293
raise ArgumentError, 'header field value cannot include CR/LF'
291294
end
292295
@header[key.downcase.to_s] = [val]
293296
end
294297
end
295298

296-
private def append_field_value(ary, val)
299+
private def append_field_value(ary, val, key)
297300
case val
298301
when Enumerable
299-
val.each{|x| append_field_value(ary, x)}
302+
val.each{|x| append_field_value(ary, x, key)}
300303
else
301304
val = val.to_s
305+
if val.bytesize > MAX_FIELD_LENGTH
306+
raise ArgumentError, "header #{key} has too long field value: #{val.bytesize} bytes (limit #{MAX_FIELD_LENGTH})"
307+
end
302308
if /[\r\n]/n.match?(val.b)
303309
raise ArgumentError, 'header field value cannot include CR/LF'
304310
end

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_httpheader.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,24 @@ def test_ASET
8989
assert_raise(ArgumentError){ @c['foo'] = ["a\nb"] }
9090
end
9191

92+
def test_set_field_too_long_key
93+
assert_raise(ArgumentError){ @c['x' * (Net::HTTPHeader::MAX_KEY_LENGTH + 1)] = 'a' }
94+
assert_nothing_raised{ @c['x' * Net::HTTPHeader::MAX_KEY_LENGTH] = 'a' }
95+
end
96+
97+
def test_set_field_too_long_value
98+
long = 'a' * (Net::HTTPHeader::MAX_FIELD_LENGTH + 1)
99+
assert_raise(ArgumentError){ @c['foo'] = long }
100+
assert_raise(ArgumentError){ @c['foo'] = [long] }
101+
assert_raise(ArgumentError){ @c.add_field 'foo', long }
102+
103+
# the error message names the key and the limit on every path
104+
@c['foo'] = 'ok'
105+
e = assert_raise(ArgumentError){ @c.add_field 'foo', long }
106+
assert_match(/foo/, e.message)
107+
assert_match(/#{Net::HTTPHeader::MAX_FIELD_LENGTH}/, e.message)
108+
end
109+
92110
def test_AREF
93111
@c['My-Header'] = 'test string'
94112
assert_equal 'test string', @c['my-header']

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)