Skip to content

Commit 44c480b

Browse files
hsbtclaude
andcommitted
Reject CR/LF in multipart boundary
A custom boundary is interpolated into the part separators as-is, so a boundary containing CR/LF could forge part headers in the same way as the field name and filename. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent c07d658 commit 44c480b

2 files changed

Lines changed: 6 additions & 2 deletions

File tree

lib/net/http/generic_request.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,9 @@ def encode_multipart_form_data(out, params, opt)
336336
boundary = opt[:boundary]
337337
require 'securerandom' unless defined?(SecureRandom)
338338
boundary ||= SecureRandom.urlsafe_base64(40)
339+
if /[\r\n]/.match?(boundary.to_s)
340+
raise ArgumentError, "multipart boundary cannot include CR/LF"
341+
end
339342
chunked_p = chunked?
340343

341344
buf = +''

test/net/http/test_http.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -933,13 +933,14 @@ def test_set_form_with_file
933933
end
934934

935935
def test_set_form_multipart_crlf_injection
936-
build = ->(data) {
936+
build = ->(data, opt = {}) {
937937
req = Net::HTTP::Post.new('/')
938938
req.set_form(data, 'multipart/form-data')
939939
out = +''
940-
req.send(:encode_multipart_form_data, out, req.instance_variable_get(:@body_data), {})
940+
req.send(:encode_multipart_form_data, out, req.instance_variable_get(:@body_data), opt)
941941
}
942942
assert_raise(ArgumentError) { build.call([["foo\r\nX-Injected: 1", 'v']]) }
943+
assert_raise(ArgumentError) { build.call([['f', 'v']], boundary: "abc\r\nX-Injected: 1") }
943944
assert_raise(ArgumentError) { build.call([['f', 'v', {filename: "a\r\nX-Injected: 1"}]]) }
944945
assert_raise(ArgumentError) do
945946
build.call([['f', 'v', {filename: 'a', content_type: "text/plain\r\nX-Injected: 1"}]])

0 commit comments

Comments
 (0)