Rack's multipart parser buffers large non‑file fields entirely in memory, enabling DoS (memory exhaustion)
Description
Rack is a modular Ruby web server interface. In versions prior to 2.2.19, 3.1.17, and 3.2.2, `Rack::Multipart::Parser stores non-file form fields (parts without a filename) entirely in memory as Ruby String objects. A single large text field in a multipart/form-data request (hundreds of megabytes or more) can consume equivalent process memory, potentially leading to out-of-memory (OOM) conditions and denial of service (DoS). Attackers can send large non-file fields to trigger excessive memory usage. Impact scales with request size and concurrency, potentially leading to worker crashes or severe garbage-collection overhead. All Rack applications processing multipart form submissions are affected. Versions 2.2.19, 3.1.17, and 3.2.2 enforce a reasonable size cap for non-file fields (e.g., 2 MiB). Workarounds include restricting maximum request body size at the web-server or proxy layer (e.g., Nginx client_max_body_size) and validating and rejecting unusually large form fields at the application level.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
rackRubyGems | < 2.2.19 | 2.2.19 |
rackRubyGems | >= 3.1, < 3.1.17 | 3.1.17 |
rackRubyGems | >= 3.2, < 3.2.2 | 3.2.2 |
Affected products
1Patches
3589127f4ac8bFix denial of service vulnerbilties in multipart parsing
3 files changed · +127 −3
CHANGELOG.md+28 −1 modified@@ -2,7 +2,12 @@ All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/). -## Unreleased +## [3.2.2] - 2025-10-07 + +### Security + +- [CVE-2025-61772](https://github.com/advisories/GHSA-wpv5-97wm-hp9c) Multipart parser buffers unbounded per-part headers, enabling DoS (memory exhaustion) +- [CVE-2025-61770](https://github.com/advisories/GHSA-p543-xpfm-54cp) Unbounded multipart preamble buffering enables DoS (memory exhaustion) ## [3.2.1] -- 2025-09-02 @@ -61,6 +66,14 @@ This release continues Rack's evolution toward a cleaner, more efficient foundat - `SERVER_NAME` and `HTTP_HOST` are now more strictly validated according to the relevant specifications. ([#2298](https://github.com/rack/rack/pull/2298), [@ioquatix]) - `Rack::Lint` now disallows `PATH_INFO="" SCRIPT_NAME=""`. ([#2298](https://github.com/rack/rack/issues/2307), [@jeremyevans]) +## [3.1.17] - 2025-10-07 + +### Security + +- [CVE-2025-61772](https://github.com/advisories/GHSA-wpv5-97wm-hp9c) Multipart parser buffers unbounded per-part headers, enabling DoS (memory exhaustion) +- [CVE-2025-61771](https://github.com/advisories/GHSA-w9pc-fmgc-vxvw) Multipart parser buffers large non‑file fields entirely in memory, enabling DoS (memory exhaustion) +- [CVE-2025-61770](https://github.com/advisories/GHSA-p543-xpfm-54cp) Unbounded multipart preamble buffering enables DoS (memory exhaustion) + ## [3.1.16] - 2025-06-04 ### Security @@ -430,6 +443,20 @@ This release introduces major improvements to Rack, including enhanced support f - Fix multipart filename generation for filenames that contain spaces. Encode spaces as "%20" instead of "+" which will be decoded properly by the multipart parser. ([#1736](https://github.com/rack/rack/pull/1645), [@muirdm](https://github.com/muirdm)) - `Rack::Request#scheme` returns `ws` or `wss` when one of the `X-Forwarded-Scheme` / `X-Forwarded-Proto` headers is set to `ws` or `wss`, respectively. ([#1730](https://github.com/rack/rack/issues/1730), [@erwanst](https://github.com/erwanst)) +## [2.2.19] - 2025-10-07 + +### Security + +- [CVE-2025-61772](https://github.com/advisories/GHSA-wpv5-97wm-hp9c) Multipart parser buffers unbounded per-part headers, enabling DoS (memory exhaustion) +- [CVE-2025-61771](https://github.com/advisories/GHSA-w9pc-fmgc-vxvw) Multipart parser buffers large non‑file fields entirely in memory, enabling DoS (memory exhaustion) +- [CVE-2025-61770](https://github.com/advisories/GHSA-p543-xpfm-54cp) Unbounded multipart preamble buffering enables DoS (memory exhaustion) + +## [2.2.18] - 2025-09-25 + +### Security + +- [CVE-2025-59830](https://github.com/rack/rack/security/advisories/GHSA-625h-95r8-8xpm) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion via semicolon-separated parameters. + ## [2.2.17] - 2025-06-03 - Backport `Rack::MediaType#params` now handles parameters without values. ([#2263](https://github.com/rack/rack/pull/2263), [@AllyMarthaJ](https://github.com/AllyMarthaJ))
lib/rack/multipart/parser.rb+15 −1 modified@@ -59,6 +59,12 @@ class Parser Tempfile.new(["RackMultipart", extension]) } + BOUNDARY_START_LIMIT = 16 * 1024 + private_constant :BOUNDARY_START_LIMIT + + MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024 + private_constant :MIME_HEADER_BYTESIZE_LIMIT + class BoundedIO # :nodoc: def initialize(io, content_length) @io = io @@ -294,6 +300,10 @@ def handle_fast_forward # retry for opening boundary else + # We raise if we don't find the multipart boundary, to avoid unbounded memory + # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default) + raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT + # no boundary found, keep reading data return :want_read end @@ -413,7 +423,11 @@ def handle_mime_head @collector.on_mime_head @mime_index, head, filename, content_type, name @state = :MIME_BODY else - :want_read + # We raise if the mime part header is too large, to avoid unbounded memory + # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default) + raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT + + return :want_read end end
test/spec_multipart.rb+84 −1 modified@@ -203,7 +203,90 @@ def rd.rewind; end env = Rack::MockRequest.env_for '/', fixture lambda { Rack::Multipart.parse_multipart(env) - }.must_raise Rack::Multipart::EmptyContentError + }.must_raise Rack::Multipart::Error + rd.close + + err = thr.value + err.must_be_instance_of Errno::EPIPE + wr.close + end + + it "rejects excessive data before boundary" do + rd, wr = IO.pipe + def rd.rewind; end + wr.sync = true + + thr = Thread.new do + begin + longer = "0123456789" * 1024 * 1024 + (1024 * 1024).times do + wr.write(longer) + end + + wr.write("\r\n\r\n--AaB03x") + wr.write("\r\n") + wr.write('content-disposition: form-data; name="a"; filename="a.txt"') + wr.write("\r\n") + wr.write("content-type: text/plain\r\n") + wr.write("\r\na") + wr.write("--AaB03x--\r\n") + wr.close + rescue => err # this is EPIPE if Rack shuts us down + err + end + end + + fixture = { + "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x", + "CONTENT_LENGTH" => (1024 * 1024 * 8).to_s, + :input => rd, + } + + env = Rack::MockRequest.env_for '/', fixture + lambda { + Rack::Multipart.parse_multipart(env) + }.must_raise(Rack::Multipart::Error).message.must_equal "multipart boundary not found within limit" + rd.close + + err = thr.value + err.must_be_instance_of Errno::EPIPE + wr.close + end + + it "rejects excessive mime header size" do + rd, wr = IO.pipe + def rd.rewind; end + wr.sync = true + + thr = Thread.new do + begin + wr.write("\r\n\r\n--AaB03x") + wr.write("\r\n") + wr.write('content-disposition: form-data; name="a"; filename="a.txt"') + wr.write("\r\n") + wr.write("content-type: text/plain\r\n") + longer = "0123456789" * 1024 * 1024 + (1024 * 1024).times do + wr.write(longer) + end + wr.write("\r\na") + wr.write("--AaB03x--\r\n") + wr.close + rescue => err # this is EPIPE if Rack shuts us down + err + end + end + + fixture = { + "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x", + "CONTENT_LENGTH" => (1024 * 1024 * 8).to_s, + :input => rd, + } + + env = Rack::MockRequest.env_for '/', fixture + lambda { + Rack::Multipart.parse_multipart(env) + }.must_raise(Rack::Multipart::Error).message.must_equal "multipart mime part header too large" rd.close err = thr.value
d869fed663b1Fix denial of service vulnerbilties in multipart parsing
3 files changed · +110 −2
CHANGELOG.md+9 −0 modified@@ -2,8 +2,17 @@ All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/). +## [2.2.19] - 2025-10-07 + +### Security + +- [CVE-2025-61772](https://github.com/advisories/GHSA-wpv5-97wm-hp9c) Multipart parser buffers unbounded per-part headers, enabling DoS (memory exhaustion) +- [CVE-2025-61770](https://github.com/advisories/GHSA-p543-xpfm-54cp) Unbounded multipart preamble buffering enables DoS (memory exhaustion) + ## [2.2.18] - 2025-09-25 +### Security + - [CVE-2025-59830](https://github.com/rack/rack/security/advisories/GHSA-625h-95r8-8xpm) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion via semicolon-separated parameters. ## [2.2.17] - 2025-06-03
lib/rack/multipart/parser.rb+18 −2 modified@@ -20,6 +20,12 @@ class Parser BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/ + BOUNDARY_START_LIMIT = 16 * 1024 + private_constant :BOUNDARY_START_LIMIT + + MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024 + private_constant :MIME_HEADER_BYTESIZE_LIMIT + class BoundedIO # :nodoc: def initialize(io, content_length) @io = io @@ -241,7 +247,13 @@ def handle_fast_forward @state = :MIME_HEAD else raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize - :want_read + + # We raise if we don't find the multipart boundary, to avoid unbounded memory + # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default) + raise EOFError, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT + + # no boundary found, keep reading data + return :want_read end end @@ -274,7 +286,11 @@ def handle_mime_head @collector.on_mime_head @mime_index, head, filename, content_type, name @state = :MIME_BODY else - :want_read + # We raise if the mime part header is too large, to avoid unbounded memory + # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default) + raise EOFError, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT + + return :want_read end end
test/spec_multipart.rb+83 −0 modified@@ -159,6 +159,89 @@ def rd.rewind; end wr.close end + it "rejects excessive data before boundary" do + rd, wr = IO.pipe + def rd.rewind; end + wr.sync = true + + thr = Thread.new do + begin + longer = "0123456789" * 1024 * 1024 + (1024 * 1024).times do + wr.write(longer) + end + + wr.write("\r\n\r\n--AaB03x") + wr.write("\r\n") + wr.write('content-disposition: form-data; name="a"; filename="a.txt"') + wr.write("\r\n") + wr.write("content-type: text/plain\r\n") + wr.write("\r\na") + wr.write("--AaB03x--\r\n") + wr.close + rescue => err # this is EPIPE if Rack shuts us down + err + end + end + + fixture = { + "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x", + "CONTENT_LENGTH" => (1024 * 1024 * 8).to_s, + :input => rd, + } + + env = Rack::MockRequest.env_for '/', fixture + lambda { + Rack::Multipart.parse_multipart(env) + }.must_raise(EOFError).message.must_equal "multipart boundary not found within limit" + rd.close + + err = thr.value + err.must_be_instance_of Errno::EPIPE + wr.close + end + + it "rejects excessive mime header size" do + rd, wr = IO.pipe + def rd.rewind; end + wr.sync = true + + thr = Thread.new do + begin + wr.write("\r\n\r\n--AaB03x") + wr.write("\r\n") + wr.write('content-disposition: form-data; name="a"; filename="a.txt"') + wr.write("\r\n") + wr.write("content-type: text/plain\r\n") + longer = "0123456789" * 1024 * 1024 + (1024 * 1024).times do + wr.write(longer) + end + wr.write("\r\na") + wr.write("--AaB03x--\r\n") + wr.close + rescue => err # this is EPIPE if Rack shuts us down + err + end + end + + fixture = { + "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x", + "CONTENT_LENGTH" => (1024 * 1024 * 8).to_s, + :input => rd, + } + + env = Rack::MockRequest.env_for '/', fixture + lambda { + Rack::Multipart.parse_multipart(env) + }.must_raise(EOFError).message.must_equal "multipart mime part header too large" + rd.close + + err = thr.value + err.must_be_instance_of Errno::EPIPE + wr.close + end + # see https://github.com/rack/rack/pull/1309 it "parse strange multipart pdf" do boundary = '---------------------------932620571087722842402766118'
e08f78c656c9Fix denial of service vulnerbilties in multipart parsing
3 files changed · +124 −2
CHANGELOG.md+25 −0 modified@@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/). +## [3.1.17] - 2025-10-07 + +### Security + +- [CVE-2025-61772](https://github.com/advisories/GHSA-wpv5-97wm-hp9c) Multipart parser buffers unbounded per-part headers, enabling DoS (memory exhaustion) +- [CVE-2025-61770](https://github.com/advisories/GHSA-p543-xpfm-54cp) Unbounded multipart preamble buffering enables DoS (memory exhaustion) + ## [3.1.16] - 2025-06-04 ### Security @@ -365,6 +372,24 @@ Rack v3.1 is primarily a maintenance release that removes features deprecated in - Fix multipart filename generation for filenames that contain spaces. Encode spaces as "%20" instead of "+" which will be decoded properly by the multipart parser. ([#1736](https://github.com/rack/rack/pull/1645), [@muirdm](https://github.com/muirdm)) - `Rack::Request#scheme` returns `ws` or `wss` when one of the `X-Forwarded-Scheme` / `X-Forwarded-Proto` headers is set to `ws` or `wss`, respectively. ([#1730](https://github.com/rack/rack/issues/1730), [@erwanst](https://github.com/erwanst)) +## [2.2.19] - 2025-10-07 + +### Security + +- [CVE-2025-61772](https://github.com/advisories/GHSA-wpv5-97wm-hp9c) Multipart parser buffers unbounded per-part headers, enabling DoS (memory exhaustion) +- [CVE-2025-61771](https://github.com/advisories/GHSA-w9pc-fmgc-vxvw) Multipart parser buffers large non‑file fields entirely in memory, enabling DoS (memory exhaustion) +- [CVE-2025-61770](https://github.com/advisories/GHSA-p543-xpfm-54cp) Unbounded multipart preamble buffering enables DoS (memory exhaustion) + +## [2.2.18] - 2025-09-25 + +### Security + +- [CVE-2025-59830](https://github.com/rack/rack/security/advisories/GHSA-625h-95r8-8xpm) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion via semicolon-separated parameters. + +## [2.2.17] - 2025-06-03 + +- Backport `Rack::MediaType#params` now handles parameters without values. ([#2263](https://github.com/rack/rack/pull/2263), [@AllyMarthaJ](https://github.com/AllyMarthaJ)) + ## [2.2.16] - 2025-05-22 - Fix incorrect backport of optional `CGI::Cookie` support. ([#2335](https://github.com/rack/rack/pull/2335), [@jeremyevans])
lib/rack/multipart/parser.rb+15 −1 modified@@ -47,6 +47,12 @@ class Parser Tempfile.new(["RackMultipart", extension]) } + BOUNDARY_START_LIMIT = 16 * 1024 + private_constant :BOUNDARY_START_LIMIT + + MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024 + private_constant :MIME_HEADER_BYTESIZE_LIMIT + class BoundedIO # :nodoc: def initialize(io, content_length) @io = io @@ -287,6 +293,10 @@ def handle_fast_forward # retry for opening boundary else + # We raise if we don't find the multipart boundary, to avoid unbounded memory + # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default) + raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT + # no boundary found, keep reading data return :want_read end @@ -406,7 +416,11 @@ def handle_mime_head @collector.on_mime_head @mime_index, head, filename, content_type, name @state = :MIME_BODY else - :want_read + # We raise if the mime part header is too large, to avoid unbounded memory + # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default) + raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT + + return :want_read end end
test/spec_multipart.rb+84 −1 modified@@ -197,7 +197,90 @@ def rd.rewind; end env = Rack::MockRequest.env_for '/', fixture lambda { Rack::Multipart.parse_multipart(env) - }.must_raise Rack::Multipart::EmptyContentError + }.must_raise Rack::Multipart::Error + rd.close + + err = thr.value + err.must_be_instance_of Errno::EPIPE + wr.close + end + + it "rejects excessive data before boundary" do + rd, wr = IO.pipe + def rd.rewind; end + wr.sync = true + + thr = Thread.new do + begin + longer = "0123456789" * 1024 * 1024 + (1024 * 1024).times do + wr.write(longer) + end + + wr.write("\r\n\r\n--AaB03x") + wr.write("\r\n") + wr.write('content-disposition: form-data; name="a"; filename="a.txt"') + wr.write("\r\n") + wr.write("content-type: text/plain\r\n") + wr.write("\r\na") + wr.write("--AaB03x--\r\n") + wr.close + rescue => err # this is EPIPE if Rack shuts us down + err + end + end + + fixture = { + "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x", + "CONTENT_LENGTH" => (1024 * 1024 * 8).to_s, + :input => rd, + } + + env = Rack::MockRequest.env_for '/', fixture + lambda { + Rack::Multipart.parse_multipart(env) + }.must_raise(Rack::Multipart::Error).message.must_equal "multipart boundary not found within limit" + rd.close + + err = thr.value + err.must_be_instance_of Errno::EPIPE + wr.close + end + + it "rejects excessive mime header size" do + rd, wr = IO.pipe + def rd.rewind; end + wr.sync = true + + thr = Thread.new do + begin + wr.write("\r\n\r\n--AaB03x") + wr.write("\r\n") + wr.write('content-disposition: form-data; name="a"; filename="a.txt"') + wr.write("\r\n") + wr.write("content-type: text/plain\r\n") + longer = "0123456789" * 1024 * 1024 + (1024 * 1024).times do + wr.write(longer) + end + wr.write("\r\na") + wr.write("--AaB03x--\r\n") + wr.close + rescue => err # this is EPIPE if Rack shuts us down + err + end + end + + fixture = { + "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x", + "CONTENT_LENGTH" => (1024 * 1024 * 8).to_s, + :input => rd, + } + + env = Rack::MockRequest.env_for '/', fixture + lambda { + Rack::Multipart.parse_multipart(env) + }.must_raise(Rack::Multipart::Error).message.must_equal "multipart mime part header too large" rd.close err = thr.value
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-w9pc-fmgc-vxvwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-61771ghsaADVISORY
- github.com/rack/rack/commit/589127f4ac8b5cf11cf88fb0cd116ffed4d2181eghsax_refsource_MISCWEB
- github.com/rack/rack/commit/d869fed663b113b95a74ad53e1b5cae6ab31f29eghsax_refsource_MISCWEB
- github.com/rack/rack/commit/e08f78c656c9394d6737c022bde087e0f33336fdghsax_refsource_MISCWEB
- github.com/rack/rack/security/advisories/GHSA-w9pc-fmgc-vxvwghsax_refsource_CONFIRMWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/rack/CVE-2025-61771.ymlghsaWEB
News mentions
0No linked articles in our index yet.