Skip to content

Commit 367a2a0

Browse files
th4s1sioquatix
authored andcommitted
Add Content-Length size check in Rack::Multipart::Parser
Compare the declared `Content-Length` against a configurable maximum (`PARSER_BYTESIZE_LIMIT`) before any parsing begins. If it exceeds the limit, raise an exception immediately.
1 parent a17cb99 commit 367a2a0

3 files changed

Lines changed: 72 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file. For info on
1616
- [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
1717
- [CVE-2026-34830](https://github.com/advisories/GHSA-qv7j-4883-hwh7) `Rack::Sendfile` header-based `X-Accel-Mapping` regex injection enables unauthorized `X-Accel-Redirect`.
1818
- [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
19+
- [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
1920

2021
## [3.1.20] - 2026-02-16
2122

lib/rack/multipart/parser.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ class Parser
6868
BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
6969
private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
7070

71+
bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
72+
PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
73+
private_constant :PARSER_BYTESIZE_LIMIT
74+
7175
class BoundedIO # :nodoc:
7276
def initialize(io, content_length)
7377
@io = io
@@ -121,6 +125,10 @@ def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
121125
boundary = parse_boundary content_type
122126
return EMPTY unless boundary
123127

128+
if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
129+
raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
130+
end
131+
124132
if boundary.length > 70
125133
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
126134
# Most clients use no more than 55 characters.
@@ -237,6 +245,7 @@ def initialize(boundary, tempfile, bufsize, query_parser)
237245
@mime_index = 0
238246
@body_retained = nil
239247
@retained_size = 0
248+
@total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
240249
@collector = Collector.new tempfile
241250

242251
@sbuf = StringScanner.new("".dup)
@@ -248,6 +257,7 @@ def initialize(boundary, tempfile, bufsize, query_parser)
248257
end
249258

250259
def parse(io)
260+
@total_bytes_read &&= nil if io.is_a?(BoundedIO)
251261
outbuf = String.new
252262
read_data(io, outbuf)
253263

@@ -291,6 +301,12 @@ def dequote(str) # From WEBrick::HTTPUtils
291301
def read_data(io, outbuf)
292302
content = io.read(@bufsize, outbuf)
293303
handle_empty_content!(content)
304+
if @total_bytes_read
305+
@total_bytes_read += content.bytesize
306+
if @total_bytes_read > PARSER_BYTESIZE_LIMIT
307+
raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
308+
end
309+
end
294310
@sbuf.concat(content)
295311
end
296312

test/spec_multipart.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ def multipart_file(name)
2929
File.join(File.dirname(__FILE__), "multipart", name.to_s)
3030
end
3131

32+
def with_multipart_limit(limit)
33+
previous = Rack::Multipart::Parser.send(:const_get, :PARSER_BYTESIZE_LIMIT)
34+
begin
35+
Rack::Multipart::Parser.send(:remove_const, :PARSER_BYTESIZE_LIMIT)
36+
Rack::Multipart::Parser.const_set(:PARSER_BYTESIZE_LIMIT, limit)
37+
yield
38+
ensure
39+
Rack::Multipart::Parser.send(:remove_const, :PARSER_BYTESIZE_LIMIT)
40+
Rack::Multipart::Parser.const_set(:PARSER_BYTESIZE_LIMIT, previous)
41+
end
42+
end
43+
3244
it "returns nil if the content type is not multipart" do
3345
env = Rack::MockRequest.env_for("/", "CONTENT_TYPE" => 'application/x-www-form-urlencoded', :input => "")
3446
Rack::Multipart.parse_multipart(env).must_be_nil
@@ -64,6 +76,49 @@ def multipart_file(name)
6476
}.must_raise Rack::Multipart::Error
6577
end
6678

79+
it "raises an exception if Content-Length exceeds total bytesize limit" do
80+
with_multipart_limit(1024) do
81+
env = Rack::MockRequest.env_for("/",
82+
"CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
83+
"CONTENT_LENGTH" => "2048",
84+
:input => StringIO.new("--AaB03x--\r\n"))
85+
86+
lambda {
87+
Rack::Multipart.parse_multipart(env)
88+
}.must_raise(Rack::Multipart::Error)
89+
end
90+
end
91+
92+
it "allows requests within the total bytesize limit" do
93+
with_multipart_limit(1024 * 1024) do
94+
env = Rack::MockRequest.env_for("/", multipart_fixture(:text))
95+
params = Rack::Multipart.parse_multipart(env)
96+
params["submit-name"].must_equal "Larry"
97+
end
98+
end
99+
100+
it "skips total bytesize check when there is no limit" do
101+
with_multipart_limit(nil) do
102+
env = Rack::MockRequest.env_for("/", multipart_fixture(:text))
103+
params = Rack::Multipart.parse_multipart(env)
104+
params["submit-name"].must_equal "Larry"
105+
end
106+
end
107+
108+
it "enforces total bytesize limit during streaming when Content-Length is absent" do
109+
with_multipart_limit(1) do
110+
# Even without Content-Length, the streaming check catches oversized uploads
111+
fixture = multipart_fixture(:text)
112+
fixture.delete("CONTENT_LENGTH")
113+
env = Rack::MockRequest.env_for("/", fixture)
114+
env.delete("CONTENT_LENGTH")
115+
116+
lambda {
117+
Rack::Multipart.parse_multipart(env)
118+
}.must_raise Rack::Multipart::Error
119+
end
120+
end
121+
67122
it "raises a bad request exception if no body is given but content type indicates a multipart body" do
68123
env = Rack::MockRequest.env_for("/", "CONTENT_TYPE" => 'multipart/form-data; boundary=BurgerBurger', :input => nil)
69124
lambda {

0 commit comments

Comments
 (0)