Skip to content

Commit b9212e4

Browse files
committed
Improve compliance with RFC 9112 for status line parsing
This patch improves compliance with RFC 9112 Section 4 for parsing the status-line of the HTTP response. - Require the HTTP version to be in the format "HTTP/x.y" - Disallow cases like "http/1.1" (lowercase "http"), "HTTP" (missing version), "HTTP/10.23" (multidigit versions) - Require a space between the status-code and the reason-phrase, even when the reason-phrase is empty - Disallow cases like "HTTP/1.1 200" (missing space after status code) - Make clear that SP parsing operates on the lenient behavior, which allows 'HTAB, VT (%x0B), FF (%x0C), or bare CR' in addition to space
1 parent b9683ee commit b9212e4

2 files changed

Lines changed: 91 additions & 20 deletions

File tree

lib/net/http/response.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def read_new(sock) #:nodoc: internal use only
157157

158158
def read_status_line(sock)
159159
str = sock.readline
160-
m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or
160+
m = /\AHTTP\/(\d\.\d)[ \t\v\f\r]+(\d\d\d)[ \t\v\f\r]+(.*)\z/n.match(str) or
161161
raise Net::HTTPBadResponse, "wrong status line: #{str.dump}"
162162
m.captures
163163
end

test/net/http/test_httpresponse.rb

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -639,9 +639,24 @@ def test_uri_equals
639639
assert_not_same uri, response.uri
640640
end
641641

642-
def test_ensure_zero_space_does_not_regress
642+
def test_normal_status_line
643+
io = dummy_io(<<EOS)
644+
HTTP/1.1 200 OK
645+
Content-Length: 5
646+
Connection: close
647+
648+
hello
649+
EOS
650+
651+
res = Net::HTTPResponse.read_new(io)
652+
assert_equal('1.1', res.http_version)
653+
assert_equal('200', res.code)
654+
assert_equal('OK', res.message)
655+
end
656+
657+
def test_reject_status_line_without_version
643658
io = dummy_io(<<EOS)
644-
HTTP/1.1 200OK
659+
HTTP 200 OK
645660
Content-Length: 5
646661
Connection: close
647662
@@ -653,24 +668,67 @@ def test_ensure_zero_space_does_not_regress
653668
end
654669
end
655670

656-
def test_allow_trailing_space_after_status
671+
def test_reject_status_line_with_multidigit_version
657672
io = dummy_io(<<EOS)
658-
HTTP/1.1 200\s
673+
HTTP/1.10 200 OK
659674
Content-Length: 5
660675
Connection: close
661676
662677
hello
663678
EOS
664679

665-
res = Net::HTTPResponse.read_new(io)
666-
assert_equal('1.1', res.http_version)
667-
assert_equal('200', res.code)
668-
assert_equal('', res.message)
680+
assert_raise Net::HTTPBadResponse do
681+
Net::HTTPResponse.read_new(io)
682+
end
669683
end
670684

671-
def test_normal_status_line
685+
def test_reject_lowercase_http_name
672686
io = dummy_io(<<EOS)
673-
HTTP/1.1 200 OK
687+
http/1.1 200 OK
688+
Content-Length: 5
689+
Connection: close
690+
691+
hello
692+
EOS
693+
694+
assert_raise Net::HTTPBadResponse do
695+
Net::HTTPResponse.read_new(io)
696+
end
697+
end
698+
699+
def test_reject_status_line_with_four_digit_code
700+
io = dummy_io(<<EOS)
701+
HTTP/1.1 2000 OK
702+
Content-Length: 5
703+
Connection: close
704+
705+
hello
706+
EOS
707+
708+
assert_raise Net::HTTPBadResponse do
709+
Net::HTTPResponse.read_new(io)
710+
end
711+
end
712+
713+
def test_reject_status_line_with_non_numeric_code
714+
io = dummy_io(<<EOS)
715+
HTTP/1.1 2oo OK
716+
Content-Length: 5
717+
Connection: close
718+
719+
hello
720+
EOS
721+
722+
assert_raise Net::HTTPBadResponse do
723+
Net::HTTPResponse.read_new(io)
724+
end
725+
end
726+
727+
def test_allow_empty_reason_phrase
728+
# RFC 9112 allows the reason-phrase itself to be empty, but the SP preceding it is required
729+
# Note the trailing space after the status code (200)
730+
io = dummy_io(<<EOS)
731+
HTTP/1.1 200
674732
Content-Length: 5
675733
Connection: close
676734
@@ -680,38 +738,51 @@ def test_normal_status_line
680738
res = Net::HTTPResponse.read_new(io)
681739
assert_equal('1.1', res.http_version)
682740
assert_equal('200', res.code)
683-
assert_equal('OK', res.message)
741+
assert_equal('', res.message)
684742
end
685743

686-
def test_allow_empty_reason_code
744+
def test_reject_empty_reason_phrase_without_preceding_space
687745
io = dummy_io(<<EOS)
688746
HTTP/1.1 200
689747
Content-Length: 5
690748
Connection: close
691749
750+
hello
751+
EOS
752+
753+
assert_raise Net::HTTPBadResponse do
754+
Net::HTTPResponse.read_new(io)
755+
end
756+
end
757+
758+
def test_allow_whitespace_chars_as_separator
759+
io = dummy_io(<<EOS)
760+
HTTP/1.1\t200\r OK
761+
Content-Length: 5
762+
Connection: close
763+
692764
hello
693765
EOS
694766

695767
res = Net::HTTPResponse.read_new(io)
696768
assert_equal('1.1', res.http_version)
697769
assert_equal('200', res.code)
698-
assert_equal(nil, res.message)
770+
assert_equal('OK', res.message)
699771
end
700772

701-
def test_raises_exception_with_missing_reason
773+
def test_allow_multiple_space_between_version_and_code
702774
io = dummy_io(<<EOS)
703-
HTTP/1.1 404
775+
HTTP/1.1 200 OK
704776
Content-Length: 5
705777
Connection: close
706778
707779
hello
708780
EOS
709781

710782
res = Net::HTTPResponse.read_new(io)
711-
assert_equal(nil, res.message)
712-
assert_raise Net::HTTPClientException do
713-
res.error!
714-
end
783+
assert_equal('1.1', res.http_version)
784+
assert_equal('200', res.code)
785+
assert_equal('OK', res.message)
715786
end
716787

717788
def test_read_code_type

0 commit comments

Comments
 (0)