Skip to content

Commit 959139e

Browse files
NiklasHaekratob
authored andcommitted
Backport CVE-2025-46727
1 parent 183d830 commit 959139e

File tree

2 files changed

+58
-2
lines changed

2 files changed

+58
-2
lines changed

rack/lib/rack/utils.rb

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ module Utils
3030
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
3131
].freeze
3232

33+
# QueryLimitError is for errors raised when the query provided exceeds one
34+
# of the query parser limits.
35+
class QueryLimitError < RangeError; end
36+
3337
# URI escapes. (CGI style space to +)
3438
def escape(s)
3539
URI.encode_www_form_component(s)
@@ -63,6 +67,8 @@ class << self
6367
attr_accessor :param_depth_limit
6468
attr_accessor :multipart_total_part_limit
6569
attr_accessor :multipart_file_limit
70+
attr_accessor :bytesize_limit # CVE-2025-46727
71+
attr_accessor :params_limit # CVE-2025-46727
6672

6773
# multipart_part_limit is the original name of multipart_file_limit, but
6874
# the limit only counts parts with filenames.
@@ -87,6 +93,34 @@ class << self
8793
# many can lead to excessive memory use and parsing time.
8894
self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
8995

96+
# This sets the default for the maximum query string bytesize that we will attempt to parse.
97+
# Attempts to use a query string that exceeds this number of bytes will result in a
98+
# `Rack::Utils::QueryLimitError` exception.
99+
self.bytesize_limit = (ENV['RACK_QUERY_PARSER_BYTESIZE_LIMIT'] || 4194304).to_i
100+
101+
# This variable sets the default for the maximum number of query
102+
# parameters that we will attempt to parse. Attempts to use a
103+
# query string with more than this many query parameters will result in a
104+
# `Rack::Utils::QueryLimitError` exception.
105+
self.params_limit = (ENV['RACK_QUERY_PARSER_PARAMS_LIMIT'] || 4096).to_i
106+
107+
def check_query_string(qs, sep)
108+
if qs
109+
if qs.bytesize > Rack::Utils.bytesize_limit
110+
raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{Rack::Utils.bytesize_limit})"
111+
end
112+
113+
if (param_count = qs.count(sep.is_a?(String) ? sep : '&')) >= Rack::Utils.params_limit
114+
raise QueryLimitError, "total number of query parameters (#{param_count+1}) exceeds limit (#{Rack::Utils.params_limit})"
115+
end
116+
117+
qs
118+
else
119+
''
120+
end
121+
end
122+
module_function :check_query_string
123+
90124
# Stolen from Mongrel, with some small modifications:
91125
# Parses a query string by breaking it up at the '&'
92126
# and ';' characters. You can also use this to parse
@@ -97,7 +131,7 @@ def parse_query(qs, d = nil, &unescaper)
97131

98132
params = KeySpaceConstrainedParams.new
99133

100-
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
134+
check_query_string(qs, d).split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
101135
next if p.empty?
102136
k, v = p.split('=', 2).map(&unescaper)
103137
next unless k || v
@@ -120,7 +154,7 @@ def parse_query(qs, d = nil, &unescaper)
120154
def parse_nested_query(qs, d = nil)
121155
params = KeySpaceConstrainedParams.new
122156

123-
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
157+
check_query_string(qs, d).split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
124158
k, v = p.split('=', 2).map { |s| unescape(s) }
125159

126160
normalize_params(params, k, v)

rack/test/spec_utils.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,28 @@ def kcodeu
383383
should "clean slash only paths" do
384384
Rack::Utils.clean_path_info("/").should.equal "/"
385385
end
386+
387+
should "respect bytesize_limit to specify maximum size of query string to parse" do
388+
Rack::Utils.bytesize_limit = 3
389+
Rack::Utils.parse_query("a=a").should.equal({"a" => "a"})
390+
Rack::Utils.parse_nested_query("a=a").should.equal({"a" => "a"})
391+
Rack::Utils.parse_nested_query("a=a", '&').should.equal({"a" => "a"})
392+
proc { Rack::Utils.parse_query("a=aa") }.should.raise(Rack::Utils::QueryLimitError)
393+
proc { Rack::Utils.parse_nested_query("a=aa") }.should.raise(Rack::Utils::QueryLimitError)
394+
proc { Rack::Utils.parse_nested_query("a=aa", '&') }.should.raise(Rack::Utils::QueryLimitError)
395+
Rack::Utils.bytesize_limit = 4194304
396+
end
397+
398+
it "accepts params_limit to specify maximum number of query parameters to parse" do
399+
Rack::Utils.params_limit = 2
400+
Rack::Utils.parse_query("a=a&b=b").should.equal({"a" => "a", "b" => "b"})
401+
Rack::Utils.parse_nested_query("a=a&b=b").should.equal({"a" => "a", "b" => "b"})
402+
Rack::Utils.parse_nested_query("a=a&b=b", '&').should.equal({"a" => "a", "b" => "b"})
403+
proc { Rack::Utils.parse_query("a=a&b=b&c=c") }.should.raise(Rack::Utils::QueryLimitError)
404+
proc { Rack::Utils.parse_nested_query("a=a&b=b&c=c", '&') }.should.raise(Rack::Utils::QueryLimitError)
405+
proc { Rack::Utils.parse_query("b[]=a&b[]=b&b[]=c") }.should.raise(Rack::Utils::QueryLimitError)
406+
Rack::Utils.params_limit = 4096
407+
end
386408
end
387409

388410
describe Rack::Utils, "byte_range" do

0 commit comments

Comments
 (0)