VYPR
High severityNVD Advisory· Published May 7, 2025· Updated May 8, 2025

Unbounded-Parameter DoS in Rack::QueryParser

CVE-2025-46727

Description

Rack is a modular Ruby web server interface. Prior to versions 2.2.14, 3.0.16, and 3.1.14, Rack::QueryParser parses query strings and application/x-www-form-urlencoded bodies into Ruby data structures without imposing any limit on the number of parameters, allowing attackers to send requests with extremely large numbers of parameters. The vulnerability arises because Rack::QueryParser iterates over each &-separated key-value pair and adds it to a Hash without enforcing an upper bound on the total number of parameters. This allows an attacker to send a single request containing hundreds of thousands (or more) of parameters, which consumes excessive memory and CPU during parsing. An attacker can trigger denial of service by sending specifically crafted HTTP requests, which can cause memory exhaustion or pin CPU resources, stalling or crashing the Rack server. This results in full service disruption until the affected worker is restarted. Versions 2.2.14, 3.0.16, and 3.1.14 fix the issue. Some other mitigations are available. One may use middleware to enforce a maximum query string size or parameter count, or employ a reverse proxy (such as Nginx) to limit request sizes and reject oversized query strings or bodies. Limiting request body sizes and query string lengths at the web server or CDN level is an effective mitigation.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
rackRubyGems
< 2.2.142.2.14
rackRubyGems
>= 3.0, < 3.0.163.0.16
rackRubyGems
>= 3.1, < 3.1.143.1.14

Affected products

1

Patches

3
cd6b70a1f2a1

Merge commit from fork

