Unbounded-Parameter DoS in Rack::QueryParser
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.
| Package | Affected versions | Patched versions |
|---|---|---|
rackRubyGems | < 2.2.14 | 2.2.14 |
rackRubyGems | >= 3.0, < 3.0.16 | 3.0.16 |
rackRubyGems | >= 3.1, < 3.1.14 | 3.1.14 |
Affected products
1Patches
35 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)
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)
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- github.com/advisories/GHSA-gjh7-p2fx-99vxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-46727ghsaADVISORY
- github.com/rack/rack/commit/2bb5263b464b65ba4b648996a579dbd180d2b712ghsax_refsource_MISCWEB
- github.com/rack/rack/commit/3f5a4249118d09d199fe480466c8c6717e43b6e3ghsax_refsource_MISCWEB
- github.com/rack/rack/commit/cd6b70a1f2a1016b73dc906f924869f4902c2d74ghsax_refsource_MISCWEB
- github.com/rack/rack/security/advisories/GHSA-gjh7-p2fx-99vxghsax_refsource_CONFIRMWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/rack/CVE-2025-46727.ymlghsaWEB
News mentions
0No linked articles in our index yet.