Skip to content

Commit e11fc16

Browse files
authored
Strict validation of content length and chunk length. (#20)
1 parent 329c2f4 commit e11fc16

File tree

4 files changed

+140
-5
lines changed

4 files changed

+140
-5
lines changed

lib/protocol/http1/body/chunked.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,20 @@ def close(error = nil)
3535
super
3636
end
3737

38+
VALID_CHUNK_LENGTH = /\A[0-9a-fA-F]+\z/
39+
3840
# Follows the procedure outlined in https://tools.ietf.org/html/rfc7230#section-4.1.3
3941
def read
4042
return nil if @finished
4143

44+
length, extensions = read_line.split(";", 2)
45+
46+
unless length =~ VALID_CHUNK_LENGTH
47+
raise BadRequest, "Invalid chunk length: #{length.dump}"
48+
end
49+
4250
# It is possible this line contains chunk extension, so we use `to_i` to only consider the initial integral part:
43-
length = read_line.to_i(16)
51+
length = Integer(length, 16)
4452

4553
if length == 0
4654
@finished = true

lib/protocol/http1/connection.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,10 +398,12 @@ def read_tunnel_body
398398
HEAD = "HEAD"
399399
CONNECT = "CONNECT"
400400

401+
VALID_CONTENT_LENGTH = /\A\d+\z/
402+
401403
def extract_content_length(headers)
402404
if content_length = headers.delete(CONTENT_LENGTH)
403-
if length = Integer(content_length, exception: false) and length >= 0
404-
yield length
405+
if content_length =~ VALID_CONTENT_LENGTH
406+
yield Integer(content_length, 10)
405407
else
406408
raise BadRequest, "Invalid content length: #{content_length}"
407409
end

test/protocol/http1/connection.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,15 +208,15 @@
208208

209209
with "HEAD" do
210210
it "can read length of head response" do
211-
body = client.read_response_body("HEAD", 200, {'content-length' => 3773})
211+
body = client.read_response_body("HEAD", 200, {'content-length' => '3773'})
212212

213213
expect(body).to be_a ::Protocol::HTTP::Body::Head
214214
expect(body.length).to be == 3773
215215
expect(body.read).to be_nil
216216
end
217217

218218
it "ignores zero length body" do
219-
body = client.read_response_body("HEAD", 200, {'content-length' => 0})
219+
body = client.read_response_body("HEAD", 200, {'content-length' => '0'})
220220

221221
expect(body).to be_nil
222222
end

test/protocol/http1/connection/bad.rb

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2019-2023, by Samuel Williams.
5+
6+
require 'protocol/http1/connection'
7+
require 'connection_context'
8+
9+
describe Protocol::HTTP1::Connection do
10+
include_context ConnectionContext
11+
12+
def before
13+
super
14+
15+
client.stream.write(input)
16+
client.stream.close
17+
end
18+
19+
with "invalid hexadecimal content-length" do
20+
def input
21+
<<~HTTP.gsub("\n", "\r\n")
22+
POST / HTTP/1.1
23+
Host: a.com
24+
Content-Length: 0x10
25+
Connection: close
26+
27+
0123456789abcdef
28+
HTTP
29+
end
30+
31+
it "should fail to parse the request body" do
32+
expect do
33+
server.read_request
34+
end.to raise_exception(Protocol::HTTP1::BadRequest)
35+
end
36+
end
37+
38+
with "invalid +integer content-length" do
39+
def input
40+
<<~HTTP.gsub("\n", "\r\n")
41+
POST / HTTP/1.1
42+
Host: a.com
43+
Content-Length: +16
44+
Connection: close
45+
46+
0123456789abcdef
47+
HTTP
48+
end
49+
50+
it "should fail to parse the request body" do
51+
expect do
52+
server.read_request
53+
end.to raise_exception(Protocol::HTTP1::BadRequest)
54+
end
55+
end
56+
57+
with "invalid -integer content-length" do
58+
def input
59+
<<~HTTP.gsub("\n", "\r\n")
60+
POST / HTTP/1.1
61+
Host: a.com
62+
Content-Length: -16
63+
Connection: close
64+
65+
0123456789abcdef
66+
HTTP
67+
end
68+
69+
it "should fail to parse the request body" do
70+
expect do
71+
server.read_request
72+
end.to raise_exception(Protocol::HTTP1::BadRequest)
73+
end
74+
end
75+
76+
with "invalid hexidecimal chunk size" do
77+
def input
78+
<<~HTTP.gsub("\n", "\r\n")
79+
POST / HTTP/1.1
80+
Host: a.com
81+
Transfer-Encoding: chunked
82+
Connection: close
83+
84+
0x10
85+
0123456789abcdef
86+
0
87+
HTTP
88+
end
89+
90+
it "should fail to parse the request body" do
91+
authority, method, target, version, headers, body = server.read_request
92+
93+
expect(body).to be_a(Protocol::HTTP1::Body::Chunked)
94+
95+
expect do
96+
body.read
97+
end.to raise_exception(Protocol::HTTP1::BadRequest)
98+
end
99+
end
100+
101+
with "invalid +integer chunk size" do
102+
def input
103+
<<~HTTP.gsub("\n", "\r\n")
104+
POST / HTTP/1.1
105+
Host: a.com
106+
Transfer-Encoding: chunked
107+
Connection: close
108+
109+
+10
110+
0123456789abcdef
111+
0
112+
HTTP
113+
end
114+
115+
it "should fail to parse the request body" do
116+
authority, method, target, version, headers, body = server.read_request
117+
118+
expect(body).to be_a(Protocol::HTTP1::Body::Chunked)
119+
120+
expect do
121+
body.read
122+
end.to raise_exception(Protocol::HTTP1::BadRequest)
123+
end
124+
end
125+
end

0 commit comments

Comments
 (0)