rack 2.2.13 → 2.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09a6d038df42d0af44940110fca3a8f9eb37a56a2acea9f1ad02f6fc39c685a9'
4
- data.tar.gz: d4a25103cf82081f357f621ee9a44027a799cda9c566f1ad4ffff3ec9d45a603
3
+ metadata.gz: 9820e6a4dc433010b3485104b374fc20188e1d673474b420b47fdccbf7bf3273
4
+ data.tar.gz: 4f77be4b3508523430c3aad81bfe70d0649941697f9c2dde9aafe7a11deef845
5
5
  SHA512:
6
- metadata.gz: 6324e627506aa9605cab9ad4778303ccb24dffa41d2877e5a9008813556f84cc4660f2638fa00431b36a23cac528c81fa23884d21e907f56370634a67f94070c
7
- data.tar.gz: bc7cabae2f718457165de32fa8905028d0cbb85bcfe150ac2a7871e78ad57b3651d1881aedfc9a366fb9aa11ca009a1344e180ad2c470f1792b4e2befb629034
6
+ metadata.gz: d5774f686721d535e104f96b7e50b1245593ce5e12df78a8ec401ccfb24e3787726b5417c5c82b1f9f0da0b4278a5e6b663b2df176fc0c16926eda3016cde10d
7
+ data.tar.gz: 2f5017277afb7baea14f73d015801a220c416202b6da76fd3c30bddb6bf6bd3abdefa1f0064097d6bcaaa0e60351b701cf4c82f0ff5da0d18e7ddfa0b924863e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  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/).
4
4
 
