Skip to content

Commit 9aac375

Browse files
jhawthorntenderlove
authored andcommitted
Limit all multipart parts, not just files
Previously we would limit the number of multipart parts which were files, but not other parts. In some cases this could cause parsing of maliciously crafted inputs to take longer than expected. [CVE-2023-27530]
1 parent 2606ac5 commit 9aac375

File tree

5 files changed

+76
-12
lines changed

5 files changed

+76
-12
lines changed

README.rdoc

+17-3
Original file line numberDiff line numberDiff line change
@@ -202,16 +202,30 @@ Limiting the depth prevents a possible stack overflow when parsing parameters.
202202

203203
Defaults to 100.
204204

205-
=== multipart_part_limit
205+
=== multipart_file_limit
206206

207-
The maximum number of parts a request can contain.
207+
The maximum number of parts with a filename a request can contain.
208208
Accepting too many part can lead to the server running out of file handles.
209209

210210
The default is 128, which means that a single request can't upload more than 128 files at once.
211211

212212
Set to 0 for no limit.
213213

214-
Can also be set via the +RACK_MULTIPART_PART_LIMIT+ environment variable.
214+
Can also be set via the +RACK_MULTIPART_FILE_LIMIT+ environment variable.
215+
216+
(This is also aliased as +multipart_part_limit+ and +RACK_MULTIPART_PART_LIMIT+ for compatibility)
217+
218+
=== multipart_total_part_limit
219+
220+
The maximum total number of parts a request can contain of any type, including
221+
both file and non-file form fields.
222+
223+
The default is 4096, which means that a single request can't contain more than
224+
4096 parts.
225+
226+
Set to 0 for no limit.
227+
228+
Can also be set via the +RACK_MULTIPART_TOTAL_PART_LIMIT+ environment variable.
215229

216230
== Changelog
217231

lib/rack/multipart/parser.rb

+15-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
module Rack
66
module Multipart
77
class MultipartPartLimitError < Errno::EMFILE; end
8+
class MultipartTotalPartLimitError < StandardError; end
89

910
class Parser
1011
(require_relative '../core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
@@ -140,7 +141,7 @@ def on_mime_head(mime_index, head, filename, content_type, name)
140141

141142
@mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
142143

143-
check_open_files
144+
check_part_limits
144145
end
145146

146147
def on_mime_body(mime_index, content)
@@ -152,13 +153,23 @@ def on_mime_finish(mime_index)
152153

153154
private
154155

155-
def check_open_files
156-
if Utils.multipart_part_limit > 0
157-
if @open_files >= Utils.multipart_part_limit
156+
def check_part_limits
157+
file_limit = Utils.multipart_file_limit
158+
part_limit = Utils.multipart_total_part_limit
159+
160+
if file_limit && file_limit > 0
161+
if @open_files >= file_limit
158162
@mime_parts.each(&:close)
159163
raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
160164
end
161165
end
166+
167+
if part_limit && part_limit > 0
168+
if @mime_parts.size >= part_limit
169+
@mime_parts.each(&:close)
170+
raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
171+
end
172+
end
162173
end
163174
end
164175

lib/rack/utils.rb

+15-4
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,24 @@ def unescape(s, encoding = Encoding::UTF_8)
5858
end
5959

6060
class << self
61-
attr_accessor :multipart_part_limit
61+
attr_accessor :multipart_total_part_limit
62+
63+
attr_accessor :multipart_file_limit
64+
65+
# multipart_part_limit is the original name of multipart_file_limit, but
66+
# the limit only counts parts with filenames.
67+
alias multipart_part_limit multipart_file_limit
68+
alias multipart_part_limit= multipart_file_limit=
6269
end
6370

64-
# The maximum number of parts a request can contain. Accepting too many part
65-
# can lead to the server running out of file handles.
71+
# The maximum number of file parts a request can contain. Accepting too
72+
# many parts can lead to the server running out of file handles.
6673
# Set to `0` for no limit.
67-
self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
74+
self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
75+
76+
# The maximum total number of parts a request can contain. Accepting too
77+
# many can lead to excessive memory use and parsing time.
78+
self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
6879

6980
def self.param_depth_limit
7081
default_query_parser.param_depth_limit

test/spec_multipart.rb

+12
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,18 @@ def initialize(*)
632632
end
633633
end
634634

635+
it "reach a multipart total limit" do
636+
begin
637+
previous_limit = Rack::Utils.multipart_total_part_limit
638+
Rack::Utils.multipart_total_part_limit = 5
639+
640+
env = Rack::MockRequest.env_for '/', multipart_fixture(:three_files_three_fields)
641+
lambda { Rack::Multipart.parse_multipart(env) }.must_raise Rack::Multipart::MultipartTotalPartLimitError
642+
ensure
643+
Rack::Utils.multipart_total_part_limit = previous_limit
644+
end
645+
end
646+
635647
it "return nil if no UploadedFiles were used" do
636648
data = Rack::Multipart.build_multipart("people" => [{ "submit-name" => "Larry", "files" => "contents" }])
637649
data.must_be_nil

test/spec_request.rb

+17-1
Original file line numberDiff line numberDiff line change
@@ -1000,7 +1000,7 @@ def initialize(*)
10001000
f[:tempfile].size.must_equal 76
10011001
end
10021002

1003-
it "MultipartPartLimitError when request has too many multipart parts if limit set" do
1003+
it "MultipartPartLimitError when request has too many multipart file parts if limit set" do
10041004
begin
10051005
data = 10000.times.map { "--AaB03x\r\nContent-Type: text/plain\r\nContent-Disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n")
10061006
data += "--AaB03x--\r"
@@ -1016,6 +1016,22 @@ def initialize(*)
10161016
end
10171017
end
10181018

1019+
it "MultipartPartLimitError when request has too many multipart total parts if limit set" do
1020+
begin
1021+
data = 10000.times.map { "--AaB03x\r\ncontent-type: text/plain\r\ncontent-disposition: attachment; name=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n")
1022+
data += "--AaB03x--\r"
1023+
1024+
options = {
1025+
"CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
1026+
"CONTENT_LENGTH" => data.length.to_s,
1027+
:input => StringIO.new(data)
1028+
}
1029+
1030+
request = make_request Rack::MockRequest.env_for("/", options)
1031+
lambda { request.POST }.must_raise Rack::Multipart::MultipartTotalPartLimitError
1032+
end
1033+
end
1034+
10191035
it 'closes tempfiles it created in the case of too many created' do
10201036
begin
10211037
data = 10000.times.map { "--AaB03x\r\nContent-Type: text/plain\r\nContent-Disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n")

0 commit comments

Comments
 (0)