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 +4 -4
- data/CHANGELOG.md +15 -1
- data/lib/rack/directory.rb +1 -1
- data/lib/rack/files.rb +1 -1
- data/lib/rack/multipart/parser.rb +24 -1
- data/lib/rack/multipart.rb +1 -1
- data/lib/rack/sendfile.rb +2 -2
- data/lib/rack/static.rb +7 -3
- data/lib/rack/utils.rb +39 -8
- data/lib/rack/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1956bc102141711f91a9f0daa1507098bd8b7ede58febde9472b875dcb0d4e4
|
|
4
|
+
data.tar.gz: a8945afc610aed0c61ce8062e3b1394a24c6a457789637cc6835cfd881dac635
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
##
|
|
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
|
|
data/lib/rack/directory.rb
CHANGED
|
@@ -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(
|
|
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
|
@@ -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
|
-
|
|
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
|
data/lib/rack/multipart.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Rack
|
|
|
12
12
|
|
|
13
13
|
EOL = "\r\n"
|
|
14
14
|
MULTIPART_BOUNDARY = "AaB03x"
|
|
15
|
-
MULTIPART = %r|\Amultipart
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
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.
|
|
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.
|
|
185
|
+
rubygems_version: 4.0.6
|
|
186
186
|
specification_version: 4
|
|
187
187
|
summary: A modular Ruby webserver interface.
|
|
188
188
|
test_files: []
|