Rack's multipart parser buffers unbounded per-part headers, 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 can accumulate unbounded data when a multipart part’s header block never terminates with the required blank line (CRLFCRLF). The parser keeps appending incoming bytes to memory without a size cap, allowing a remote attacker to exhaust memory and cause a denial of service (DoS). Attackers can send incomplete multipart headers to trigger high memory use, leading to process termination (OOM) or severe slowdown. The effect scales with request size limits and concurrency. All applications handling multipart uploads may be affected. Versions 2.2.19, 3.1.17, and 3.2.2 cap per-part header size (e.g., 64 KiB). As a workaround, restrict maximum request sizes at the proxy or web server layer (e.g., Nginx client_max_body_size).
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
3d869fed663b1Fix 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
589127f4ac8bFix 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
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-wpv5-97wm-hp9cghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-61772ghsaADVISORY
- 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-wpv5-97wm-hp9cghsax_refsource_CONFIRMWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/rack/CVE-2025-61772.ymlghsaWEB
News mentions
0No linked articles in our index yet.