rack 3.2.5 → 3.2.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dcab43418ebec9227dad4a8e8fbfeb403097a3c34bcebc63a230350984c68ab9
4
- data.tar.gz: 6b82c8e25fa9bc835cfe24a05da9731bd9a0654b40a56a01c6f77ac7fc7029f4
3
+ metadata.gz: 37024c5110b365f1dd0b5627dd903edf42344fa9ad98a99af672e220d2370288
4
+ data.tar.gz: d0bd60323a75ff2828b33713963dcb5769898abc82a7ab3c2b9ac4ad36550b88
5
5
  SHA512:
6
- metadata.gz: a727df6903901d26a8d4e031302cb0c5b45134b9e31b2443b6e4d4a595233f4d18d58ff265cafbd9e344e4b7958fd3d27b6e46d57c6f37c9744617da8eba6db4
7
- data.tar.gz: 8c914906cc9c4d0610ab8dc0de815cb714311261dc50ef6d1a6c1b5ecacd9d3f58e477014176a0044dd1f1dd24ecc55554eb6eebe78995205bd6c47bee83d9eb
6
+ metadata.gz: 825e73fe75136333217b5f395f25245fe295fa4749a8068711011ba5c47350043941e4a4319837015e211f6cbd29952970fdc79f5deb0364c76d995c8d9de457
7
+ data.tar.gz: 57b8333f46704c6aead2d24ab214ab99ddb8296f93cc62298385d20bf4bffad829fae3b5e3a658e399d1628ab355a9abd69e4e1f2d08971740547b6ff1425f5b
data/CHANGELOG.md CHANGED
@@ -2,7 +2,25 @@
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
- ## Unreleased
5
+ ## [3.2.6] - 2026-04-01
6
+
7
+ ### Security
8
+
9
+ - [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) Root directory disclosure via unescaped regex interpolation in `Rack::Directory`.
10
+ - [CVE-2026-34230](https://github.com/advisories/GHSA-v569-hp3g-36wr) Avoid O(n^2) algorithm in `Rack::Utils.select_best_encoding` which could lead to denial of service.
11
+ - [CVE-2026-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) Forwarded header semicolon injection enables Host and Scheme spoofing.
12
+ - [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
13
+ - [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
14
+ - [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
15
+ - [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
16
+ - [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
17
+ - [CVE-2026-34830](https://github.com/advisories/GHSA-qv7j-4883-hwh7) `Rack::Sendfile` header-based `X-Accel-Mapping` regex injection enables unauthorized `X-Accel-Redirect`.
18
+ - [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
19
+ - [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
20
+ - [CVE-2026-34827](https://github.com/advisories/GHSA-v6x5-cg8r-vv6x) Quadratic-time multipart header parsing allows denial of service via escape-heavy quoted parameters.
21
+ - [CVE-2026-26962](https://github.com/advisories/GHSA-rx22-g9mx-qrhv) Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
22
+
23
+ ## [3.2.5] - 2026-02-16
6
24
 
7
25
  ### Security
8
26
 
@@ -91,6 +109,13 @@ This release continues Rack's evolution toward a cleaner, more efficient foundat
91
109
  - `SERVER_NAME` and `HTTP_HOST` are now more strictly validated according to the relevant specifications. ([#2298](https://github.com/rack/rack/pull/2298), [@ioquatix])
92
110
  - `Rack::Lint` now disallows `PATH_INFO="" SCRIPT_NAME=""`. ([#2298](https://github.com/rack/rack/issues/2307), [@jeremyevans])
93
111
 
112
+ ## [3.1.20] - 2026-02-16
113
+
114
+ ### Security
115
+
116
+ - [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
117
+ - [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
118
+
94
119
  ## [3.1.19] - 2025-11-03
95
120
 
96
121
  ### Fixed
@@ -481,6 +506,13 @@ This release introduces major improvements to Rack, including enhanced support f
481
506
  - 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))
482
507
  - `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))
483
508
 
509
+ ## [2.2.22] - 2026-02-16
510
+
511
+ ### Security
512
+
513
+ - [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
514
+ - [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
515
+
484
516
  ## [2.2.21] - 2025-11-03
485
517
 
486
518
  ### Fixed
@@ -51,7 +51,7 @@ table { width:100%%; }
51
51
  class DirectoryBody < Struct.new(:root, :path, :files)
52
52
  # Yield strings for each part of the directory entry
53
53
  def each
54
- show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
54
+ show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
55
55
  yield(DIR_PAGE_HEADER % [ show_path, show_path ])
56
56
 
57
57
  unless path.chomp('/') == root
data/lib/rack/files.rb CHANGED
@@ -194,7 +194,7 @@ EOF
194
194
  status,
195
195
  {
196
196
  CONTENT_TYPE => "text/plain",
197
- CONTENT_LENGTH => body.size.to_s,
197
+ CONTENT_LENGTH => body.bytesize.to_s,
198
198
  "x-cascade" => "pass"
199
199
  }.merge!(headers),
200
200
  [body]
@@ -33,7 +33,7 @@ module Rack
33
33
  EOL = "\r\n"
34
34
  FWS = /[ \t]+(?:\r\n[ \t]+)?/ # whitespace with optional folding
35
35
  HEADER_VALUE = "(?:[^\r\n]|\r\n[ \t])*" # anything but a non-folding CRLF
36
- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
36
+ MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
37
37
  MULTIPART_CONTENT_TYPE = /^Content-Type:#{FWS}?(#{HEADER_VALUE})/ni
38
38
  MULTIPART_CONTENT_DISPOSITION = /^Content-Disposition:#{FWS}?(#{HEADER_VALUE})/ni
39
39
  MULTIPART_CONTENT_ID = /^Content-ID:#{FWS}?(#{HEADER_VALUE})/ni
@@ -80,6 +80,13 @@ module Rack
80
80
  BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
81
81
  private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
82
82
 
83
+ bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
84
+ PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
85
+ private_constant :PARSER_BYTESIZE_LIMIT
86
+
87
+ CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
88
+ private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
89
+
83
90
  class BoundedIO # :nodoc:
84
91
  def initialize(io, content_length)
85
92
  @io = io
@@ -116,7 +123,15 @@ module Rack
116
123
  return unless content_type
117
124
  data = content_type.match(MULTIPART)
118
125
  return unless data
119
- data[1]
126
+
127
+ unless data[1].empty?
128
+ raise Error, "whitespace between boundary parameter name and equal sign"
129
+ end
130
+ if data.post_match.match?(/boundary\s*=/i)
131
+ raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
132
+ end
133
+
134
+ data[2]
120
135
  end
121
136
 
122
137
  def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
@@ -125,6 +140,10 @@ module Rack
125
140
  boundary = parse_boundary content_type
126
141
  return EMPTY unless boundary
127
142
 
143
+ if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
144
+ raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
145
+ end
146
+
128
147
  if boundary.length > 70
129
148
  # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
130
149
  # Most clients use no more than 55 characters.
@@ -241,6 +260,8 @@ module Rack
241
260
  @mime_index = 0
242
261
  @body_retained = nil
243
262
  @retained_size = 0
263
+ @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
264
+ @content_disposition_quoted_escapes = 0
244
265
  @collector = Collector.new tempfile
245
266
 
246
267
  @sbuf = StringScanner.new("".dup)
@@ -252,6 +273,7 @@ module Rack
252
273
  end
253
274
 
254
275
  def parse(io)
276
+ @total_bytes_read &&= nil if io.is_a?(BoundedIO)
255
277
  outbuf = String.new
256
278
  read_data(io, outbuf)
257
279
 
@@ -290,6 +312,12 @@ module Rack
290
312
  def read_data(io, outbuf)
291
313
  content = io.read(@bufsize, outbuf)
292
314
  handle_empty_content!(content)
315
+ if @total_bytes_read
316
+ @total_bytes_read += content.bytesize
317
+ if @total_bytes_read > PARSER_BYTESIZE_LIMIT
318
+ raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
319
+ end
320
+ end
293
321
  @sbuf.concat(content)
294
322
  end
295
323
 
@@ -339,13 +367,21 @@ module Rack
339
367
 
340
368
  CONTENT_DISPOSITION_MAX_PARAMS = 16
341
369
  CONTENT_DISPOSITION_MAX_BYTES = 1536
370
+ OBS_UNFOLD = /\r\n([ \t])/
371
+ private_constant :OBS_UNFOLD
372
+
342
373
  def handle_mime_head
343
374
  if @sbuf.scan_until(@head_regex)
344
375
  head = @sbuf[1]
345
376
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
377
+ content_type.gsub!(OBS_UNFOLD, '\1') if content_type
378
+
346
379
  if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
347
380
  disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
348
381
 
382
+ # Implement OBS unfolding (RFC 5322 Section 2.2.3)
383
+ disposition.gsub!(OBS_UNFOLD, '\1')
384
+
349
385
  # ignore actual content-disposition value (should always be form-data)
350
386
  i = disposition.index(';')
351
387
  disposition.slice!(0, i+1)
@@ -383,6 +419,11 @@ module Rack
383
419
  # stop parsing parameter value if found ending quote
384
420
  break if c == '"'
385
421
 
422
+ @content_disposition_quoted_escapes += 1
423
+ if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
424
+ raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
425
+ end
426
+
386
427
  escaped_char = disposition.slice!(0, 1)
387
428
  if param == 'filename' && escaped_char != '"'
388
429
  # Possible IE uploaded filename, append both escape backslash and value
data/lib/rack/request.rb CHANGED
@@ -723,8 +723,8 @@ module Rack
723
723
  # Match IPv6 as a string of hex digits and colons in square brackets
724
724
  \[(?<address>#{ipv6})\]
725
725
  |
726
- # Match any other printable string (except square brackets) as a hostname
727
- (?<address>[[[:graph:]&&[^\[\]]]]*?)
726
+ # Match characters allowed by RFC 3986 Section 3.2.2
727
+ (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
728
728
  )
729
729
  (:(?<port>\d+))?
730
730
  \z
data/lib/rack/sendfile.rb CHANGED
@@ -51,7 +51,7 @@ module Rack
51
51
  #
52
52
  # The `x-accel-mapping` header should specify the location on the file system,
53
53
  # followed by an equals sign (=), followed name of the private URL pattern
54
- # that it maps to. The middleware performs a simple substitution on the
54
+ # that it maps to. The middleware performs a case-insensitive substitution on the
55
55
  # resulting path.
56
56
  #
57
57
  # To enable `x-accel-redirect`, you must configure the middleware explicitly:
@@ -186,7 +186,7 @@ module Rack
186
186
  # Safe to use header: explicit config + no app mappings:
187
187
  mapping.split(',').map(&:strip).each do |m|
188
188
  internal, external = m.split('=', 2).map(&:strip)
189
- new_path = path.sub(/\A#{internal}/i, external)
189
+ new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
190
190
  return new_path unless path == new_path
191
191
  end
192
192
 
data/lib/rack/static.rb CHANGED
@@ -93,6 +93,9 @@ module Rack
93
93
  def initialize(app, options = {})
94
94
  @app = app
95
95
  @urls = options[:urls] || ["/favicon.ico"]
96
+ if @urls.kind_of?(Array)
97
+ @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
98
+ end
96
99
  @index = options[:index]
97
100
  @gzip = options[:gzip]
98
101
  @cascade = options[:cascade]
@@ -115,7 +118,7 @@ module Rack
115
118
  end
116
119
 
117
120
  def route_file(path)
118
- @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
121
+ @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
119
122
  end
120
123
 
121
124
  def can_serve(path)
@@ -165,6 +168,8 @@ module Rack
165
168
 
166
169
  # Convert HTTP header rules to HTTP headers
167
170
  def applicable_rules(path)
171
+ path = ::Rack::Utils.unescape_path(path)
172
+
168
173
  @header_rules.find_all do |rule, new_headers|
169
174
  case rule
170
175
  when :all
@@ -172,10 +177,9 @@ module Rack
172
177
  when :fonts
173
178
  /\.(?:ttf|otf|eot|woff2|woff|svg)\z/.match?(path)
174
179
  when String
175
- path = ::Rack::Utils.unescape(path)
176
180
  path.start_with?(rule) || path.start_with?('/' + rule)
177
181
  when Array
178
- /\.(#{rule.join('|')})\z/.match?(path)
182
+ /\.#{Regexp.union(rule)}\z/.match?(path)
179
183
  when Regexp
180
184
  rule.match?(path)
181
185
  else
data/lib/rack/utils.rb CHANGED
@@ -146,17 +146,77 @@ module Rack
146
146
  end
147
147
  end
148
148
 
149
+ ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze
150
+ private_constant :ALLOWED_FORWARED_PARAMS
151
+
149
152
  def forwarded_values(forwarded_header)
150
- return nil unless forwarded_header
151
- forwarded_header = forwarded_header.to_s.gsub("\n", ";")
152
-
153
- forwarded_header.split(';').each_with_object({}) do |field, values|
154
- field.split(',').each do |pair|
155
- pair = pair.split('=').map(&:strip).join('=')
156
- return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
157
- (values[$1.downcase.to_sym] ||= []) << $2
153
+ return unless forwarded_header
154
+ header = forwarded_header.to_s.tr("\n", ";")
155
+ header.sub!(/\A[\s;,]+/, '')
156
+ num_params = num_escapes = 0
157
+ max_params = max_escapes = 1024
158
+ params = {}
159
+
160
+ # Parse parameter list
161
+ while i = header.index('=')
162
+ # Only parse up to max parameters, to avoid potential denial of service
163
+ num_params += 1
164
+ return if num_params > max_params
165
+
166
+ # Found end of parameter name, ensure forward progress in loop
167
+ param = header.slice!(0, i+1)
168
+
169
+ # Remove ending equals and preceding whitespace from parameter name
170
+ param.chomp!('=')
171
+ param.strip!
172
+ param.downcase!
173
+ return unless param = ALLOWED_FORWARED_PARAMS[param]
174
+
175
+ if header[0] == '"'
176
+ # Parameter value is quoted, parse it, handling backslash escapes
177
+ header.slice!(0, 1)
178
+ value = String.new
179
+
180
+ while i = header.index(/(["\\])/)
181
+ c = $1
182
+
183
+ # Append all content until ending quote or escape
184
+ value << header.slice!(0, i)
185
+
186
+ # Remove either backslash or ending quote,
187
+ # ensures forward progress in loop
188
+ header.slice!(0, 1)
189
+
190
+ # stop parsing parameter value if found ending quote
191
+ break if c == '"'
192
+
193
+ # Only allow up to max escapes, to avoid potential denial of service
194
+ num_escapes += 1
195
+ return if num_escapes > max_escapes
196
+ escaped_char = header.slice!(0, 1)
197
+ value << escaped_char
198
+ end
199
+ else
200
+ if i = header.index(/[;,]/)
201
+ # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
202
+ value = header.slice!(0, i)
203
+ value.sub!(/[\s;,]+\z/, '')
204
+ else
205
+ # If no ending semicolon, assume remainder of line is value and stop parsing
206
+ header.strip!
207
+ value = header
208
+ header = ''
209
+ end
210
+ value.lstrip!
158
211
  end
212
+
213
+ (params[param] ||= []) << value
214
+
215
+ # skip trailing semicolons/commas/whitespace, to proceed to next parameter
216
+ header.sub!(/\A[\s;,]+/, '') unless header.empty?
159
217
  end
218
+
219
+ params
160
220
  end
161
221
  module_function :forwarded_values
162
222
 
@@ -193,17 +253,41 @@ module Rack
193
253
  # :nocov:
194
254
  end
195
255
 
256
+ # Given an array of available encoding strings, and an array of
257
+ # acceptable encodings for a request, where each element of the
258
+ # acceptable encodings array is an array where the first element
259
+ # is an encoding name and the second element is the numeric
260
+ # priority for the encoding, return the available encoding with
261
+ # the highest priority.
262
+ #
263
+ # The accept_encoding argument is typically generated by calling
264
+ # Request#accept_encoding.
265
+ #
266
+ # Example:
267
+ #
268
+ # select_best_encoding(%w(compress gzip identity),
269
+ # [["compress", 0.5], ["gzip", 1.0]])
270
+ # # => "gzip"
271
+ #
272
+ # To reduce denial of service potential, only the first 16
273
+ # acceptable encodings are considered.
196
274
  def select_best_encoding(available_encodings, accept_encoding)
197
275
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
198
276
 
277
+ # Only process the first 16 encodings
278
+ accept_encoding = accept_encoding[0...16]
199
279
  expanded_accept_encoding = []
280
+ wildcard_seen = false
200
281
 
201
282
  accept_encoding.each do |m, q|
202
283
  preference = available_encodings.index(m) || available_encodings.size
203
284
 
204
285
  if m == "*"
205
- (available_encodings - accept_encoding.map(&:first)).each do |m2|
206
- expanded_accept_encoding << [m2, q, preference]
286
+ unless wildcard_seen
287
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
288
+ expanded_accept_encoding << [m2, q, preference]
289
+ end
290
+ wildcard_seen = true
207
291
  end
208
292
  else
209
293
  expanded_accept_encoding << [m, q, preference]
@@ -211,7 +295,13 @@ module Rack
211
295
  end
212
296
 
213
297
  encoding_candidates = expanded_accept_encoding
214
- .sort_by { |_, q, p| [-q, p] }
298
+ .sort do |(_, q1, p1), (_, q2, p2)|
299
+ if r = (q1 <=> q2).nonzero?
300
+ -r
301
+ else
302
+ (p1 <=> p2).nonzero? || 0
303
+ end
304
+ end
215
305
  .map!(&:first)
216
306
 
217
307
  unless encoding_candidates.include?("identity")
@@ -399,17 +489,19 @@ module Rack
399
489
  # Parses the "Range:" header, if present, into an array of Range objects.
400
490
  # Returns nil if the header is missing or syntactically invalid.
401
491
  # Returns an empty array if none of the ranges are satisfiable.
402
- def byte_ranges(env, size)
403
- get_byte_ranges env['HTTP_RANGE'], size
492
+ def byte_ranges(env, size, max_ranges: 100)
493
+ get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
404
494
  end
405
495
 
406
- def get_byte_ranges(http_range, size)
496
+ def get_byte_ranges(http_range, size, max_ranges: 100)
407
497
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
408
498
  # Ignore Range when file size is 0 to avoid a 416 error.
409
499
  return nil if size.zero?
410
500
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
501
+ byte_range = $1
502
+ return nil if byte_range.count(',') >= max_ranges
411
503
  ranges = []
412
- $1.split(/,[ \t]*/).each do |range_spec|
504
+ byte_range.split(/,[ \t]*/).each do |range_spec|
413
505
  return nil unless range_spec.include?('-')
414
506
  range = range_spec.split('-')
415
507
  r0, r1 = range[0], range[1]
data/lib/rack/version.rb CHANGED
@@ -6,7 +6,7 @@
6
6
  # See MIT-LICENSE or https://opensource.org/licenses/MIT.
7
7
 
8
8
  module Rack
9
- VERSION = "3.2.5"
9
+ VERSION = "3.2.6"
10
10
 
11
11
  RELEASE = VERSION
12
12
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.5
4
+ version: 3.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leah Neukirchen
@@ -156,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
156
  - !ruby/object:Gem::Version
157
157
  version: '0'
158
158
  requirements: []
159
- rubygems_version: 4.0.3
159
+ rubygems_version: 4.0.6
160
160
  specification_version: 4
161
161
  summary: A modular Ruby webserver interface.
162
162
  test_files: []