Skip to content

Commit 23e859e

Browse files
authored
Make Content-Length handling strict (#324)
`Content-Length` value must have only digits. See also RFC 9110 Section 8.6: https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6 Content-Length = 1*DIGIT The current implementation accepts garbage before/after digits such as `abc5` and `5abc`. This rejects these invalid values. But this accepts same values in one `Content-Length` Content-Length: 5, 5 and in multiple `Content-Length`es: Content-Length: 5 Content-Length: 5 This is for the following part in the RFC: https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-13 > Likewise, a sender MUST NOT forward a message with a Content-Length > header field value that does not match the ABNF above, with one > exception: a recipient of a Content-Length header field value > consisting of the same decimal value repeated as a comma-separated > list (e.g, "Content-Length: 42, 42") MAY either reject the message > as invalid or replace that invalid field value with a single > instance of the decimal value, since this likely indicates that a > duplicate was generated or combined by an upstream message > processor.
1 parent c49514e commit 23e859e

4 files changed

Lines changed: 130 additions & 12 deletions

File tree

‎lib/net/http/header.rb‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -631,11 +631,35 @@ def set_range(r, e = nil)
631631
# res = Net::HTTP.get_response(hostname, '/todos/1')
632632
# res.content_length # => nil
633633
#
634+
# The value must consist of digits only.
635+
# Multiple <tt>'Content-Length'</tt> values are accepted
636+
# only when they are all identical;
637+
# otherwise Net::HTTPHeaderSyntaxError is raised.
638+
# See {RFC 9110 Section 8.6}[https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6].
634639
def content_length
635-
return nil unless key?('Content-Length')
636-
len = self['Content-Length'].slice(/\d+/) or
637-
raise Net::HTTPHeaderSyntaxError, 'wrong Content-Length format'
638-
len.to_i
640+
values = @header['content-length']
641+
return nil if values.nil?
642+
643+
lengths = []
644+
values.each do |value|
645+
value = value.strip
646+
if value.empty?
647+
raise Net::HTTPHeaderSyntaxError,
648+
"empty Content-Length value"
649+
end
650+
lengths.concat(value.split(/\s*,\s*/, -1))
651+
end
652+
if lengths.uniq.size != 1
653+
raise Net::HTTPHeaderSyntaxError,
654+
"Content-Length has multiple different values: " +
655+
values.join(", ")
656+
end
657+
length = lengths.first
658+
unless /\A\d+\z/.match?(length)
659+
raise Net::HTTPHeaderSyntaxError,
660+
"wrong Content-Length format: #{length}"
661+
end
662+
length.to_i
639663
end
640664

641665
# Sets the value of field <tt>'Content-Length'</tt> to the given numeric;

‎test/net/http/test_httpheader.rb‎

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -396,19 +396,55 @@ def test_content_length
396396
try_content_length 500, '500'
397397
try_content_length 10000_0000_0000, '1000000000000'
398398
try_content_length 123, ' 123'
399-
try_content_length 1, '1 23'
400-
try_content_length 500, '(OK)500'
401-
assert_raise(Net::HTTPHeaderSyntaxError, 'here is no digit, but') {
402-
@c['content-length'] = 'no digit'
403-
@c.content_length
404-
}
399+
400+
# Same values in one Content-Length field are accepted.
401+
# See: https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-13
402+
try_content_length 5, '5, 5'
403+
404+
# Same values in multiple Content-Length fields are accepted.
405+
# See: https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-13
406+
@c.delete('content-length')
407+
@c.add_field('content-length', '7')
408+
@c.add_field('content-length', '7')
409+
assert_equal 7, @c.content_length
405410
end
406411

407412
def try_content_length(len, str)
408413
@c['content-length'] = str
409414
assert_equal len, @c.content_length
410415
end
411416

417+
def test_content_length_invalid
418+
try_invalid_content_length ''
419+
try_invalid_content_length '1 23'
420+
try_invalid_content_length '(OK)500'
421+
try_invalid_content_length 'no digit'
422+
try_invalid_content_length 'abc5'
423+
try_invalid_content_length '5abc'
424+
try_invalid_content_length '5, 6'
425+
426+
@c.delete('content-length')
427+
@c.add_field('content-length', '7')
428+
@c.add_field('content-length', '8')
429+
assert_raise(Net::HTTPHeaderSyntaxError) {
430+
@c.content_length
431+
}
432+
433+
@c.delete('content-length')
434+
@c.add_field('content-length', '5')
435+
@c.add_field('content-length', '')
436+
assert_raise(Net::HTTPHeaderSyntaxError) {
437+
@c.content_length
438+
}
439+
end
440+
441+
def try_invalid_content_length(str)
442+
@c['content-length'] = str
443+
assert_raise(Net::HTTPHeaderSyntaxError, str) {
444+
@c.content_length
445+
}
446+
end
447+
412448
def test_content_length=
413449
@c.content_length = 0
414450
assert_equal 0, @c.content_length

‎test/net/http/test_httpresponse.rb‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,64 @@ def test_read_body
9999
Connection: close
100100
Content-Length: 5
101101
102+
hello
103+
EOS
104+
105+
res = Net::HTTPResponse.read_new(io)
106+
107+
body = nil
108+
109+
res.reading_body io, true do
110+
body = res.read_body
111+
end
112+
113+
assert_equal 'hello', body
114+
end
115+
116+
def test_read_body_invalid_content_length
117+
io = dummy_io(<<EOS)
118+
HTTP/1.1 200 OK
119+
Connection: close
120+
Content-Length: abc5
121+
122+
hello
123+
EOS
124+
125+
res = Net::HTTPResponse.read_new(io)
126+
127+
assert_raise(Net::HTTPHeaderSyntaxError) do
128+
res.reading_body io, true do
129+
res.read_body
130+
end
131+
end
132+
end
133+
134+
def test_read_body_multiple_different_content_length
135+
io = dummy_io(<<EOS)
136+
HTTP/1.1 200 OK
137+
Connection: close
138+
Content-Length: 5
139+
Content-Length: 100
140+
141+
hello
142+
EOS
143+
144+
res = Net::HTTPResponse.read_new(io)
145+
146+
assert_raise(Net::HTTPHeaderSyntaxError) do
147+
res.reading_body io, true do
148+
res.read_body
149+
end
150+
end
151+
end
152+
153+
def test_read_body_multiple_same_content_length
154+
io = dummy_io(<<EOS)
155+
HTTP/1.1 200 OK
156+
Connection: close
157+
Content-Length: 5
158+
Content-Length: 5
159+
102160
hello
103161
EOS
104162

‎test/net/http/utils.rb‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,9 @@ def finish
209209
def build_response_headers
210210
response = "HTTP/1.1 #{@status} #{status_message(@status)}\r\n"
211211
if @chunked
212-
@headers['Transfer-Encoding'] = 'chunked'
212+
self['Transfer-Encoding'] = 'chunked'
213213
else
214-
@headers['Content-Length'] = @body.bytesize.to_s
214+
self['Content-Length'] ||= @body.bytesize.to_s
215215
end
216216
@headers.each do |key, value|
217217
response << "#{key}: #{value}\r\n"

0 commit comments

Comments
 (0)