Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions lib/net/http/header.rb
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,9 @@ def initialize_http_header(initheader) #:nodoc:
warn "net/http: nil HTTP header: #{key}", uplevel: 3 if $VERBOSE
else
value = value.strip # raise error for invalid byte sequences
if key.to_s.bytesize > MAX_KEY_LENGTH
raise ArgumentError, "too long (#{key.bytesize} bytes) header: #{key[0, 30].inspect}..."
end
validate_field_name(key)
if value.to_s.bytesize > MAX_FIELD_LENGTH
raise ArgumentError, "header #{key} has too long field value: #{value.bytesize}"
raise ArgumentError, "header #{key} has too long field value: #{value.bytesize} bytes (limit #{MAX_FIELD_LENGTH})"
end
if value.count("\r\n") > 0
raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF"
Expand Down Expand Up @@ -264,14 +261,17 @@ def []=(key, val)
def add_field(key, val)
stringified_downcased_key = key.downcase.to_s
if @header.key?(stringified_downcased_key)
append_field_value(@header[stringified_downcased_key], val)
append_field_value(@header[stringified_downcased_key], val, key)
else
set_field(key, val)
end
end

# :stopdoc:
private def validate_field_name(key)
if key.to_s.bytesize > MAX_KEY_LENGTH
raise ArgumentError, "too long (#{key.to_s.bytesize} bytes) header: #{key.to_s[0, 30].inspect}..."
end
if /[\x00-\x1f\x7f:]/n.match?(key.to_s.b)
raise ArgumentError, "header field name cannot include control characters or colon: #{key.to_s[0, 30].inspect}"
end
Expand All @@ -282,23 +282,29 @@ def add_field(key, val)
case val
when Enumerable
ary = []
append_field_value(ary, val)
append_field_value(ary, val, key)
@header[key.downcase.to_s] = ary
else
val = val.to_s # for compatibility use to_s instead of to_str
if val.bytesize > MAX_FIELD_LENGTH
raise ArgumentError, "header #{key} has too long field value: #{val.bytesize} bytes (limit #{MAX_FIELD_LENGTH})"
end
if val.b.count("\r\n") > 0
raise ArgumentError, 'header field value cannot include CR/LF'
end
@header[key.downcase.to_s] = [val]
end
end

private def append_field_value(ary, val)
private def append_field_value(ary, val, key)
case val
when Enumerable
val.each{|x| append_field_value(ary, x)}
val.each{|x| append_field_value(ary, x, key)}
else
val = val.to_s
if val.bytesize > MAX_FIELD_LENGTH
raise ArgumentError, "header #{key} has too long field value: #{val.bytesize} bytes (limit #{MAX_FIELD_LENGTH})"
end
if /[\r\n]/n.match?(val.b)
raise ArgumentError, 'header field value cannot include CR/LF'
end
Expand Down
9 changes: 8 additions & 1 deletion lib/net/http/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@
# there is a protocol error.
#
class Net::HTTPResponse
# The maximum total size in bytes of the response header.
MAX_RESPONSE_HEADER_LENGTH = 1024 * 1024 # 1 MiB

class << self
# true if the response has a body.
def body_permitted?
Expand Down Expand Up @@ -170,8 +173,12 @@ def response_class(code)

def each_response_header(sock)
key = value = nil
remaining = MAX_RESPONSE_HEADER_LENGTH
while true
line = sock.readuntil("\n", true).sub(/\s+\z/, '')
line = sock.readuntil("\n", true)
remaining -= line.bytesize
raise Net::HTTPBadResponse, 'response header too large' if remaining < 0
line = line.sub(/\s+\z/, '')
break if line.empty?
if line[0] == ?\s or line[0] == ?\t and value
value << ' ' unless value.empty?
Expand Down
18 changes: 18 additions & 0 deletions test/net/http/test_httpheader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,24 @@ def test_ASET
assert_raise(ArgumentError){ @c['foo'] = ["a\nb"] }
end

def test_set_field_too_long_key
assert_raise(ArgumentError){ @c['x' * (Net::HTTPHeader::MAX_KEY_LENGTH + 1)] = 'a' }
assert_nothing_raised{ @c['x' * Net::HTTPHeader::MAX_KEY_LENGTH] = 'a' }
end

def test_set_field_too_long_value
long = 'a' * (Net::HTTPHeader::MAX_FIELD_LENGTH + 1)
assert_raise(ArgumentError){ @c['foo'] = long }
assert_raise(ArgumentError){ @c['foo'] = [long] }
assert_raise(ArgumentError){ @c.add_field 'foo', long }

# the error message names the key and the limit on every path
@c['foo'] = 'ok'
e = assert_raise(ArgumentError){ @c.add_field 'foo', long }
assert_match(/foo/, e.message)
assert_match(/#{Net::HTTPHeader::MAX_FIELD_LENGTH}/, e.message)
end

def test_AREF
@c['My-Header'] = 'test string'
assert_equal 'test string', @c['my-header']
Expand Down
24 changes: 24 additions & 0 deletions test/net/http/test_httpresponse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,30 @@ def test_singleline_header
assert_equal('close', res['connection'])
end

def test_response_header_too_large
big_value = 'a' * (Net::HTTPHeader::MAX_FIELD_LENGTH - 100)
count = (Net::HTTPResponse::MAX_RESPONSE_HEADER_LENGTH / big_value.bytesize) + 2
headers = +"HTTP/1.1 200 OK\n"
count.times { |i| headers << "X-Pad-#{i}: #{big_value}\n" }
headers << "\nhello\n"
io = dummy_io(headers)
assert_raise(Net::HTTPBadResponse) do
Net::HTTPResponse.read_new(io)
end
end

def test_response_header_within_limit
big_value = 'a' * (Net::HTTPHeader::MAX_FIELD_LENGTH - 100)
count = (Net::HTTPResponse::MAX_RESPONSE_HEADER_LENGTH / big_value.bytesize) - 1
headers = +"HTTP/1.1 200 OK\n"
count.times { |i| headers << "X-Pad-#{i}: #{big_value}\n" }
headers << "\nhello\n"
io = dummy_io(headers)
assert_nothing_raised do
Net::HTTPResponse.read_new(io)
end
end

def test_multiline_header
io = dummy_io(<<EOS)
HTTP/1.1 200 OK
Expand Down
Loading