VYPR
High severityNVD Advisory· Published Oct 7, 2025· Updated Oct 7, 2025

Rack's multipart parser buffers large non‑file fields entirely in memory, enabling DoS (memory exhaustion)

CVE-2025-61771

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.

PackageAffected versionsPatched versions
rackRubyGems
< 2.2.192.2.19
rackRubyGems
>= 3.1, < 3.1.173.1.17
rackRubyGems
>= 3.2, < 3.2.23.2.2

Affected products

1

Patches

3
589127f4ac8b

Fix denial of service vulnerbilties in multipart parsing

https://github.com/rack/rackJeremy EvansSep 16, 2025via ghsa
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
    
d869fed663b1

Fix denial of service vulnerbilties in multipart parsing

https://github.com/rack/rackJeremy EvansSep 16, 2025via ghsa
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'
    
e08f78c656c9

Fix denial of service vulnerbilties in multipart parsing

https://github.com/rack/rackJeremy EvansSep 16, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.