https://github.com/rack/rackJeremy EvansMay 6, 2025via ghsa
5 files changed · +123 10
  • CHANGELOG.md+18 0 modified
    @@ -2,6 +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/).
     
    +## [3.1.14] - 2025-05-06
    +
    +### Security
    +
    +- [CVE-2025-46727](https://github.com/rack/rack/security/advisories/GHSA-gjh7-p2fx-99vx) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion.
    +
     ## [3.1.13] - 2025-04-13
     
     - Ensure `Rack::ETag` correctly updates response body. ([#2324](https://github.com/rack/rack/pull/2324), [@ioquatix])
    @@ -133,6 +139,12 @@ Rack v3.1 is primarily a maintenance release that removes features deprecated in
     
     - In `Rack::Files`, ignore the `Range` header if served file is 0 bytes. ([#2159](https://github.com/rack/rack/pull/2159), [@zarqman])
     
    +## [3.0.16] - 2025-05-06
    +
    +### Security
    +
    +- [CVE-2025-46727](https://github.com/rack/rack/security/advisories/GHSA-gjh7-p2fx-99vx) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion.
    +
     ## [3.0.15] - 2025-04-13
     
     - Ensure `Rack::ETag` correctly updates response body. ([#2324](https://github.com/rack/rack/pull/2324), [@ioquatix])
    @@ -331,6 +343,12 @@ 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.14] - 2025-05-06
    +
    +### Security
    +
    +- [CVE-2025-46727](https://github.com/rack/rack/security/advisories/GHSA-gjh7-p2fx-99vx) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion.
    +
     ## [2.2.13] - 2025-03-11
     
     ### Security
    
  • lib/rack/query_parser.rb+50 8 modified
    @@ -21,21 +21,47 @@ class InvalidParameterError < ArgumentError
           include BadRequest
         end
     
    -    # ParamsTooDeepError is the error that is raised when params are recursively
    -    # nested over the specified limit.
    -    class ParamsTooDeepError < RangeError
    +    # QueryLimitError is for errors raised when the query provided exceeds one
    +    # of the query parser limits.
    +    class QueryLimitError < RangeError
           include BadRequest
         end
     
    -    def self.make_default(param_depth_limit)
    -      new Params, param_depth_limit
    +    # ParamsTooDeepError is the old name for the error that is raised when params
    +    # are recursively nested over the specified limit. Make it the same as
    +    # as QueryLimitError, so that code that rescues ParamsTooDeepError error
    +    # to handle bad query strings also now handles other limits.
    +    ParamsTooDeepError = QueryLimitError
    +
    +    def self.make_default(param_depth_limit, **options)
    +      new(Params, param_depth_limit, **options)
         end
     
         attr_reader :param_depth_limit
     
    -    def initialize(params_class, param_depth_limit)
    +    env_int = lambda do |key, val|
    +      if str_val = ENV[key]
    +        begin
    +          val = Integer(str_val, 10)
    +        rescue ArgumentError
    +          raise ArgumentError, "non-integer value provided for environment variable #{key}"
    +        end
    +      end
    +
    +      val
    +    end
    +
    +    BYTESIZE_LIMIT = env_int.call("RACK_QUERY_PARSER_BYTESIZE_LIMIT", 4194304)
    +    private_constant :BYTESIZE_LIMIT
    +
    +    PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
    +    private_constant :PARAMS_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
    +      @bytesize_limit = bytesize_limit
    +      @params_limit = params_limit
         end
     
         # Stolen from Mongrel, with some small modifications:
    @@ -47,7 +73,7 @@ def parse_query(qs, separator = nil, &unescaper)
     
           params = make_params
     
    -      (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
    +      check_query_string(qs, separator).split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
             next if p.empty?
             k, v = p.split('=', 2).map!(&unescaper)
     
    @@ -74,7 +100,7 @@ def parse_nested_query(qs, separator = nil)
           params = make_params
     
           unless qs.nil? || qs.empty?
    -        (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
    +        check_query_string(qs, separator).split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
               k, v = p.split('=', 2).map! { |s| unescape(s) }
     
               _normalize_params(params, k, v, 0)
    @@ -189,6 +215,22 @@ def params_hash_has_key?(hash, key)
           true
         end
     
    +    def check_query_string(qs, sep)
    +      if qs
    +        if qs.bytesize > @bytesize_limit
    +          raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
    +        end
    +
    +        if (param_count = qs.count(sep.is_a?(String) ? sep : '&')) >= @params_limit
    +          raise QueryLimitError, "total number of query parameters (#{param_count+1}) exceeds limit (#{@params_limit})"
    +        end
    +
    +        qs
    +      else
    +        ''
    +      end
    +    end
    +
         def unescape(string, encoding = Encoding::UTF_8)
           URI.decode_www_form_component(string, encoding)
         end
    
  • README.md+27 0 modified
    @@ -183,6 +183,33 @@ quickly and without doing the same web stuff all over:
     Rack exposes several configuration parameters to control various features of the
     implementation.
     
    +### `RACK_QUERY_PARSER_BYTESIZE_LIMIT`
    +
    +This environment variable sets the default for the maximum query string bytesize
    +that `Rack::QueryParser` will attempt to parse.  Attempts to use a query string
    +that exceeds this number of bytes will result in a
    +`Rack::QueryParser::QueryLimitError` exception. If this enviroment variable is
    +provided, it must be an integer, or `Rack::QueryParser` will raise an exception.
    +
    +The default limit can be overridden on a per-`Rack::QueryParser` basis using
    +the `bytesize_limit` keyword argument when creating the `Rack::QueryParser`.
    +
    +### `RACK_QUERY_PARSER_PARAMS_LIMIT`
    +
    +This environment variable sets the default for the maximum number of query
    +parameters that `Rack::QueryParser` will attempt to parse.  Attempts to use a
    +query string with more than this many query parameters will result in a
    +`Rack::QueryParser::QueryLimitError` exception. If this enviroment variable is
    +provided, it must be an integer, or `Rack::QueryParser` will raise an exception.
    +
    +The default limit can be overridden on a per-`Rack::QueryParser` basis using
    +the `params_limit` keyword argument when creating the `Rack::QueryParser`.
    +
    +This is implemented by counting the number of parameter separators in the
    +query string, before attempting parsing, so if the same parameter key is
    +used multiple times in the query, each counts as a separate parameter for
    +this check.
    +
     ### `param_depth_limit`
     
     ```ruby
    
  • test/spec_query_parser.rb+21 2 modified
    @@ -7,11 +7,30 @@
     end
     
     describe Rack::QueryParser do
    -  query_parser ||= Rack::QueryParser.make_default(8)
    -
       it "can normalize values with missing values" do
    +    query_parser = Rack::QueryParser.make_default(8)
         query_parser.parse_nested_query("a=a").must_equal({"a" => "a"})
         query_parser.parse_nested_query("a=").must_equal({"a" => ""})
         query_parser.parse_nested_query("a").must_equal({"a" => nil})
       end
    +
    +  it "accepts bytesize_limit to specify maximum size of query string to parse" do
    +    query_parser = Rack::QueryParser.make_default(32, bytesize_limit: 3)
    +    query_parser.parse_query("a=a").must_equal({"a" => "a"})
    +    query_parser.parse_nested_query("a=a").must_equal({"a" => "a"})
    +    query_parser.parse_nested_query("a=a", '&').must_equal({"a" => "a"})
    +    proc { query_parser.parse_query("a=aa") }.must_raise Rack::QueryParser::QueryLimitError
    +    proc { query_parser.parse_nested_query("a=aa") }.must_raise Rack::QueryParser::QueryLimitError
    +    proc { query_parser.parse_nested_query("a=aa", '&') }.must_raise Rack::QueryParser::QueryLimitError
    +  end
    +
    +  it "accepts params_limit to specify maximum number of query parameters to parse" do
    +    query_parser = Rack::QueryParser.make_default(32, params_limit: 2)
    +    query_parser.parse_query("a=a&b=b").must_equal({"a" => "a", "b" => "b"})
    +    query_parser.parse_nested_query("a=a&b=b").must_equal({"a" => "a", "b" => "b"})
    +    query_parser.parse_nested_query("a=a&b=b", '&').must_equal({"a" => "a", "b" => "b"})
    +    proc { query_parser.parse_query("a=a&b=b&c=c") }.must_raise Rack::QueryParser::QueryLimitError
    +    proc { query_parser.parse_nested_query("a=a&b=b&c=c", '&') }.must_raise Rack::QueryParser::QueryLimitError
    +    proc { query_parser.parse_query("b[]=a&b[]=b&b[]=c") }.must_raise Rack::QueryParser::QueryLimitError
    +  end
     end
    
  • test/spec_request.rb+7 0 modified
    @@ -1910,6 +1910,9 @@ def params
     
       class NonDelegate < Rack::Request
         def delegate?; false; end
    +    def query_parser
    +      Rack::QueryParser.make_default(Rack::Utils.param_depth_limit, bytesize_limit: 2**30)
    +    end
       end
     
       def make_request(env)
    @@ -1931,6 +1934,10 @@ def initialize(req)
           end
     
           def delegate?; true; end
    +
    +      def query_parser
    +        Rack::QueryParser.make_default(Rack::Utils.param_depth_limit, bytesize_limit: 2**30)
    +      end
         end
     
         def make_request(env)
    
2bb5263b464b

Merge commit from fork

https://github.com/rack/rackJeremy EvansMay 6, 2025via ghsa
5 files changed · +118 10
  • CHANGELOG.md+12 0 modified
    @@ -2,6 +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/).
     
    +## [3.0.16] - 2025-05-06
    +
    +### Security
    +
    +- [CVE-2025-46727](https://github.com/rack/rack/security/advisories/GHSA-gjh7-p2fx-99vx) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion.
    +
     ## [3.0.15] - 2025-04-13
     
     - Ensure `Rack::ETag` correctly updates response body. ([#2324](https://github.com/rack/rack/pull/2324), [@ioquatix])
    @@ -204,6 +210,12 @@ All notable changes to this project will be documented in this file. For info on
     - 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.14] - 2025-05-06
    +
    +### Security
    +
    +- [CVE-2025-46727](https://github.com/rack/rack/security/advisories/GHSA-gjh7-p2fx-99vx) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion.
    +
     ## [2.2.13] - 2025-03-11
     
     ### Security
    
  • lib/rack/query_parser.rb+51 8 modified
    @@ -16,27 +16,54 @@ class ParameterTypeError < TypeError; end
         # sequence.
         class InvalidParameterError < ArgumentError; end
     
    -    # ParamsTooDeepError is the error that is raised when params are recursively
    -    # nested over the specified limit.
    -    class ParamsTooDeepError < RangeError; end
    +    # QueryLimitError is for errors raised when the query provided exceeds one
    +    # of the query parser limits.
    +    class QueryLimitError < RangeError
    +    end
    +
    +    # ParamsTooDeepError is the old name for the error that is raised when params
    +    # are recursively nested over the specified limit. Make it the same as
    +    # as QueryLimitError, so that code that rescues ParamsTooDeepError error
    +    # to handle bad query strings also now handles other limits.
    +    ParamsTooDeepError = QueryLimitError
     
    -    def self.make_default(_key_space_limit=(not_deprecated = true; nil), param_depth_limit)
    +    def self.make_default(_key_space_limit=(not_deprecated = true; nil), param_depth_limit, **options)
           unless not_deprecated
             warn("`first argument `key_space limit` is deprecated and no longer has an effect. Please call with only one argument, which will be required in a future version of Rack", uplevel: 1)
           end
     
    -      new Params, param_depth_limit
    +      new Params, param_depth_limit, **options
         end
     
         attr_reader :param_depth_limit
     
    -    def initialize(params_class, _key_space_limit=(not_deprecated = true; nil), param_depth_limit)
    +    env_int = lambda do |key, val|
    +      if str_val = ENV[key]
    +        begin
    +          val = Integer(str_val, 10)
    +        rescue ArgumentError
    +          raise ArgumentError, "non-integer value provided for environment variable #{key}"
    +        end
    +      end
    +
    +      val
    +    end
    +
    +    BYTESIZE_LIMIT = env_int.call("RACK_QUERY_PARSER_BYTESIZE_LIMIT", 4194304)
    +    private_constant :BYTESIZE_LIMIT
    +
    +    PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
    +    private_constant :PARAMS_LIMIT
    +
    +    def initialize(params_class, _key_space_limit=(not_deprecated = true; nil), param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
           unless not_deprecated
             warn("`second argument `key_space limit` is deprecated and no longer has an effect. Please call with only two arguments, which will be required in a future version of Rack", uplevel: 1)
           end
     
           @params_class = params_class
           @param_depth_limit = param_depth_limit
    +      @bytesize_limit = bytesize_limit
    +      @params_limit = params_limit
         end
     
         # Stolen from Mongrel, with some small modifications:
    @@ -48,7 +75,7 @@ def parse_query(qs, separator = nil, &unescaper)
     
           params = make_params
     
    -      (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
    +      check_query_string(qs, separator).split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
             next if p.empty?
             k, v = p.split('=', 2).map!(&unescaper)
     
    @@ -75,7 +102,7 @@ def parse_nested_query(qs, separator = nil)
           params = make_params
     
           unless qs.nil? || qs.empty?
    -        (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
    +        check_query_string(qs, separator).split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
               k, v = p.split('=', 2).map! { |s| unescape(s) }
     
               _normalize_params(params, k, v, 0)
    @@ -190,6 +217,22 @@ def params_hash_has_key?(hash, key)
           true
         end
     
    +    def check_query_string(qs, sep)
    +      if qs
    +        if qs.bytesize > @bytesize_limit
    +          raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
    +        end
    +
    +        if (param_count = qs.count(sep.is_a?(String) ? sep : '&')) >= @params_limit
    +          raise QueryLimitError, "total number of query parameters (#{param_count+1}) exceeds limit (#{@params_limit})"
    +        end
    +
    +        qs
    +      else
    +        ''
    +      end
    +    end
    +
         def unescape(string, encoding = Encoding::UTF_8)
           URI.decode_www_form_component(string, encoding)
         end
    
  • README.md+27 0 modified
    @@ -165,6 +165,33 @@ quickly and without doing the same web stuff all over:
     Rack exposes several configuration parameters to control various features of the
     implementation.
     
    +### `RACK_QUERY_PARSER_BYTESIZE_LIMIT`
    +
    +This environment variable sets the default for the maximum query string bytesize
    +that `Rack::QueryParser` will attempt to parse.  Attempts to use a query string
    +that exceeds this number of bytes will result in a
    +`Rack::QueryParser::QueryLimitError` exception. If this enviroment variable is
    +provided, it must be an integer, or `Rack::QueryParser` will raise an exception.
    +
    +The default limit can be overridden on a per-`Rack::QueryParser` basis using
    +the `bytesize_limit` keyword argument when creating the `Rack::QueryParser`.
    +
    +### `RACK_QUERY_PARSER_PARAMS_LIMIT`
    +
    +This environment variable sets the default for the maximum number of query
    +parameters that `Rack::QueryParser` will attempt to parse.  Attempts to use a
    +query string with more than this many query parameters will result in a
    +`Rack::QueryParser::QueryLimitError` exception. If this enviroment variable is
    +provided, it must be an integer, or `Rack::QueryParser` will raise an exception.
    +
    +The default limit can be overridden on a per-`Rack::QueryParser` basis using
    +the `params_limit` keyword argument when creating the `Rack::QueryParser`.
    +
    +This is implemented by counting the number of parameter separators in the
    +query string, before attempting parsing, so if the same parameter key is
    +used multiple times in the query, each counts as a separate parameter for
    +this check.
    +
     ### `param_depth_limit`
     
     ```ruby
    
  • test/spec_query_parser.rb+21 2 modified
    @@ -7,11 +7,30 @@
     end
     
     describe Rack::QueryParser do
    -  query_parser ||= Rack::QueryParser.make_default(8)
    -
       it "can normalize values with missing values" do
    +    query_parser = Rack::QueryParser.make_default(8)
         query_parser.parse_nested_query("a=a").must_equal({"a" => "a"})
         query_parser.parse_nested_query("a=").must_equal({"a" => ""})
         query_parser.parse_nested_query("a").must_equal({"a" => nil})
       end
    +
    +  it "accepts bytesize_limit to specify maximum size of query string to parse" do
    +    query_parser = Rack::QueryParser.make_default(32, bytesize_limit: 3)
    +    query_parser.parse_query("a=a").must_equal({"a" => "a"})
    +    query_parser.parse_nested_query("a=a").must_equal({"a" => "a"})
    +    query_parser.parse_nested_query("a=a", '&').must_equal({"a" => "a"})
    +    proc { query_parser.parse_query("a=aa") }.must_raise Rack::QueryParser::QueryLimitError
    +    proc { query_parser.parse_nested_query("a=aa") }.must_raise Rack::QueryParser::QueryLimitError
    +    proc { query_parser.parse_nested_query("a=aa", '&') }.must_raise Rack::QueryParser::QueryLimitError
    +  end
    +
    +  it "accepts params_limit to specify maximum number of query parameters to parse" do
    +    query_parser = Rack::QueryParser.make_default(32, params_limit: 2)
    +    query_parser.parse_query("a=a&b=b").must_equal({"a" => "a", "b" => "b"})
    +    query_parser.parse_nested_query("a=a&b=b").must_equal({"a" => "a", "b" => "b"})
    +    query_parser.parse_nested_query("a=a&b=b", '&').must_equal({"a" => "a", "b" => "b"})
    +    proc { query_parser.parse_query("a=a&b=b&c=c") }.must_raise Rack::QueryParser::QueryLimitError
    +    proc { query_parser.parse_nested_query("a=a&b=b&c=c", '&') }.must_raise Rack::QueryParser::QueryLimitError
    +    proc { query_parser.parse_query("b[]=a&b[]=b&b[]=c") }.must_raise Rack::QueryParser::QueryLimitError
    +  end
     end
    
  • test/spec_request.rb+7 0 modified
    @@ -1932,6 +1932,9 @@ def params
     
       class NonDelegate < Rack::Request
         def delegate?; false; end
    +    def query_parser
    +      Rack::QueryParser.make_default(Rack::Utils.param_depth_limit, bytesize_limit: 2**30)
    +    end
       end
     
       def make_request(env)
    @@ -1953,6 +1956,10 @@ def initialize(req)
           end
     
           def delegate?; true; end
    +
    +      def query_parser
    +        Rack::QueryParser.make_default(Rack::Utils.param_depth_limit, bytesize_limit: 2**30)
    +      end
         end
     
         def make_request(env)
    
3f5a4249118d

Merge commit from fork

https://github.com/rack/rackJeremy EvansMay 6, 2025via ghsa
5 files changed · +122 10
  • 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-32441](https://github.com/rack/rack/security/advisories/GHSA-vpfw-47h7-xj4g) Rack session can be restored after deletion.
    +- [CVE-2025-46727](https://github.com/rack/rack/security/advisories/GHSA-gjh7-p2fx-99vx) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion.
     
     ## [2.2.13] - 2025-03-11
     
    
  • lib/rack/query_parser.rb+53 10 modified
    @@ -16,20 +16,47 @@ class ParameterTypeError < TypeError; end
         # sequence.
         class InvalidParameterError < ArgumentError; end
     
    -    # ParamsTooDeepError is the error that is raised when params are recursively
    -    # nested over the specified limit.
    -    class ParamsTooDeepError < RangeError; end
    +    # QueryLimitError is for errors raised when the query provided exceeds one
    +    # of the query parser limits.
    +    class QueryLimitError < RangeError
    +    end
    +
    +    # ParamsTooDeepError is the old name for the error that is raised when params
    +    # are recursively nested over the specified limit. Make it the same as
    +    # as QueryLimitError, so that code that rescues ParamsTooDeepError error
    +    # to handle bad query strings also now handles other limits.
    +    ParamsTooDeepError = QueryLimitError
     
    -    def self.make_default(key_space_limit, param_depth_limit)
    -      new Params, key_space_limit, param_depth_limit
    +    def self.make_default(key_space_limit, param_depth_limit, **options)
    +      new(Params, key_space_limit, param_depth_limit, **options)
         end
     
         attr_reader :key_space_limit, :param_depth_limit
     
    -    def initialize(params_class, key_space_limit, param_depth_limit)
    +    env_int = lambda do |key, val|
    +      if str_val = ENV[key]
    +        begin
    +          val = Integer(str_val, 10)
    +        rescue ArgumentError
    +          raise ArgumentError, "non-integer value provided for environment variable #{key}"
    +        end
    +      end
    +
    +      val
    +    end
    +
    +    BYTESIZE_LIMIT = env_int.call("RACK_QUERY_PARSER_BYTESIZE_LIMIT", 4194304)
    +    private_constant :BYTESIZE_LIMIT
    +
    +    PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
    +    private_constant :PARAMS_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
           @param_depth_limit = param_depth_limit
    +      @bytesize_limit = bytesize_limit
    +      @params_limit = params_limit
         end
     
         # Stolen from Mongrel, with some small modifications:
    @@ -42,7 +69,7 @@ def parse_query(qs, d = nil, &unescaper)
     
           params = make_params
     
    -      (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
    +      check_query_string(qs, d).split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
             next if p.empty?
             k, v = p.split('=', 2).map!(&unescaper)
     
    @@ -69,7 +96,7 @@ def parse_nested_query(qs, d = nil)
           params = make_params
     
           unless qs.nil? || qs.empty?
    -        (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
    +        check_query_string(qs, d).split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
               k, v = p.split('=', 2).map! { |s| unescape(s) }
     
               normalize_params(params, k, v, param_depth_limit)
    @@ -155,8 +182,24 @@ def params_hash_has_key?(hash, key)
           true
         end
     
    -    def unescape(s)
    -      Utils.unescape(s)
    +    def check_query_string(qs, sep)
    +      if qs
    +        if qs.bytesize > @bytesize_limit
    +          raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
    +        end
    +
    +        if (param_count = qs.count(sep.is_a?(String) ? sep : '&')) >= @params_limit
    +          raise QueryLimitError, "total number of query parameters (#{param_count+1}) exceeds limit (#{@params_limit})"
    +        end
    +
    +        qs
    +      else
    +        ''
    +      end
    +    end
    +
    +    def unescape(string, encoding = Encoding::UTF_8)
    +      Utils.unescape(string, encoding)
         end
     
         class Params
    
  • README.rdoc+27 0 modified
    @@ -179,6 +179,33 @@ e.g:
     
         Rack::Utils.key_space_limit = 128
     
    +=== `RACK_QUERY_PARSER_BYTESIZE_LIMIT`
    +
    +This environment variable sets the default for the maximum query string bytesize
    +that `Rack::QueryParser` will attempt to parse.  Attempts to use a query string
    +that exceeds this number of bytes will result in a
    +`Rack::QueryParser::QueryLimitError` exception. If this enviroment variable is
    +provided, it must be an integer, or `Rack::QueryParser` will raise an exception.
    +
    +The default limit can be overridden on a per-`Rack::QueryParser` basis using
    +the `bytesize_limit` keyword argument when creating the `Rack::QueryParser`.
    +
    +=== `RACK_QUERY_PARSER_PARAMS_LIMIT`
    +
    +This environment variable sets the default for the maximum number of query
    +parameters that `Rack::QueryParser` will attempt to parse.  Attempts to use a
    +query string with more than this many query parameters will result in a
    +`Rack::QueryParser::QueryLimitError` exception. If this enviroment variable is
    +provided, it must be an integer, or `Rack::QueryParser` will raise an exception.
    +
    +The default limit can be overridden on a per-`Rack::QueryParser` basis using
    +the `params_limit` keyword argument when creating the `Rack::QueryParser`.
    +
    +This is implemented by counting the number of parameter separators in the
    +query string, before attempting parsing, so if the same parameter key is
    +used multiple times in the query, each counts as a separate parameter for
    +this check.
    +
     === key_space_limit
     
     The default number of bytes to allow all parameters keys in a given parameter hash to take up.
    
  • test/spec_query_parser.rb+33 0 added
    @@ -0,0 +1,33 @@
    +# frozen_string_literal: true
    +
    +require_relative 'helper'
    +require_relative '../lib/rack/query_parser'
    +
    +describe Rack::QueryParser do
    +  it "can normalize values with missing values" do
    +    query_parser = Rack::QueryParser.make_default(Rack::Utils.key_space_limit, 8)
    +    query_parser.parse_nested_query("a=a").must_equal({"a" => "a"})
    +    query_parser.parse_nested_query("a=").must_equal({"a" => ""})
    +    query_parser.parse_nested_query("a").must_equal({"a" => nil})
    +  end
    +
    +  it "accepts bytesize_limit to specify maximum size of query string to parse" do
    +    query_parser = Rack::QueryParser.make_default(Rack::Utils.key_space_limit, 32, bytesize_limit: 3)
    +    query_parser.parse_query("a=a").must_equal({"a" => "a"})
    +    query_parser.parse_nested_query("a=a").must_equal({"a" => "a"})
    +    query_parser.parse_nested_query("a=a", '&').must_equal({"a" => "a"})
    +    proc { query_parser.parse_query("a=aa") }.must_raise Rack::QueryParser::QueryLimitError
    +    proc { query_parser.parse_nested_query("a=aa") }.must_raise Rack::QueryParser::QueryLimitError
    +    proc { query_parser.parse_nested_query("a=aa", '&') }.must_raise Rack::QueryParser::QueryLimitError
    +  end
    +
    +  it "accepts params_limit to specify maximum number of query parameters to parse" do
    +    query_parser = Rack::QueryParser.make_default(Rack::Utils.key_space_limit, 32, params_limit: 2)
    +    query_parser.parse_query("a=a&b=b").must_equal({"a" => "a", "b" => "b"})
    +    query_parser.parse_nested_query("a=a&b=b").must_equal({"a" => "a", "b" => "b"})
    +    query_parser.parse_nested_query("a=a&b=b", '&').must_equal({"a" => "a", "b" => "b"})
    +    proc { query_parser.parse_query("a=a&b=b&c=c") }.must_raise Rack::QueryParser::QueryLimitError
    +    proc { query_parser.parse_nested_query("a=a&b=b&c=c", '&') }.must_raise Rack::QueryParser::QueryLimitError
    +    proc { query_parser.parse_query("b[]=a&b[]=b&b[]=c") }.must_raise Rack::QueryParser::QueryLimitError
    +  end
    +end
    
  • test/spec_request.rb+8 0 modified
    @@ -1535,6 +1535,10 @@ def params
     
       class NonDelegate < Rack::Request
         def delegate?; false; end
    +
    +    def query_parser
    +      Rack::QueryParser.make_default(Rack::Utils.key_space_limit, Rack::Utils.param_depth_limit, bytesize_limit: 2**30)
    +    end
       end
     
       def make_request(env)
    @@ -1558,6 +1562,10 @@ def initialize(req)
           def delegate?; true; end
     
           def env; @req.env.dup; end
    +
    +      def query_parser
    +        Rack::QueryParser.make_default(Rack::Utils.key_space_limit, Rack::Utils.param_depth_limit, bytesize_limit: 2**30)
    +      end
         end
     
         def make_request(env)
    

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.