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

Rack is vulnerable to a memory-exhaustion DoS through unbounded URL-encoded body parsing

CVE-2025-61919

Description

Rack is a modular Ruby web server interface. Prior to versions 2.2.20, 3.1.18, and 3.2.3, Rack::Request#POST reads the entire request body into memory for Content-Type: application/x-www-form-urlencoded, calling rack.input.read(nil) without enforcing a length or cap. Large request bodies can therefore be buffered completely into process memory before parsing, leading to denial of service (DoS) through memory exhaustion. Users should upgrade to Rack version 2.2.20, 3.1.18, or 3.2.3, anu of which enforces form parameter limits using query_parser.bytesize_limit, preventing unbounded reads of application/x-www-form-urlencoded bodies. Additionally, enforce strict maximum body size at the proxy or web server layer (e.g., Nginx client_max_body_size, Apache LimitRequestBody).

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
rackRubyGems
< 2.2.202.2.20
rackRubyGems
>= 3.0, < 3.1.183.1.18
rackRubyGems
>= 3.2, < 3.2.33.2.3

Affected products

1

Patches

3
4e2c903991a7

Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.

https://github.com/rack/rackSamuel WilliamsOct 9, 2025via ghsa
4 files changed · +95 2
  • CHANGELOG.md+1 0 modified
    @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. For info on
     ### Security
     
     - [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
    +- [CVE-2025-61919](https://github.com/advisories/GHSA-6xw4-3v39-52mm) Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.
     
     ## [2.2.19] - 2025-10-07
     
    
  • lib/rack/query_parser.rb+3 1 modified
    @@ -51,6 +51,8 @@ def self.make_default(key_space_limit, param_depth_limit, **options)
         PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
         private_constant :PARAMS_LIMIT
     
    +    attr_reader :bytesize_limit
    +
         def initialize(params_class, key_space_limit, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
           @params_class = params_class
           @key_space_limit = key_space_limit
    @@ -185,7 +187,7 @@ def params_hash_has_key?(hash, key)
         def check_query_string(qs, sep)
           if qs
             if qs.bytesize > @bytesize_limit
    -          raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
    +          raise QueryLimitError, "total query size exceeds limit (#{@bytesize_limit})"
             end
     
             if (param_count = qs.count(sep.is_a?(String) ? sep : '&;')) >= @params_limit
    
  • lib/rack/request.rb+4 1 modified
    @@ -444,7 +444,10 @@ def POST
               get_header(RACK_REQUEST_FORM_HASH)
             elsif form_data? || parseable_data?
               unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
    -            form_vars = get_header(RACK_INPUT).read
    +            # Add 2 bytes. One to check whether it is over the limit, and a second
    +            # in case the slice! call below removes the last byte
    +            # If read returns nil, use the empty string
    +            form_vars = get_header(RACK_INPUT).read(query_parser.bytesize_limit + 2) || ''
     
                 # Fix for Safari Ajax postings that always append \0
                 # form_vars.sub!(/\0\z/, '') # performance replacement:
    
  • test/spec_request.rb+87 0 modified
    @@ -499,6 +499,93 @@ def initialize(*)
         req.POST.must_equal "foo" => "bar", "quux" => "bla"
       end
     
    +  it "limit POST body read to bytesize_limit when parsing url-encoded data" do
    +    # Create a mock input that tracks read calls
    +    reads = []
    +    mock_input = Object.new
    +    mock_input.define_singleton_method(:read) do |len=nil|
    +      reads << len
    +      # Return mutable string
    +      "foo=bar".dup
    +    end
    +    mock_input.define_singleton_method(:rewind) do
    +      # no-op for compatibility
    +    end
    +
    +    request = make_request \
    +      Rack::MockRequest.env_for("/",
    +        'REQUEST_METHOD' => 'POST',
    +        'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    +        'rack.input' => mock_input)
    +
    +    request.POST.must_equal "foo" => "bar"
    +
    +    # Verify read was called with a limit (bytesize_limit + 2), not nil
    +    reads.size.must_equal 1
    +    reads.first.wont_be_nil
    +    reads.first.must_equal(request.send(:query_parser).bytesize_limit + 2)
    +  end
    +
    +  it "handle nil return from rack.input.read when parsing url-encoded data" do
    +    # Simulate an input that returns nil on read
    +    mock_input = Object.new
    +    mock_input.define_singleton_method(:read) do |len=nil|
    +      nil
    +    end
    +    mock_input.define_singleton_method(:rewind) do
    +      # no-op for compatibility
    +    end
    +
    +    request = make_request \
    +      Rack::MockRequest.env_for("/",
    +        'REQUEST_METHOD' => 'POST',
    +        'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    +        'rack.input' => mock_input)
    +
    +    # Should handle nil gracefully and return empty hash
    +    request.POST.must_equal({})
    +  end
    +
    +  it "truncate POST body at bytesize_limit when parsing url-encoded data" do
    +    # Create input larger than limit
    +    large_body = "a=1&" * 1000000  # Very large body
    +
    +    request = make_request \
    +      Rack::MockRequest.env_for("/",
    +        'REQUEST_METHOD' => 'POST',
    +        'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    +        :input => large_body)
    +
    +    # Should parse only up to the limit without reading entire body into memory
    +    # The actual parsing may fail due to size limit, which is expected
    +    proc { request.POST }.must_raise Rack::QueryParser::QueryLimitError
    +  end
    +
    +  it "clean up Safari's ajax POST body with limited read" do
    +    # Verify Safari null-byte cleanup still works with bounded read
    +    reads = []
    +    mock_input = Object.new
    +    mock_input.define_singleton_method(:read) do |len=nil|
    +      reads << len
    +      # Return mutable string (dup ensures it's not frozen)
    +      "foo=bar\0".dup
    +    end
    +    mock_input.define_singleton_method(:rewind) do
    +      # no-op for compatibility
    +    end
    +
    +    request = make_request \
    +      Rack::MockRequest.env_for("/",
    +        'REQUEST_METHOD' => 'POST',
    +        'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    +        'rack.input' => mock_input)
    +
    +    request.POST.must_equal "foo" => "bar"
    +
    +    # Verify bounded read was used
    +    reads.first.wont_be_nil
    +  end
    +
       it "get value by key from params with #[]" do
         req = make_request \
           Rack::MockRequest.env_for("?foo=quux")
    
cbd541e8a3d0

Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.

https://github.com/rack/rackSamuel WilliamsOct 9, 2025via ghsa
4 files changed · +86 2
  • CHANGELOG.md+1 0 modified
    @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. For info on
     ### Security
     
     - [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
    +- [CVE-2025-61919](https://github.com/advisories/GHSA-6xw4-3v39-52mm) Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.
     
     ## [3.1.17] - 2025-10-07
     
    
  • lib/rack/query_parser.rb+3 1 modified
    @@ -57,6 +57,8 @@ def self.make_default(param_depth_limit, **options)
         PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
         private_constant :PARAMS_LIMIT
     
    +    attr_reader :bytesize_limit
    +
         def initialize(params_class, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
           @params_class = params_class
           @param_depth_limit = param_depth_limit
    @@ -218,7 +220,7 @@ def params_hash_has_key?(hash, key)
         def check_query_string(qs, sep)
           if qs
             if qs.bytesize > @bytesize_limit
    -          raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
    +          raise QueryLimitError, "total query size exceeds limit (#{@bytesize_limit})"
             end
     
             if (param_count = qs.count(sep.is_a?(String) ? sep : '&')) >= @params_limit
    
  • lib/rack/request.rb+4 1 modified
    @@ -528,7 +528,10 @@ def POST
                   set_header RACK_REQUEST_FORM_PAIRS, pairs
                   set_header RACK_REQUEST_FORM_HASH, expand_param_pairs(pairs)
                 else
    -              form_vars = get_header(RACK_INPUT).read
    +              # Add 2 bytes. One to check whether it is over the limit, and a second
    +              # in case the slice! call below removes the last byte
    +              # If read returns nil, use the empty string
    +              form_vars = get_header(RACK_INPUT).read(query_parser.bytesize_limit + 2) || ''
     
                   # Fix for Safari Ajax postings that always append \0
                   # form_vars.sub!(/\0\z/, '') # performance replacement:
    
  • test/spec_request.rb+78 0 modified
    @@ -796,6 +796,84 @@ def initialize(*)
         req.POST.must_equal "foo" => "bar", "quux" => "bla"
       end
     
    +  it "limit POST body read to bytesize_limit when parsing url-encoded data" do
    +    # Create a mock input that tracks read calls
    +    reads = []
    +    mock_input = Object.new
    +    mock_input.define_singleton_method(:read) do |len=nil|
    +      reads << len
    +      # Return mutable string
    +      "foo=bar".dup
    +    end
    +
    +    request = make_request \
    +      Rack::MockRequest.env_for("/",
    +        'REQUEST_METHOD' => 'POST',
    +        'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    +        'rack.input' => mock_input)
    +
    +    request.POST.must_equal "foo" => "bar"
    +
    +    # Verify read was called with a limit (bytesize_limit + 2), not nil
    +    reads.size.must_equal 1
    +    reads.first.wont_be_nil
    +    reads.first.must_equal(request.send(:query_parser).bytesize_limit + 2)
    +  end
    +
    +  it "handle nil return from rack.input.read when parsing url-encoded data" do
    +    # Simulate an input that returns nil on read
    +    mock_input = Object.new
    +    mock_input.define_singleton_method(:read) do |len=nil|
    +      nil
    +    end
    +
    +    request = make_request \
    +      Rack::MockRequest.env_for("/",
    +        'REQUEST_METHOD' => 'POST',
    +        'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    +        'rack.input' => mock_input)
    +
    +    # Should handle nil gracefully and return empty hash
    +    request.POST.must_equal({})
    +  end
    +
    +  it "truncate POST body at bytesize_limit when parsing url-encoded data" do
    +    # Create input larger than limit
    +    large_body = "a=1&" * 1000000  # Very large body
    +
    +    request = make_request \
    +      Rack::MockRequest.env_for("/",
    +        'REQUEST_METHOD' => 'POST',
    +        'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    +        :input => large_body)
    +
    +    # Should parse only up to the limit without reading entire body into memory
    +    # The actual parsing may fail due to size limit, which is expected
    +    proc { request.POST }.must_raise Rack::QueryParser::QueryLimitError
    +  end
    +
    +  it "clean up Safari's ajax POST body with limited read" do
    +    # Verify Safari null-byte cleanup still works with bounded read
    +    reads = []
    +    mock_input = Object.new
    +    mock_input.define_singleton_method(:read) do |len=nil|
    +      reads << len
    +      # Return mutable string (dup ensures it's not frozen)
    +      "foo=bar\0".dup
    +    end
    +
    +    request = make_request \
    +      Rack::MockRequest.env_for("/",
    +        'REQUEST_METHOD' => 'POST',
    +        'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    +        'rack.input' => mock_input)
    +
    +    request.POST.must_equal "foo" => "bar"
    +
    +    # Verify bounded read was used
    +    reads.first.wont_be_nil
    +  end
    +
       it "return values for the keys in the order given from values_at" do
         req = make_request Rack::MockRequest.env_for("?foo=baz&wun=der&bar=ful")
     
    
e179614c4a65

Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.

https://github.com/rack/rackSamuel WilliamsOct 9, 2025via ghsa
4 files changed · +86 2
  • CHANGELOG.md+1 0 modified
    @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. For info on
     ### Security
     
     - [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
    +- [CVE-2025-61919](https://github.com/advisories/GHSA-6xw4-3v39-52mm) Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.
     
     ## [3.2.2] - 2025-10-07
     
    
  • lib/rack/query_parser.rb+3 1 modified
    @@ -57,6 +57,8 @@ def self.make_default(param_depth_limit, **options)
         PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
         private_constant :PARAMS_LIMIT
     
    +    attr_reader :bytesize_limit
    +
         def initialize(params_class, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
           @params_class = params_class
           @param_depth_limit = param_depth_limit
    @@ -221,7 +223,7 @@ def each_query_pair(qs, separator, unescaper = nil)
           return if !qs || qs.empty?
     
           if qs.bytesize > @bytesize_limit
    -        raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
    +        raise QueryLimitError, "total query size exceeds limit (#{@bytesize_limit})"
           end
     
           pairs = qs.split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP, @params_limit + 1)
    
  • lib/rack/request.rb+4 1 modified
    @@ -513,7 +513,10 @@ def form_pairs
                 if pairs = Rack::Multipart.parse_multipart(env, Rack::Multipart::ParamList)
                   set_header RACK_REQUEST_FORM_PAIRS, pairs
                 else
    -              form_vars = get_header(RACK_INPUT).read
    +              # Add 2 bytes. One to check whether it is over the limit, and a second
    +              # in case the slice! call below removes the last byte
    +              # If read returns nil, use the empty string
    +              form_vars = get_header(RACK_INPUT).read(query_parser.bytesize_limit + 2) || ''
     
                   # Fix for Safari Ajax postings that always append \0
                   # form_vars.sub!(/\0\z/, '') # performance replacement:
    
  • test/spec_request.rb+78 0 modified
    @@ -795,6 +795,84 @@ def initialize(*)
         req.POST.must_equal "foo" => "bar", "quux" => "bla"
       end
     
    +  it "limit POST body read to bytesize_limit when parsing url-encoded data" do
    +    # Create a mock input that tracks read calls
    +    reads = []
    +    mock_input = Object.new
    +    mock_input.define_singleton_method(:read) do |len=nil|
    +      reads << len
    +      # Return mutable string
    +      "foo=bar".dup
    +    end
    +
    +    request = make_request \
    +      Rack::MockRequest.env_for("/",
    +        'REQUEST_METHOD' => 'POST',
    +        'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    +        'rack.input' => mock_input)
    +
    +    request.POST.must_equal "foo" => "bar"
    +
    +    # Verify read was called with a limit (bytesize_limit + 2), not nil
    +    reads.size.must_equal 1
    +    reads.first.wont_be_nil
    +    reads.first.must_equal(request.send(:query_parser).bytesize_limit + 2)
    +  end
    +
    +  it "handle nil return from rack.input.read when parsing url-encoded data" do
    +    # Simulate an input that returns nil on read
    +    mock_input = Object.new
    +    mock_input.define_singleton_method(:read) do |len=nil|
    +      nil
    +    end
    +
    +    request = make_request \
    +      Rack::MockRequest.env_for("/",
    +        'REQUEST_METHOD' => 'POST',
    +        'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    +        'rack.input' => mock_input)
    +
    +    # Should handle nil gracefully and return empty hash
    +    request.POST.must_equal({})
    +  end
    +
    +  it "truncate POST body at bytesize_limit when parsing url-encoded data" do
    +    # Create input larger than limit
    +    large_body = "a=1&" * 1000000  # Very large body
    +
    +    request = make_request \
    +      Rack::MockRequest.env_for("/",
    +        'REQUEST_METHOD' => 'POST',
    +        'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    +        :input => large_body)
    +
    +    # Should parse only up to the limit without reading entire body into memory
    +    # The actual parsing may fail due to size limit, which is expected
    +    proc { request.POST }.must_raise Rack::QueryParser::QueryLimitError
    +  end
    +
    +  it "clean up Safari's ajax POST body with limited read" do
    +    # Verify Safari null-byte cleanup still works with bounded read
    +    reads = []
    +    mock_input = Object.new
    +    mock_input.define_singleton_method(:read) do |len=nil|
    +      reads << len
    +      # Return mutable string (dup ensures it's not frozen)
    +      "foo=bar\0".dup
    +    end
    +
    +    request = make_request \
    +      Rack::MockRequest.env_for("/",
    +        'REQUEST_METHOD' => 'POST',
    +        'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    +        'rack.input' => mock_input)
    +
    +    request.POST.must_equal "foo" => "bar"
    +
    +    # Verify bounded read was used
    +    reads.first.wont_be_nil
    +  end
    +
       it "return form_pairs for url-encoded POST data" do
         req = make_request \
           Rack::MockRequest.env_for("/",
    

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.