rack 2.2.22 → 2.2.23

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: cde47be51f51633c220096db26d6dee3833210970dac5703415a343d9adec7f3
4
- data.tar.gz: 5aea7ddfbd6bd9257fb7c0586993fe0830effdee2a223173b0924c5909894ad2
3
+ metadata.gz: f1956bc102141711f91a9f0daa1507098bd8b7ede58febde9472b875dcb0d4e4
4
+ data.tar.gz: a8945afc610aed0c61ce8062e3b1394a24c6a457789637cc6835cfd881dac635
5
5
  SHA512:
6
- metadata.gz: de22c9fd5e8d0f68002522d903913e47c337485112172da4a233777794045c11dee7cceefe5e4573d4791f83fa6ffc90fbc3b5bf8249c0fef69142619bdc8c38
7
- data.tar.gz: 0cbd1f6fcc12b243467e0e7c03e6a8e775013472061c83a0f33f4404a6347fd1796432bc2779bf49b87f47b4aff64a19e2dcd438a5ff050df44c944e181429dd
6
+ metadata.gz: afe4c41e29d6112ba8fe825b33b93d10317d576add705a72de25825caa504c21eefe86cf9308df573dd422cfb4aed24e9453ad4e92720188f4f98c4d2f04c5c4
7
+ data.tar.gz: d28cd489644e2191917b28f39d689d384c21296e4f4a8a2bab7b1413f4110cb2092336039afc571d1278adbbfa4ebac9b2d27cb342b873336aa6efe037a93f4d
data/CHANGELOG.md CHANGED
@@ -2,7 +2,21 @@
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
+ ## [2.2.23] - 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-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
12
+ - [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
13
+ - [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
14
+ - [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
15
+ - [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`.
16
+ - [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
17
+ - [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
18
+
19
+ ## [2.2.22] - 2026-02-16
6
20
 
7
21
  ### Security
8
22
 
@@ -45,7 +45,7 @@ table { width:100%%; }
45
45
  class DirectoryBody < Struct.new(:root, :path, :files)
46
46
  # Yield strings for each part of the directory entry
47
47
  def each
48
- show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
48
+ show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
49
49
  yield(DIR_PAGE_HEADER % [ show_path, show_path ])
50
50
 
51
51
  unless path.chomp('/') == root
data/lib/rack/files.rb CHANGED
@@ -196,7 +196,7 @@ EOF
196
196
  status,
197
197
  {
198
198
  CONTENT_TYPE => "text/plain",
199
- CONTENT_LENGTH => body.size.to_s,
199
+ CONTENT_LENGTH => body.bytesize.to_s,
200
200
  "X-Cascade" => "pass"
201
201
  }.merge!(headers),
202
202
  [body]
@@ -41,6 +41,10 @@ module Rack
41
41
  BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
42
42
  private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
43
43
 
44
+ bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
45
+ PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
46
+ private_constant :PARSER_BYTESIZE_LIMIT
47
+
44
48
  class BoundedIO # :nodoc:
45
49
  def initialize(io, content_length)
46
50
  @io = io
@@ -81,7 +85,15 @@ module Rack
81
85
  return unless content_type
82
86
  data = content_type.match(MULTIPART)
83
87
  return unless data
84
- data[1]
88
+
89
+ unless data[1].empty?
90
+ raise EOFError, "whitespace between boundary parameter name and equal sign"
91
+ end
92
+ if data.post_match =~ /boundary\s*=/i
93
+ raise EOFError, "multiple boundary parameters found in multipart content type"
94
+ end
95
+
96
+ data[2]
85
97
  end
86
98
 
87
99
  def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
@@ -90,6 +102,10 @@ module Rack
90
102
  boundary = parse_boundary content_type
91
103
  return EMPTY unless boundary
92
104
 
105
+ if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
106
+ raise EOFError, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
107
+ end
108
+
93
109
  io = BoundedIO.new(io, content_length) if content_length
94
110
  outbuf = String.new
95
111
 
@@ -210,6 +226,7 @@ module Rack
210
226
  @mime_index = 0
211
227
  @body_retained = nil
212
228
  @retained_size = 0
229
+ @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
213
230
  @collector = Collector.new tempfile
214
231
 
215
232
  @sbuf = StringScanner.new("".dup)
@@ -221,6 +238,12 @@ module Rack
221
238
 
222
239
  def on_read(content)
223
240
  handle_empty_content!(content)
241
+ if @total_bytes_read
242
+ @total_bytes_read += content.bytesize
243
+ if @total_bytes_read > PARSER_BYTESIZE_LIMIT
244
+ raise EOFError, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
245
+ end
246
+ end
224
247
  @sbuf.concat content
225
248
  run_parser
226
249
  end
@@ -12,7 +12,7 @@ module Rack
12
12
 
13
13
  EOL = "\r\n"
14
14
  MULTIPART_BOUNDARY = "AaB03x"
15
- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
15
+ MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
16
16
  TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
17
17
  CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
18
18
  VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
data/lib/rack/sendfile.rb CHANGED
@@ -47,7 +47,7 @@ module Rack
47
47
  #
48
48
  # The X-Accel-Mapping header should specify the location on the file system,
49
49
  # followed by an equals sign (=), followed name of the private URL pattern
50
- # that it maps to. The middleware performs a simple substitution on the
50
+ # that it maps to. The middleware performs a case-insensitive substitution on the
51
51
  # resulting path.
52
52
  #
53
53
  # To enable X-Accel-Redirect, you must configure the middleware explicitly:
@@ -181,7 +181,7 @@ module Rack
181
181
  # Safe to use header: explicit config + no app mappings:
182
182
  mapping.split(',').map(&:strip).each do |m|
183
183
  internal, external = m.split('=', 2).map(&:strip)
184
- new_path = path.sub(/\A#{internal}/i, external)
184
+ new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
185
185
  return new_path unless path == new_path
186
186
  end
187
187
 
data/lib/rack/static.rb CHANGED
@@ -91,6 +91,9 @@ module Rack
91
91
  def initialize(app, options = {})
92
92
  @app = app
93
93
  @urls = options[:urls] || ["/favicon.ico"]
94
+ if @urls.kind_of?(Array)
95
+ @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
96
+ end
94
97
  @index = options[:index]
95
98
  @gzip = options[:gzip]
96
99
  @cascade = options[:cascade]
@@ -113,7 +116,7 @@ module Rack
113
116
  end
114
117
 
115
118
  def route_file(path)
116
- @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
119
+ @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
117
120
  end
118
121
 
119
122
  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
@@ -186,17 +186,41 @@ module Rack
186
186
  string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
187
187
  end
188
188
 
189
+ # Given an array of available encoding strings, and an array of
190
+ # acceptable encodings for a request, where each element of the
191
+ # acceptable encodings array is an array where the first element
192
+ # is an encoding name and the second element is the numeric
193
+ # priority for the encoding, return the available encoding with
194
+ # the highest priority.
195
+ #
196
+ # The accept_encoding argument is typically generated by calling
197
+ # Request#accept_encoding.
198
+ #
199
+ # Example:
200
+ #
201
+ # select_best_encoding(%w(compress gzip identity),
202
+ # [["compress", 0.5], ["gzip", 1.0]])
203
+ # # => "gzip"
204
+ #
205
+ # To reduce denial of service potential, only the first 16
206
+ # acceptable encodings are considered.
189
207
  def select_best_encoding(available_encodings, accept_encoding)
190
208
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
191
209
 
210
+ # Only process the first 16 encodings
211
+ accept_encoding = accept_encoding[0...16]
192
212
  expanded_accept_encoding = []
213
+ wildcard_seen = false
193
214
 
194
215
  accept_encoding.each do |m, q|
195
216
  preference = available_encodings.index(m) || available_encodings.size
196
217
 
197
218
  if m == "*"
198
- (available_encodings - accept_encoding.map(&:first)).each do |m2|
199
- expanded_accept_encoding << [m2, q, preference]
219
+ unless wildcard_seen
220
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
221
+ expanded_accept_encoding << [m2, q, preference]
222
+ end
223
+ wildcard_seen = true
200
224
  end
201
225
  else
202
226
  expanded_accept_encoding << [m, q, preference]
@@ -204,7 +228,13 @@ module Rack
204
228
  end
205
229
 
206
230
  encoding_candidates = expanded_accept_encoding
207
- .sort_by { |_, q, p| [-q, p] }
231
+ .sort do |(_, q1, p1), (_, q2, p2)|
232
+ if r = (q1 <=> q2).nonzero?
233
+ -r
234
+ else
235
+ (p1 <=> p2).nonzero? || 0
236
+ end
237
+ end
208
238
  .map!(&:first)
209
239
 
210
240
  unless encoding_candidates.include?("identity")
@@ -350,16 +380,17 @@ module Rack
350
380
  # Parses the "Range:" header, if present, into an array of Range objects.
351
381
  # Returns nil if the header is missing or syntactically invalid.
352
382
  # Returns an empty array if none of the ranges are satisfiable.
353
- def byte_ranges(env, size)
354
- warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
355
- get_byte_ranges env['HTTP_RANGE'], size
383
+ def byte_ranges(env, size, max_ranges: 100)
384
+ get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
356
385
  end
357
386
 
358
- def get_byte_ranges(http_range, size)
387
+ def get_byte_ranges(http_range, size, max_ranges: 100)
359
388
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
360
389
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
390
+ byte_range = $1
391
+ return nil if byte_range.count(',') >= max_ranges
361
392
  ranges = []
362
- $1.split(/,\s*/).each do |range_spec|
393
+ byte_range.split(/,[ \t]*/).each do |range_spec|
363
394
  return nil unless range_spec.include?('-')
364
395
  range = range_spec.split('-')
365
396
  r0, r1 = range[0], range[1]
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.22"
23
+ RELEASE = "2.2.23"
24
24
 
25
25
  # Return the Rack release as a dotted string.
26
26
  def self.release
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: 2.2.22
4
+ version: 2.2.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leah Neukirchen
@@ -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: 4.0.3
185
+ rubygems_version: 4.0.6
186
186
  specification_version: 4
187
187
  summary: A modular Ruby webserver interface.
188
188
  test_files: []