5
+ ## [2.2.19] - 2025-10-07
6
+
7
+ ### Security
8
+
9
+ - [CVE-2025-61772](https://github.com/advisories/GHSA-wpv5-97wm-hp9c) Multipart parser buffers unbounded per-part headers, enabling DoS (memory exhaustion)
10
+ - [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)
11
+ - [CVE-2025-61770](https://github.com/advisories/GHSA-p543-xpfm-54cp) Unbounded multipart preamble buffering enables DoS (memory exhaustion)
12
+
13
+ ## [2.2.18] - 2025-09-25
14
+
15
+ ### Security
16
+
17
+ - [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.
18
+
19
+ ## [2.2.17] - 2025-06-03
20
+
21
+ - Backport `Rack::MediaType#params` now handles parameters without values. ([#2263](https://github.com/rack/rack/pull/2263), [@AllyMarthaJ](https://github.com/AllyMarthaJ))
22
+
23
+ ## [2.2.16] - 2025-05-22
24
+
25
+ - Fix incorrect backport of optional `CGI::Cookie` support. ([#2335](https://github.com/rack/rack/pull/2335), [@jeremyevans])
26
+
27
+ ## [2.2.15] - 2025-05-18
28
+
29
+ - Optional support for `CGI::Cookie` if not available. ([#2327](https://github.com/rack/rack/pull/2327), [#2333](https://github.com/rack/rack/pull/2333), [@earlopain])
30
+
31
+ ## [2.2.14] - 2025-05-06
32
+
33
+ ### Security
34
+
35
+ - [CVE-2025-32441](https://github.com/rack/rack/security/advisories/GHSA-vpfw-47h7-xj4g) Rack session can be restored after deletion.
36
+ - [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.
37
+
5
38
  ## [2.2.13] - 2025-03-11
6
39
 
7
40
  ### Security
@@ -770,3 +803,7 @@ Items below this line are from the previously maintained HISTORY.md and NEWS.md
770
803
  - Removed Rails adapter, was too alpha.
771
804
 
772
805
  ## [0.1] 2007-03-03
806
+
807
+ [@ioquatix]: https://github.com/ioquatix "Samuel Williams"
808
+ [@jeremyevans]: https://github.com/jeremyevans "Jeremy Evans"
809
+ [@earlopain]: https://github.com/earlopain "Earlopain"
data/README.rdoc CHANGED
@@ -179,6 +179,41 @@ e.g:
179
179
 
180
180
  Rack::Utils.key_space_limit = 128
181
181
 
182
+ === `RACK_QUERY_PARSER_BYTESIZE_LIMIT`
183
+
184
+ This environment variable sets the default for the maximum query string bytesize
185
+ that `Rack::QueryParser` will attempt to parse. Attempts to use a query string
186
+ that exceeds this number of bytes will result in a
187
+ `Rack::QueryParser::QueryLimitError` exception. If this enviroment variable is
188
+ provided, it must be an integer, or `Rack::QueryParser` will raise an exception.
189
+
190
+ The default limit can be overridden on a per-`Rack::QueryParser` basis using
191
+ the `bytesize_limit` keyword argument when creating the `Rack::QueryParser`.
192
+
193
+ === `RACK_QUERY_PARSER_PARAMS_LIMIT`
194
+
195
+ This environment variable sets the default for the maximum number of query
196
+ parameters that `Rack::QueryParser` will attempt to parse. Attempts to use a
197
+ query string with more than this many query parameters will result in a
198
+ `Rack::QueryParser::QueryLimitError` exception. If this enviroment variable is
199
+ provided, it must be an integer, or `Rack::QueryParser` will raise an exception.
200
+
201
+ The default limit can be overridden on a per-`Rack::QueryParser` basis using
202
+ the `params_limit` keyword argument when creating the `Rack::QueryParser`.
203
+
204
+ This is implemented by counting the number of parameter separators in the
205
+ query string, before attempting parsing, so if the same parameter key is
206
+ used multiple times in the query, each counts as a separate parameter for
207
+ this check.
208
+
209
+ === `RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT`
210
+
211
+ This environment variable sets the maximum amount of memory Rack will use
212
+ to buffer multipart parameters when parsing a request body. This considers
213
+ the size of the multipart mime headers and the body part for multipart
214
+ parameters that are buffered in memory and do not use tempfiles. This
215
+ defaults to 16MB if not provided.
216
+
182
217
  === key_space_limit
183
218
 
184
219
  The default number of bytes to allow all parameters keys in a given parameter hash to take up.
@@ -15,8 +15,6 @@ module Rack
15
15
  host = options.delete(:Host) || default_host
16
16
  port = options.delete(:Port) || 8080
17
17
  args = [host, port, app, options]
18
- # Thin versions below 0.8.0 do not support additional options
19
- args.pop if ::Thin::VERSION::MAJOR < 1 && ::Thin::VERSION::MINOR < 8
20
18
  server = ::Thin::Server.new(*args)
21
19
  yield server if block_given?
22
20
  server.start
@@ -27,6 +27,11 @@ module Rack
27
27
  # provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
28
28
  # this method responds with the following Hash:
29
29
  # { 'charset' => 'utf-8' }
30
+ #
31
+ # This will pass back parameters with empty strings in the hash if they
32
+ # lack a value (e.g., "text/plain;charset=" will return { 'charset' => '' },
33
+ # and "text/plain;charset" will return { 'charset' => '' }, similarly to
34
+ # the query params parser (barring the latter case, which returns nil instead)).
30
35
  def params(content_type)
31
36
  return {} if content_type.nil?
32
37
 
@@ -40,9 +45,9 @@ module Rack
40
45
 
41
46
  private
42
47
 
43
- def strip_doublequotes(str)
44
- (str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str
45
- end
48
+ def strip_doublequotes(str)
49
+ (str && str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str || ''
50
+ end
46
51
  end
47
52
  end
48
53
  end
data/lib/rack/mock.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  require 'uri'
4
4
  require 'stringio'
5
5
  require_relative '../rack'
6
- require 'cgi/cookie'
7
6
 
8
7
  module Rack
9
8
  # Rack::MockRequest helps testing your Rack application without
@@ -171,6 +170,36 @@ module Rack
171
170
  # MockRequest.
172
171
 
173
172
  class MockResponse < Rack::Response
173
+ begin
174
+ # Recent versions of the CGI gem may not provide `CGI::Cookie`.
175
+ require 'cgi/cookie'
176
+ Cookie = CGI::Cookie
177
+ rescue LoadError
178
+ class Cookie
179
+ attr_reader :name, :value, :path, :domain, :expires, :secure
180
+
181
+ def initialize(args)
182
+ @name = args["name"]
183
+ @value = args["value"]
184
+ @path = args["path"]
185
+ @domain = args["domain"]
186
+ @expires = args["expires"]
187
+ @secure = args["secure"]
188
+ end
189
+
190
+ def method_missing(method_name, *args, &block)
191
+ @value.send(method_name, *args, &block)
192
+ end
193
+ # :nocov:
194
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
195
+ # :nocov:
196
+
197
+ def respond_to_missing?(method_name, include_all = false)
198
+ @value.respond_to?(method_name, include_all) || super
199
+ end
200
+ end
201
+ end
202
+
174
203
  class << self
175
204
  alias [] new
176
205
  end
@@ -236,7 +265,7 @@ module Rack
236
265
  set_cookie_header.split("\n").each do |cookie|
237
266
  cookie_name, cookie_filling = cookie.split('=', 2)
238
267
  cookie_attributes = identify_cookie_attributes cookie_filling
239
- parsed_cookie = CGI::Cookie.new(
268
+ parsed_cookie = Cookie.new(
240
269
  'name' => cookie_name.strip,
241
270
  'value' => cookie_attributes.fetch('value'),
242
271
  'path' => cookie_attributes.fetch('path', nil),
@@ -253,7 +282,7 @@ module Rack
253
282
  def identify_cookie_attributes(cookie_filling)
254
283
  cookie_bits = cookie_filling.split(';')
255
284
  cookie_attributes = Hash.new
256
- cookie_attributes.store('value', cookie_bits[0].strip)
285
+ cookie_attributes.store('value', Array(cookie_bits[0].strip))
257
286
  cookie_bits.each do |bit|
258
287
  if bit.include? '='
259
288
  cookie_attribute, attribute_value = bit.split('=')
@@ -20,6 +20,27 @@ module Rack
20
20
 
21
21
  BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
22
22
 
23
+ BOUNDARY_START_LIMIT = 16 * 1024
24
+ private_constant :BOUNDARY_START_LIMIT
25
+
26
+ MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024
27
+ private_constant :MIME_HEADER_BYTESIZE_LIMIT
28
+
29
+ env_int = lambda do |key, val|
30
+ if str_val = ENV[key]
31
+ begin
32
+ val = Integer(str_val, 10)
33
+ rescue ArgumentError
34
+ raise ArgumentError, "non-integer value provided for environment variable #{key}"
35
+ end
36
+ end
37
+
38
+ val
39
+ end
40
+
41
+ BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
42
+ private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
43
+
23
44
  class BoundedIO # :nodoc:
24
45
  def initialize(io, content_length)
25
46
  @io = io
@@ -187,6 +208,8 @@ module Rack
187
208
  @end_boundary = @boundary + '--'
188
209
  @state = :FAST_FORWARD
189
210
  @mime_index = 0
211
+ @body_retained = nil
212
+ @retained_size = 0
190
213
  @collector = Collector.new tempfile
191
214
 
192
215
  @sbuf = StringScanner.new("".dup)
@@ -241,7 +264,13 @@ module Rack
241
264
  @state = :MIME_HEAD
242
265
  else
243
266
  raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
244
- :want_read
267
+
268
+ # We raise if we don't find the multipart boundary, to avoid unbounded memory
269
+ # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
270
+ raise EOFError, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
271
+
272
+ # no boundary found, keep reading data
273
+ return :want_read
245
274
  end
246
275
  end
247
276
 
@@ -271,16 +300,30 @@ module Rack
271
300
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
272
301
  end
273
302
 
303
+ # Mime part head data is retained for both TempfilePart and BufferPart
304
+ # for the entireity of the parse, even though it isn't used for BufferPart.
305
+ update_retained_size(head.bytesize)
306
+
307
+ # If a filename is given, a TempfilePart will be used, so the body will
308
+ # not be buffered in memory. However, if a filename is not given, a BufferPart
309
+ # will be used, and the body will be buffered in memory.
310
+ @body_retained = !filename
311
+
274
312
  @collector.on_mime_head @mime_index, head, filename, content_type, name
275
313
  @state = :MIME_BODY
276
314
  else
277
- :want_read
315
+ # We raise if the mime part header is too large, to avoid unbounded memory
316
+ # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
317
+ raise EOFError, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
318
+
319
+ return :want_read
278
320
  end
279
321
  end
280
322
 
281
323
  def handle_mime_body
282
324
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
283
325
  body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
326
+ update_retained_size(body.bytesize) if @body_retained
284
327
  @collector.on_mime_body @mime_index, body
285
328
  @sbuf.pos += body.length + 2 # skip \r\n after the content
286
329
  @state = :CONSUME_TOKEN
@@ -289,7 +332,9 @@ module Rack
289
332
  # Save what we have so far
290
333
  if @rx_max_size < @sbuf.rest_size
291
334
  delta = @sbuf.rest_size - @rx_max_size
292
- @collector.on_mime_body @mime_index, @sbuf.peek(delta)
335
+ body = @sbuf.peek(delta)
336
+ update_retained_size(body.bytesize) if @body_retained
337
+ @collector.on_mime_body @mime_index, body
293
338
  @sbuf.pos += delta
294
339
  @sbuf.string = @sbuf.rest
295
340
  end
@@ -299,6 +344,17 @@ module Rack
299
344
 
300
345
  def full_boundary; @full_boundary; end
301
346
 
347
+ def update_retained_size(size)
348
+ @retained_size += size
349
+ if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
350
+ raise EOFError, "multipart data over retained size limit"
351
+ end
352
+ end
353
+
354
+ # Scan until the we find the start or end of the boundary.
355
+ # If we find it, return the appropriate symbol for the start or
356
+ # end of the boundary. If we don't find the start or end of the
357
+ # boundary, clear the buffer and return nil.
302
358
  def consume_boundary
303
359
  while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
304
360
  case read_buffer.strip
@@ -16,20 +16,47 @@ module Rack
16
16
  # sequence.
17
17
  class InvalidParameterError < ArgumentError; end
18
18
 
19
- # ParamsTooDeepError is the error that is raised when params are recursively
20
- # nested over the specified limit.
21
- class ParamsTooDeepError < RangeError; end
19
+ # QueryLimitError is for errors raised when the query provided exceeds one
20
+ # of the query parser limits.
21
+ class QueryLimitError < RangeError
22
+ end
23
+
24
+ # ParamsTooDeepError is the old name for the error that is raised when params
25
+ # are recursively nested over the specified limit. Make it the same as
26
+ # as QueryLimitError, so that code that rescues ParamsTooDeepError error
27
+ # to handle bad query strings also now handles other limits.
28
+ ParamsTooDeepError = QueryLimitError
22
29
 
23
- def self.make_default(key_space_limit, param_depth_limit)
24
- new Params, key_space_limit, param_depth_limit
30
+ def self.make_default(key_space_limit, param_depth_limit, **options)
31
+ new(Params, key_space_limit, param_depth_limit, **options)
25
32
  end
26
33
 
27
34
  attr_reader :key_space_limit, :param_depth_limit
28
35
 
29
- def initialize(params_class, key_space_limit, param_depth_limit)
36
+ env_int = lambda do |key, val|
37
+ if str_val = ENV[key]
38
+ begin
39
+ val = Integer(str_val, 10)
40
+ rescue ArgumentError
41
+ raise ArgumentError, "non-integer value provided for environment variable #{key}"
42
+ end
43
+ end
44
+
45
+ val
46
+ end
47
+
48
+ BYTESIZE_LIMIT = env_int.call("RACK_QUERY_PARSER_BYTESIZE_LIMIT", 4194304)
49
+ private_constant :BYTESIZE_LIMIT
50
+
51
+ PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
52
+ private_constant :PARAMS_LIMIT
53
+
54
+ def initialize(params_class, key_space_limit, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
30
55
  @params_class = params_class
31
56
  @key_space_limit = key_space_limit
32
57
  @param_depth_limit = param_depth_limit
58
+ @bytesize_limit = bytesize_limit
59
+ @params_limit = params_limit
33
60
  end
34
61
 
35
62
  # Stolen from Mongrel, with some small modifications:
@@ -42,7 +69,7 @@ module Rack
42
69
 
43
70
  params = make_params
44
71
 
45
- (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
72
+ check_query_string(qs, d).split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
46
73
  next if p.empty?
47
74
  k, v = p.split('=', 2).map!(&unescaper)
48
75
 
@@ -69,7 +96,7 @@ module Rack
69
96
  params = make_params
70
97
 
71
98
  unless qs.nil? || qs.empty?
72
- (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
99
+ check_query_string(qs, d).split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
73
100
  k, v = p.split('=', 2).map! { |s| unescape(s) }
74
101
 
75
102
  normalize_params(params, k, v, param_depth_limit)
@@ -155,8 +182,24 @@ module Rack
155
182
  true
156
183
  end
157
184
 
158
- def unescape(s)
159
- Utils.unescape(s)
185
+ def check_query_string(qs, sep)
186
+ if qs
187
+ if qs.bytesize > @bytesize_limit
188
+ raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
189
+ end
190
+
191
+ if (param_count = qs.count(sep.is_a?(String) ? sep : '&;')) >= @params_limit
192
+ raise QueryLimitError, "total number of query parameters (#{param_count+1}) exceeds limit (#{@params_limit})"
193
+ end
194
+
195
+ qs
196
+ else
197
+ ''
198
+ end
199
+ end
200
+
201
+ def unescape(string)
202
+ Utils.unescape(string)
160
203
  end
161
204
 
162
205
  class Params
@@ -55,6 +55,7 @@ module Rack
55
55
 
56
56
  def write_session(req, session_id, new_session, options)
57
57
  with_lock(req) do
58
+ return false unless get_session_with_fallback(session_id)
58
59
  @pool.store session_id.private_id, new_session
59
60
  session_id
60
61
  end
@@ -64,7 +65,11 @@ module Rack
64
65
  with_lock(req) do
65
66
  @pool.delete(session_id.public_id)
66
67
  @pool.delete(session_id.private_id)
67
- generate_sid unless options[:drop]
68
+ unless options[:drop]
69
+ sid = generate_sid
70
+ @pool.store(sid.private_id, {})
71
+ sid
72
+ end
68
73
  end
69
74
  end
70
75
 
data/lib/rack/version.rb CHANGED
@@ -20,7 +20,7 @@ module Rack
20
20
  VERSION.join(".")
21
21
  end
22
22
 
23
- RELEASE = "2.2.13"
23
+ RELEASE = "2.2.19"
24
24
 
25
25
  # Return the Rack release as a dotted string.
26
26
  def self.release
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.13
4
+ version: 2.2.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leah Neukirchen
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: minitest
@@ -76,9 +76,9 @@ executables:
76
76
  - rackup
77
77
  extensions: []
78
78
  extra_rdoc_files:
79
- - README.rdoc
80
79
  - CHANGELOG.md
81
80
  - CONTRIBUTING.md
81
+ - README.rdoc
82
82
  files:
83
83
  - CHANGELOG.md
84
84
  - CONTRIBUTING.md
@@ -182,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
182
182
  - !ruby/object:Gem::Version
183
183
  version: '0'
184
184
  requirements: []
185
- rubygems_version: 3.6.2
185
+ rubygems_version: 3.6.9
186
186
  specification_version: 4
187
187
  summary: A modular Ruby webserver interface.
188
188
  test_files: []