Skip to content

Commit ec5eeb5

Browse files
authored
net.http: fix panic in parse_multipart_form for invalid boundary (fix #24974) (#24976)
1 parent 34cfe19 commit ec5eeb5

3 files changed

Lines changed: 63 additions & 33 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module http
2+
3+
fn test_build_request_headers_with_empty_body_adds_content_length_zero() {
4+
// Create a request with no data.
5+
mut req := Request{}
6+
// Build the headers for it. Ensure that Content-Length: 0 is added
7+
// for requests without a body, which is required by some servers.
8+
// We use a POST request, as it is most likely to be affected by this.
9+
headers := req.build_request_headers(.post, 'localhost', 80, '/')
10+
assert headers.contains('Content-Length: 0\r\n')
11+
}

‎vlib/net/http/request.v‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -566,9 +566,10 @@ pub fn parse_multipart_form(body string, boundary string) (map[string]string, ma
566566
mut files := map[string][]FileData{}
567567
// TODO: do not use split, but only indexes, to reduce copying of potentially large data
568568
sections := body.split(boundary)
569-
fields := sections[1..sections.len - 1]
569+
fields := sections#[1..sections.len - 1]
570+
mut line_segments := []LineSegmentIndexes{cap: 100}
570571
for field in fields {
571-
mut line_segments := []LineSegmentIndexes{cap: 100}
572+
line_segments.clear()
572573
mut line_idx, mut line_start := 0, 0
573574
for cidx, c in field {
574575
if line_idx >= 6 {
@@ -582,6 +583,9 @@ pub fn parse_multipart_form(body string, boundary string) (map[string]string, ma
582583
}
583584
}
584585
line_segments << LineSegmentIndexes{line_start, field.len}
586+
if line_segments.len < 2 {
587+
continue
588+
}
585589
line1 := field[line_segments[1].start..line_segments[1].end]
586590
line2 := field[line_segments[2].start..line_segments[2].end]
587591
disposition := parse_disposition(line1.trim_space())

‎vlib/net/http/request_test.v‎

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// vtest build: !windows
2-
module http
3-
2+
import net.http
43
import io
54

65
struct StringReader {
@@ -30,40 +29,40 @@ fn reader(s string) &io.BufferedReader {
3029

3130
fn test_parse_request_not_http() {
3231
mut reader__ := reader('hello')
33-
parse_request(mut reader__) or { return }
32+
http.parse_request(mut reader__) or { return }
3433
panic('should not have parsed')
3534
}
3635

3736
fn test_parse_request_no_headers() {
3837
mut reader_ := reader('GET / HTTP/1.1\r\n\r\n')
39-
req := parse_request(mut reader_) or { panic('did not parse: ${err}') }
38+
req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') }
4039
assert req.method == .get
4140
assert req.url == '/'
4241
assert req.version == .v1_1
4342
}
4443

4544
fn test_parse_request_two_headers() {
4645
mut reader_ := reader('GET / HTTP/1.1\r\nTest1: a\r\nTest2: B\r\n\r\n')
47-
req := parse_request(mut reader_) or { panic('did not parse: ${err}') }
46+
req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') }
4847
assert req.header.custom_values('Test1') == ['a']
4948
assert req.header.custom_values('Test2') == ['B']
5049
}
5150

5251
fn test_parse_request_two_header_values() {
5352
mut reader_ := reader('GET / HTTP/1.1\r\nTest1: a; b\r\nTest2: c\r\nTest2: d\r\n\r\n')
54-
req := parse_request(mut reader_) or { panic('did not parse: ${err}') }
53+
req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') }
5554
assert req.header.custom_values('Test1') == ['a; b']
5655
assert req.header.custom_values('Test2') == ['c', 'd']
5756
}
5857

5958
fn test_parse_request_body() {
6059
mut reader_ := reader('GET / HTTP/1.1\r\nTest1: a\r\nTest2: b\r\nContent-Length: 4\r\n\r\nbodyabc')
61-
req := parse_request(mut reader_) or { panic('did not parse: ${err}') }
60+
req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') }
6261
assert req.data == 'body'
6362
}
6463

6564
fn test_parse_request_line() {
66-
method, target, version := parse_request_line('GET /target HTTP/1.1') or {
65+
method, target, version := http.parse_request_line('GET /target HTTP/1.1') or {
6766
panic('did not parse: ${err}')
6867
}
6968
assert method == .get
@@ -72,34 +71,34 @@ fn test_parse_request_line() {
7271
}
7372

7473
fn test_parse_form() {
75-
assert parse_form('foo=bar&bar=baz') == {
74+
assert http.parse_form('foo=bar&bar=baz') == {
7675
'foo': 'bar'
7776
'bar': 'baz'
7877
}
79-
assert parse_form('foo=bar=&bar=baz') == {
78+
assert http.parse_form('foo=bar=&bar=baz') == {
8079
'foo': 'bar='
8180
'bar': 'baz'
8281
}
83-
assert parse_form('foo=bar%3D&bar=baz') == {
82+
assert http.parse_form('foo=bar%3D&bar=baz') == {
8483
'foo': 'bar='
8584
'bar': 'baz'
8685
}
87-
assert parse_form('foo=b%26ar&bar=baz') == {
86+
assert http.parse_form('foo=b%26ar&bar=baz') == {
8887
'foo': 'b&ar'
8988
'bar': 'baz'
9089
}
91-
assert parse_form('a=b& c=d') == {
90+
assert http.parse_form('a=b& c=d') == {
9291
'a': 'b'
9392
' c': 'd'
9493
}
95-
assert parse_form('a=b&c= d ') == {
94+
assert http.parse_form('a=b&c= d ') == {
9695
'a': 'b'
9796
'c': ' d '
9897
}
99-
assert parse_form('{json}') == {
98+
assert http.parse_form('{json}') == {
10099
'json': '{json}'
101100
}
102-
assert parse_form('{
101+
assert http.parse_form('{
103102
"_id": "76c",
104103
"friends": [
105104
{
@@ -139,10 +138,10 @@ Content-Disposition: form-data; name=\"${names[1]}\"\r
139138
${contents[1]}\r
140139
--${boundary}--\r
141140
"
142-
form, files := parse_multipart_form(data, boundary)
141+
form, files := http.parse_multipart_form(data, boundary)
143142
assert files == {
144143
names[0]: [
145-
FileData{
144+
http.FileData{
146145
filename: file
147146
content_type: ct
148147
data: contents[0]
@@ -167,7 +166,7 @@ Content-Disposition: form-data; name="password"\r
167166
admin123\r
168167
--${boundary}--\r
169168
'
170-
form, files := parse_multipart_form(data, boundary)
169+
form, files := http.parse_multipart_form(data, boundary)
171170
for k, v in form {
172171
eprintln('> k: ${k} | v: ${v}')
173172
eprintln('>> k.bytes(): ${k.bytes()}')
@@ -180,7 +179,7 @@ admin123\r
180179
fn test_multipart_form_body() {
181180
files := {
182181
'foo': [
183-
FileData{
182+
http.FileData{
184183
filename: 'bar.v'
185184
content_type: 'application/octet-stream'
186185
data: 'baz'
@@ -191,8 +190,8 @@ fn test_multipart_form_body() {
191190
'fooz': 'buzz'
192191
}
193192

194-
body, boundary := multipart_form_body(form, files)
195-
parsed_form, parsed_files := parse_multipart_form(body, boundary)
193+
body, boundary := http.multipart_form_body(form, files)
194+
parsed_form, parsed_files := http.parse_multipart_form(body, boundary)
196195
assert parsed_files == files
197196
assert parsed_form == form
198197
}
@@ -201,17 +200,33 @@ fn test_parse_large_body() {
201200
body := 'A'.repeat(10_001) // greater than max_bytes
202201
req := 'GET / HTTP/1.1\r\nContent-Length: ${body.len}\r\n\r\n${body}'
203202
mut reader_ := reader(req)
204-
result := parse_request(mut reader_)!
203+
result := http.parse_request(mut reader_)!
205204
assert result.data.len == body.len
206205
assert result.data == body
207206
}
208207

209-
fn test_build_request_headers_with_empty_body_adds_content_length_zero() {
210-
// Create a request with no data.
211-
mut req := Request{}
212-
// Build the headers for it. Ensure that Content-Length: 0 is added
213-
// for requests without a body, which is required by some servers.
214-
// We use a POST request, as it is most likely to be affected by this.
215-
headers := req.build_request_headers(.post, 'localhost', 80, '/')
216-
assert headers.contains('Content-Length: 0\r\n')
208+
fn test_parse_multipart_form_empty_body() {
209+
body := ''
210+
boundary := '----WebKitFormBoundaryQcBIkwnOACVsvR8b'
211+
form, files := http.parse_multipart_form(body, boundary)
212+
assert form.len == 0
213+
assert files.len == 0
214+
}
215+
216+
fn test_parse_multipart_form_issue_24974_raw() {
217+
body := r'------WebKiormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="michael-sum-LEpfefQf4rU-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="mikhail-vasilyev-IFxjDdqK_0U-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b--\r\n'
218+
boundary := r'----WebKitFormBoundaryQcBIkwnOACVsvR8b'
219+
form, files := http.parse_multipart_form(body, boundary)
220+
assert form.len == 0
221+
assert files.len == 0
222+
}
223+
224+
fn test_parse_multipart_form_issue_24974_cooked() {
225+
body := '------WebKiormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="michael-sum-LEpfefQf4rU-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="mikhail-vasilyev-IFxjDdqK_0U-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b--\r\n'
226+
boundary := '----WebKitFormBoundaryQcBIkwnOACVsvR8b'
227+
form, files := http.parse_multipart_form(body, boundary)
228+
assert form.len == 0
229+
assert files.len == 1
230+
assert files['files'][0].filename == 'mikhail-vasilyev-IFxjDdqK_0U-unsplash.jpg'
231+
assert files['files'][0].content_type == 'image/jpeg'
217232
}

0 commit comments

Comments
 (0)