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

Rack's multipart parser buffers unbounded per-part headers, enabling DoS (memory exhaustion)

CVE-2025-61772

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.

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
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
    
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
    

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.