Skip to content

Commit 02a962f

Browse files
hsbtclaude
andcommitted
Reject CR/LF in multipart field name, filename, and content type
encode_multipart_form_data interpolated the field name, filename, and per-part content type into Content-Disposition and Content-Type lines with only quote_string escaping backslash and double quote, so CR/LF in any of them could forge part headers and tamper with the request. Fixes #195 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5fe0f96 commit 02a962f

2 files changed

Lines changed: 20 additions & 0 deletions

File tree

lib/net/http/generic_request.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,9 @@ def encode_multipart_form_data(out, params, opt)
350350
if filename
351351
filename = quote_string(filename, charset)
352352
type = h[:content_type] || 'application/octet-stream'
353+
if /[\r\n]/.match?(type)
354+
raise ArgumentError, "field content type cannot include CR/LF"
355+
end
353356
buf << "Content-Disposition: form-data; " \
354357
"name=\"#{key}\"; filename=\"#{filename}\"\r\n" \
355358
"Content-Type: #{type}\r\n\r\n"
@@ -384,6 +387,9 @@ def encode_multipart_form_data(out, params, opt)
384387

385388
def quote_string(str, charset)
386389
str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset
390+
if /[\r\n]/.match?(str)
391+
raise ArgumentError, "multipart field name or filename cannot include CR/LF"
392+
end
387393
str.gsub(/[\\"]/, '\\\\\&')
388394
end
389395

test/net/http/test_http.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,20 @@ def test_set_form_with_file
931931
}
932932
}
933933
end
934+
935+
def test_set_form_multipart_crlf_injection
936+
build = ->(data) {
937+
req = Net::HTTP::Post.new('/')
938+
req.set_form(data, 'multipart/form-data')
939+
out = +''
940+
req.send(:encode_multipart_form_data, out, req.instance_variable_get(:@body_data), {})
941+
}
942+
assert_raise(ArgumentError) { build.call([["foo\r\nX-Injected: 1", 'v']]) }
943+
assert_raise(ArgumentError) { build.call([['f', 'v', {filename: "a\r\nX-Injected: 1"}]]) }
944+
assert_raise(ArgumentError) do
945+
build.call([['f', 'v', {filename: 'a', content_type: "text/plain\r\nX-Injected: 1"}]])
946+
end
947+
end
934948
end
935949

936950
class TestNetHTTP_v1_2 < Test::Unit::TestCase

0 commit comments

Comments
 (0)