Open
Conversation
Bumps [rack](https://github.com/rack/rack) from 3.2.3 to 3.2.6. - [Release notes](https://github.com/rack/rack/releases) - [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md) - [Commits](rack/rack@v3.2.3...v3.2.6) --- updated-dependencies: - dependency-name: rack dependency-version: 3.2.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
Contributor
4 similar comments
Contributor
Contributor
Contributor
Contributor
Contributor
gem compare rack 3.2.3 3.2.6Compared versions: ["3.2.3", "3.2.6"]
DIFFERENT rubygems_version:
3.2.3: 3.6.9
3.2.6: 4.0.6
DIFFERENT version:
3.2.3: 3.2.3
3.2.6: 3.2.6
DIFFERENT files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md +61/-0
lib/rack/directory.rb +6/-3
lib/rack/files.rb +1/-1
lib/rack/mock_response.rb +11/-2
lib/rack/multipart/parser.rb +44/-3
lib/rack/request.rb +2/-2
lib/rack/sendfile.rb +2/-2
lib/rack/static.rb +7/-3
lib/rack/utils.rb +107/-15
lib/rack/version.rb +1/-1
DIFFERENT extra_rdoc_files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md +61/-0 |
4 similar comments
Contributor
gem compare rack 3.2.3 3.2.6Compared versions: ["3.2.3", "3.2.6"]
DIFFERENT rubygems_version:
3.2.3: 3.6.9
3.2.6: 4.0.6
DIFFERENT version:
3.2.3: 3.2.3
3.2.6: 3.2.6
DIFFERENT files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md +61/-0
lib/rack/directory.rb +6/-3
lib/rack/files.rb +1/-1
lib/rack/mock_response.rb +11/-2
lib/rack/multipart/parser.rb +44/-3
lib/rack/request.rb +2/-2
lib/rack/sendfile.rb +2/-2
lib/rack/static.rb +7/-3
lib/rack/utils.rb +107/-15
lib/rack/version.rb +1/-1
DIFFERENT extra_rdoc_files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md +61/-0 |
Contributor
gem compare rack 3.2.3 3.2.6Compared versions: ["3.2.3", "3.2.6"]
DIFFERENT rubygems_version:
3.2.3: 3.6.9
3.2.6: 4.0.6
DIFFERENT version:
3.2.3: 3.2.3
3.2.6: 3.2.6
DIFFERENT files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md +61/-0
lib/rack/directory.rb +6/-3
lib/rack/files.rb +1/-1
lib/rack/mock_response.rb +11/-2
lib/rack/multipart/parser.rb +44/-3
lib/rack/request.rb +2/-2
lib/rack/sendfile.rb +2/-2
lib/rack/static.rb +7/-3
lib/rack/utils.rb +107/-15
lib/rack/version.rb +1/-1
DIFFERENT extra_rdoc_files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md +61/-0 |
Contributor
gem compare rack 3.2.3 3.2.6Compared versions: ["3.2.3", "3.2.6"]
DIFFERENT rubygems_version:
3.2.3: 3.6.9
3.2.6: 4.0.6
DIFFERENT version:
3.2.3: 3.2.3
3.2.6: 3.2.6
DIFFERENT files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md +61/-0
lib/rack/directory.rb +6/-3
lib/rack/files.rb +1/-1
lib/rack/mock_response.rb +11/-2
lib/rack/multipart/parser.rb +44/-3
lib/rack/request.rb +2/-2
lib/rack/sendfile.rb +2/-2
lib/rack/static.rb +7/-3
lib/rack/utils.rb +107/-15
lib/rack/version.rb +1/-1
DIFFERENT extra_rdoc_files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md +61/-0 |
Contributor
gem compare rack 3.2.3 3.2.6Compared versions: ["3.2.3", "3.2.6"]
DIFFERENT rubygems_version:
3.2.3: 3.6.9
3.2.6: 4.0.6
DIFFERENT version:
3.2.3: 3.2.3
3.2.6: 3.2.6
DIFFERENT files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md +61/-0
lib/rack/directory.rb +6/-3
lib/rack/files.rb +1/-1
lib/rack/mock_response.rb +11/-2
lib/rack/multipart/parser.rb +44/-3
lib/rack/request.rb +2/-2
lib/rack/sendfile.rb +2/-2
lib/rack/static.rb +7/-3
lib/rack/utils.rb +107/-15
lib/rack/version.rb +1/-1
DIFFERENT extra_rdoc_files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md +61/-0 |
Contributor
gem compare --diff rack 3.2.3 3.2.6Compared versions: ["3.2.3", "3.2.6"]
DIFFERENT files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md
--- /tmp/d20260402-517-pruvkb/rack-3.2.3/CHANGELOG.md 2026-04-02 18:50:25.488453999 +0000
+++ /tmp/d20260402-517-pruvkb/rack-3.2.6/CHANGELOG.md 2026-04-02 18:50:25.498453993 +0000
@@ -4,0 +5,35 @@
+## [3.2.6] - 2026-04-01
+
+### Security
+
+- [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) Root directory disclosure via unescaped regex interpolation in `Rack::Directory`.
+- [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.
+- [CVE-2026-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) Forwarded header semicolon injection enables Host and Scheme spoofing.
+- [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
+- [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
+- [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
+- [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
+- [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
+- [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`.
+- [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
+- [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
+- [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.
+- [CVE-2026-26962](https://github.com/advisories/GHSA-rx22-g9mx-qrhv) Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
+
+## [3.2.5] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+### Fixed
+
+- Fix `Rack::MockResponse#body` when the body is a Proc. ([#2420](https://github.com/rack/rack/pull/2420), [#2423](https://github.com/rack/rack/pull/2423), [@tavianator](https://github.com/tavianator), [@ioquatix])
+
+## [3.2.4] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
+
@@ -76,0 +112,13 @@
+## [3.1.20] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+## [3.1.19] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
+
@@ -459,0 +508,13 @@
+
+## [2.2.22] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+## [2.2.21] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
lib/rack/directory.rb
--- /tmp/d20260402-517-pruvkb/rack-3.2.3/lib/rack/directory.rb 2026-04-02 18:50:25.491453998 +0000
+++ /tmp/d20260402-517-pruvkb/rack-3.2.6/lib/rack/directory.rb 2026-04-02 18:50:25.504453989 +0000
@@ -20 +20 @@
- DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
+ DIR_FILE = "<tr><td class='name'><a href='./%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
@@ -54 +54 @@
- show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
+ show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
@@ -84,0 +85 @@
+ @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
@@ -121 +122,3 @@
- return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
+
+ expanded_path = ::File.expand_path(::File.join(@root, path_info))
+ return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)
lib/rack/files.rb
--- /tmp/d20260402-517-pruvkb/rack-3.2.3/lib/rack/files.rb 2026-04-02 18:50:25.492453997 +0000
+++ /tmp/d20260402-517-pruvkb/rack-3.2.6/lib/rack/files.rb 2026-04-02 18:50:25.504453989 +0000
@@ -197 +197 @@
- CONTENT_LENGTH => body.size.to_s,
+ CONTENT_LENGTH => body.bytesize.to_s,
lib/rack/mock_response.rb
--- /tmp/d20260402-517-pruvkb/rack-3.2.3/lib/rack/mock_response.rb 2026-04-02 18:50:25.494453996 +0000
+++ /tmp/d20260402-517-pruvkb/rack-3.2.6/lib/rack/mock_response.rb 2026-04-02 18:50:25.507453987 +0000
@@ -2,0 +3 @@
+require 'stringio'
@@ -85,2 +86,10 @@
- @body.each do |chunk|
- buffer << chunk
+ begin
+ if @body.respond_to?(:each)
+ @body.each do |chunk|
+ buffer << chunk
+ end
+ else
+ @body.call(StringIO.new(buffer))
+ end
+ ensure
+ @body.close if @body.respond_to?(:close)
lib/rack/multipart/parser.rb
--- /tmp/d20260402-517-pruvkb/rack-3.2.3/lib/rack/multipart/parser.rb 2026-04-02 18:50:25.494453996 +0000
+++ /tmp/d20260402-517-pruvkb/rack-3.2.6/lib/rack/multipart/parser.rb 2026-04-02 18:50:25.508453986 +0000
@@ -36 +36 @@
- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
+ MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
@@ -82,0 +83,7 @@
+ bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
+ PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
+ private_constant :PARSER_BYTESIZE_LIMIT
+
+ CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
+ private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
+
@@ -119 +126,9 @@
- data[1]
+
+ unless data[1].empty?
+ raise Error, "whitespace between boundary parameter name and equal sign"
+ end
+ if data.post_match.match?(/boundary\s*=/i)
+ raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
+ end
+
+ data[2]
@@ -127,0 +143,4 @@
+ if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
+ raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
+ end
+
@@ -243,0 +263,2 @@
+ @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
+ @content_disposition_quoted_escapes = 0
@@ -254,0 +276 @@
+ @total_bytes_read &&= nil if io.is_a?(BoundedIO)
@@ -292,0 +315,6 @@
+ if @total_bytes_read
+ @total_bytes_read += content.bytesize
+ if @total_bytes_read > PARSER_BYTESIZE_LIMIT
+ raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
+ end
+ end
@@ -341,0 +370,3 @@
+ OBS_UNFOLD = /\r\n([ \t])/
+ private_constant :OBS_UNFOLD
+
@@ -345,0 +377,2 @@
+ content_type.gsub!(OBS_UNFOLD, '\1') if content_type
+
@@ -348,0 +382,3 @@
+ # Implement OBS unfolding (RFC 5322 Section 2.2.3)
+ disposition.gsub!(OBS_UNFOLD, '\1')
+
@@ -385,0 +422,5 @@
+ @content_disposition_quoted_escapes += 1
+ if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
+ raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
+ end
+
@@ -454 +495 @@
- raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
+ raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
lib/rack/request.rb
--- /tmp/d20260402-517-pruvkb/rack-3.2.3/lib/rack/request.rb 2026-04-02 18:50:25.496453994 +0000
+++ /tmp/d20260402-517-pruvkb/rack-3.2.6/lib/rack/request.rb 2026-04-02 18:50:25.515453982 +0000
@@ -726,2 +726,2 @@
- # Match any other printable string (except square brackets) as a hostname
- (?<address>[[[:graph:]&&[^\[\]]]]*?)
+ # Match characters allowed by RFC 3986 Section 3.2.2
+ (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
lib/rack/sendfile.rb
--- /tmp/d20260402-517-pruvkb/rack-3.2.3/lib/rack/sendfile.rb 2026-04-02 18:50:25.496453994 +0000
+++ /tmp/d20260402-517-pruvkb/rack-3.2.6/lib/rack/sendfile.rb 2026-04-02 18:50:25.516453981 +0000
@@ -54 +54 @@
- # that it maps to. The middleware performs a simple substitution on the
+ # that it maps to. The middleware performs a case-insensitive substitution on the
@@ -189 +189 @@
- new_path = path.sub(/\A#{internal}/i, external)
+ new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
lib/rack/static.rb
--- /tmp/d20260402-517-pruvkb/rack-3.2.3/lib/rack/static.rb 2026-04-02 18:50:25.496453994 +0000
+++ /tmp/d20260402-517-pruvkb/rack-3.2.6/lib/rack/static.rb 2026-04-02 18:50:25.516453981 +0000
@@ -95,0 +96,3 @@
+ if @urls.kind_of?(Array)
+ @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
+ end
@@ -118 +121 @@
- @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
+ @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
@@ -167,0 +171,2 @@
+ path = ::Rack::Utils.unescape_path(path)
+
@@ -175 +179,0 @@
- path = ::Rack::Utils.unescape(path)
@@ -178 +182 @@
- /\.(#{rule.join('|')})\z/.match?(path)
+ /\.#{Regexp.union(rule)}\z/.match?(path)
lib/rack/utils.rb
--- /tmp/d20260402-517-pruvkb/rack-3.2.3/lib/rack/utils.rb 2026-04-02 18:50:25.497453994 +0000
+++ /tmp/d20260402-517-pruvkb/rack-3.2.6/lib/rack/utils.rb 2026-04-02 18:50:25.518453980 +0000
@@ -149,3 +149,2 @@
- def forwarded_values(forwarded_header)
- return nil unless forwarded_header
- forwarded_header = forwarded_header.to_s.gsub("\n", ";")
+ ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze
+ private_constant :ALLOWED_FORWARED_PARAMS
@@ -153,5 +152,59 @@
- forwarded_header.split(';').each_with_object({}) do |field, values|
- field.split(',').each do |pair|
- pair = pair.split('=').map(&:strip).join('=')
- return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
- (values[$1.downcase.to_sym] ||= []) << $2
+ def forwarded_values(forwarded_header)
+ return unless forwarded_header
+ header = forwarded_header.to_s.tr("\n", ";")
+ header.sub!(/\A[\s;,]+/, '')
+ num_params = num_escapes = 0
+ max_params = max_escapes = 1024
+ params = {}
+
+ # Parse parameter list
+ while i = header.index('=')
+ # Only parse up to max parameters, to avoid potential denial of service
+ num_params += 1
+ return if num_params > max_params
+
+ # Found end of parameter name, ensure forward progress in loop
+ param = header.slice!(0, i+1)
+
+ # Remove ending equals and preceding whitespace from parameter name
+ param.chomp!('=')
+ param.strip!
+ param.downcase!
+ return unless param = ALLOWED_FORWARED_PARAMS[param]
+
+ if header[0] == '"'
+ # Parameter value is quoted, parse it, handling backslash escapes
+ header.slice!(0, 1)
+ value = String.new
+
+ while i = header.index(/(["\\])/)
+ c = $1
+
+ # Append all content until ending quote or escape
+ value << header.slice!(0, i)
+
+ # Remove either backslash or ending quote,
+ # ensures forward progress in loop
+ header.slice!(0, 1)
+
+ # stop parsing parameter value if found ending quote
+ break if c == '"'
+
+ # Only allow up to max escapes, to avoid potential denial of service
+ num_escapes += 1
+ return if num_escapes > max_escapes
+ escaped_char = header.slice!(0, 1)
+ value << escaped_char
+ end
+ else
+ if i = header.index(/[;,]/)
+ # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
+ value = header.slice!(0, i)
+ value.sub!(/[\s;,]+\z/, '')
+ else
+ # If no ending semicolon, assume remainder of line is value and stop parsing
+ header.strip!
+ value = header
+ header = ''
+ end
+ value.lstrip!
@@ -158,0 +212,5 @@
+
+ (params[param] ||= []) << value
+
+ # skip trailing semicolons/commas/whitespace, to proceed to next parameter
+ header.sub!(/\A[\s;,]+/, '') unless header.empty?
@@ -159,0 +218,2 @@
+
+ params
@@ -195,0 +256,18 @@
+ # Given an array of available encoding strings, and an array of
+ # acceptable encodings for a request, where each element of the
+ # acceptable encodings array is an array where the first element
+ # is an encoding name and the second element is the numeric
+ # priority for the encoding, return the available encoding with
+ # the highest priority.
+ #
+ # The accept_encoding argument is typically generated by calling
+ # Request#accept_encoding.
+ #
+ # Example:
+ #
+ # select_best_encoding(%w(compress gzip identity),
+ # [["compress", 0.5], ["gzip", 1.0]])
+ # # => "gzip"
+ #
+ # To reduce denial of service potential, only the first 16
+ # acceptable encodings are considered.
@@ -198,0 +277,2 @@
+ # Only process the first 16 encodings
+ accept_encoding = accept_encoding[0...16]
@@ -199,0 +280 @@
+ wildcard_seen = false
@@ -205,2 +286,5 @@
- (available_encodings - accept_encoding.map(&:first)).each do |m2|
- expanded_accept_encoding << [m2, q, preference]
+ unless wildcard_seen
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
+ expanded_accept_encoding << [m2, q, preference]
+ end
+ wildcard_seen = true
@@ -214 +298,7 @@
- .sort_by { |_, q, p| [-q, p] }
+ .sort do |(_, q1, p1), (_, q2, p2)|
+ if r = (q1 <=> q2).nonzero?
+ -r
+ else
+ (p1 <=> p2).nonzero? || 0
+ end
+ end
@@ -402,2 +492,2 @@
- def byte_ranges(env, size)
- get_byte_ranges env['HTTP_RANGE'], size
+ def byte_ranges(env, size, max_ranges: 100)
+ get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
@@ -406 +496 @@
- def get_byte_ranges(http_range, size)
+ def get_byte_ranges(http_range, size, max_ranges: 100)
@@ -410,0 +501,2 @@
+ byte_range = $1
+ return nil if byte_range.count(',') >= max_ranges
@@ -412 +504 @@
- $1.split(/,[ \t]*/).each do |range_spec|
+ byte_range.split(/,[ \t]*/).each do |range_spec|
lib/rack/version.rb
--- /tmp/d20260402-517-pruvkb/rack-3.2.3/lib/rack/version.rb 2026-04-02 18:50:25.497453994 +0000
+++ /tmp/d20260402-517-pruvkb/rack-3.2.6/lib/rack/version.rb 2026-04-02 18:50:25.518453980 +0000
@@ -9 +9 @@
- VERSION = "3.2.3"
+ VERSION = "3.2.6" |
Contributor
gem compare --diff rack 3.2.3 3.2.6Compared versions: ["3.2.3", "3.2.6"]
DIFFERENT files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md
--- /tmp/d20260402-519-yrm4vn/rack-3.2.3/CHANGELOG.md 2026-04-02 18:50:26.080718666 +0000
+++ /tmp/d20260402-519-yrm4vn/rack-3.2.6/CHANGELOG.md 2026-04-02 18:50:26.089718659 +0000
@@ -4,0 +5,35 @@
+## [3.2.6] - 2026-04-01
+
+### Security
+
+- [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) Root directory disclosure via unescaped regex interpolation in `Rack::Directory`.
+- [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.
+- [CVE-2026-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) Forwarded header semicolon injection enables Host and Scheme spoofing.
+- [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
+- [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
+- [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
+- [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
+- [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
+- [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`.
+- [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
+- [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
+- [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.
+- [CVE-2026-26962](https://github.com/advisories/GHSA-rx22-g9mx-qrhv) Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
+
+## [3.2.5] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+### Fixed
+
+- Fix `Rack::MockResponse#body` when the body is a Proc. ([#2420](https://github.com/rack/rack/pull/2420), [#2423](https://github.com/rack/rack/pull/2423), [@tavianator](https://github.com/tavianator), [@ioquatix])
+
+## [3.2.4] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
+
@@ -76,0 +112,13 @@
+## [3.1.20] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+## [3.1.19] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
+
@@ -459,0 +508,13 @@
+
+## [2.2.22] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+## [2.2.21] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
lib/rack/directory.rb
--- /tmp/d20260402-519-yrm4vn/rack-3.2.3/lib/rack/directory.rb 2026-04-02 18:50:26.082718665 +0000
+++ /tmp/d20260402-519-yrm4vn/rack-3.2.6/lib/rack/directory.rb 2026-04-02 18:50:26.092718656 +0000
@@ -20 +20 @@
- DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
+ DIR_FILE = "<tr><td class='name'><a href='./%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
@@ -54 +54 @@
- show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
+ show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
@@ -84,0 +85 @@
+ @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
@@ -121 +122,3 @@
- return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
+
+ expanded_path = ::File.expand_path(::File.join(@root, path_info))
+ return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)
lib/rack/files.rb
--- /tmp/d20260402-519-yrm4vn/rack-3.2.3/lib/rack/files.rb 2026-04-02 18:50:26.082718665 +0000
+++ /tmp/d20260402-519-yrm4vn/rack-3.2.6/lib/rack/files.rb 2026-04-02 18:50:26.092718656 +0000
@@ -197 +197 @@
- CONTENT_LENGTH => body.size.to_s,
+ CONTENT_LENGTH => body.bytesize.to_s,
lib/rack/mock_response.rb
--- /tmp/d20260402-519-yrm4vn/rack-3.2.3/lib/rack/mock_response.rb 2026-04-02 18:50:26.084718663 +0000
+++ /tmp/d20260402-519-yrm4vn/rack-3.2.6/lib/rack/mock_response.rb 2026-04-02 18:50:26.092718656 +0000
@@ -2,0 +3 @@
+require 'stringio'
@@ -85,2 +86,10 @@
- @body.each do |chunk|
- buffer << chunk
+ begin
+ if @body.respond_to?(:each)
+ @body.each do |chunk|
+ buffer << chunk
+ end
+ else
+ @body.call(StringIO.new(buffer))
+ end
+ ensure
+ @body.close if @body.respond_to?(:close)
lib/rack/multipart/parser.rb
--- /tmp/d20260402-519-yrm4vn/rack-3.2.3/lib/rack/multipart/parser.rb 2026-04-02 18:50:26.085718662 +0000
+++ /tmp/d20260402-519-yrm4vn/rack-3.2.6/lib/rack/multipart/parser.rb 2026-04-02 18:50:26.094718655 +0000
@@ -36 +36 @@
- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
+ MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
@@ -82,0 +83,7 @@
+ bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
+ PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
+ private_constant :PARSER_BYTESIZE_LIMIT
+
+ CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
+ private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
+
@@ -119 +126,9 @@
- data[1]
+
+ unless data[1].empty?
+ raise Error, "whitespace between boundary parameter name and equal sign"
+ end
+ if data.post_match.match?(/boundary\s*=/i)
+ raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
+ end
+
+ data[2]
@@ -127,0 +143,4 @@
+ if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
+ raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
+ end
+
@@ -243,0 +263,2 @@
+ @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
+ @content_disposition_quoted_escapes = 0
@@ -254,0 +276 @@
+ @total_bytes_read &&= nil if io.is_a?(BoundedIO)
@@ -292,0 +315,6 @@
+ if @total_bytes_read
+ @total_bytes_read += content.bytesize
+ if @total_bytes_read > PARSER_BYTESIZE_LIMIT
+ raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
+ end
+ end
@@ -341,0 +370,3 @@
+ OBS_UNFOLD = /\r\n([ \t])/
+ private_constant :OBS_UNFOLD
+
@@ -345,0 +377,2 @@
+ content_type.gsub!(OBS_UNFOLD, '\1') if content_type
+
@@ -348,0 +382,3 @@
+ # Implement OBS unfolding (RFC 5322 Section 2.2.3)
+ disposition.gsub!(OBS_UNFOLD, '\1')
+
@@ -385,0 +422,5 @@
+ @content_disposition_quoted_escapes += 1
+ if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
+ raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
+ end
+
@@ -454 +495 @@
- raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
+ raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
lib/rack/request.rb
--- /tmp/d20260402-519-yrm4vn/rack-3.2.3/lib/rack/request.rb 2026-04-02 18:50:26.086718661 +0000
+++ /tmp/d20260402-519-yrm4vn/rack-3.2.6/lib/rack/request.rb 2026-04-02 18:50:26.094718655 +0000
@@ -726,2 +726,2 @@
- # Match any other printable string (except square brackets) as a hostname
- (?<address>[[[:graph:]&&[^\[\]]]]*?)
+ # Match characters allowed by RFC 3986 Section 3.2.2
+ (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
lib/rack/sendfile.rb
--- /tmp/d20260402-519-yrm4vn/rack-3.2.3/lib/rack/sendfile.rb 2026-04-02 18:50:26.087718661 +0000
+++ /tmp/d20260402-519-yrm4vn/rack-3.2.6/lib/rack/sendfile.rb 2026-04-02 18:50:26.095718654 +0000
@@ -54 +54 @@
- # that it maps to. The middleware performs a simple substitution on the
+ # that it maps to. The middleware performs a case-insensitive substitution on the
@@ -189 +189 @@
- new_path = path.sub(/\A#{internal}/i, external)
+ new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
lib/rack/static.rb
--- /tmp/d20260402-519-yrm4vn/rack-3.2.3/lib/rack/static.rb 2026-04-02 18:50:26.087718661 +0000
+++ /tmp/d20260402-519-yrm4vn/rack-3.2.6/lib/rack/static.rb 2026-04-02 18:50:26.095718654 +0000
@@ -95,0 +96,3 @@
+ if @urls.kind_of?(Array)
+ @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
+ end
@@ -118 +121 @@
- @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
+ @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
@@ -167,0 +171,2 @@
+ path = ::Rack::Utils.unescape_path(path)
+
@@ -175 +179,0 @@
- path = ::Rack::Utils.unescape(path)
@@ -178 +182 @@
- /\.(#{rule.join('|')})\z/.match?(path)
+ /\.#{Regexp.union(rule)}\z/.match?(path)
lib/rack/utils.rb
--- /tmp/d20260402-519-yrm4vn/rack-3.2.3/lib/rack/utils.rb 2026-04-02 18:50:26.089718659 +0000
+++ /tmp/d20260402-519-yrm4vn/rack-3.2.6/lib/rack/utils.rb 2026-04-02 18:50:26.096718653 +0000
@@ -149,3 +149,2 @@
- def forwarded_values(forwarded_header)
- return nil unless forwarded_header
- forwarded_header = forwarded_header.to_s.gsub("\n", ";")
+ ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze
+ private_constant :ALLOWED_FORWARED_PARAMS
@@ -153,5 +152,59 @@
- forwarded_header.split(';').each_with_object({}) do |field, values|
- field.split(',').each do |pair|
- pair = pair.split('=').map(&:strip).join('=')
- return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
- (values[$1.downcase.to_sym] ||= []) << $2
+ def forwarded_values(forwarded_header)
+ return unless forwarded_header
+ header = forwarded_header.to_s.tr("\n", ";")
+ header.sub!(/\A[\s;,]+/, '')
+ num_params = num_escapes = 0
+ max_params = max_escapes = 1024
+ params = {}
+
+ # Parse parameter list
+ while i = header.index('=')
+ # Only parse up to max parameters, to avoid potential denial of service
+ num_params += 1
+ return if num_params > max_params
+
+ # Found end of parameter name, ensure forward progress in loop
+ param = header.slice!(0, i+1)
+
+ # Remove ending equals and preceding whitespace from parameter name
+ param.chomp!('=')
+ param.strip!
+ param.downcase!
+ return unless param = ALLOWED_FORWARED_PARAMS[param]
+
+ if header[0] == '"'
+ # Parameter value is quoted, parse it, handling backslash escapes
+ header.slice!(0, 1)
+ value = String.new
+
+ while i = header.index(/(["\\])/)
+ c = $1
+
+ # Append all content until ending quote or escape
+ value << header.slice!(0, i)
+
+ # Remove either backslash or ending quote,
+ # ensures forward progress in loop
+ header.slice!(0, 1)
+
+ # stop parsing parameter value if found ending quote
+ break if c == '"'
+
+ # Only allow up to max escapes, to avoid potential denial of service
+ num_escapes += 1
+ return if num_escapes > max_escapes
+ escaped_char = header.slice!(0, 1)
+ value << escaped_char
+ end
+ else
+ if i = header.index(/[;,]/)
+ # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
+ value = header.slice!(0, i)
+ value.sub!(/[\s;,]+\z/, '')
+ else
+ # If no ending semicolon, assume remainder of line is value and stop parsing
+ header.strip!
+ value = header
+ header = ''
+ end
+ value.lstrip!
@@ -158,0 +212,5 @@
+
+ (params[param] ||= []) << value
+
+ # skip trailing semicolons/commas/whitespace, to proceed to next parameter
+ header.sub!(/\A[\s;,]+/, '') unless header.empty?
@@ -159,0 +218,2 @@
+
+ params
@@ -195,0 +256,18 @@
+ # Given an array of available encoding strings, and an array of
+ # acceptable encodings for a request, where each element of the
+ # acceptable encodings array is an array where the first element
+ # is an encoding name and the second element is the numeric
+ # priority for the encoding, return the available encoding with
+ # the highest priority.
+ #
+ # The accept_encoding argument is typically generated by calling
+ # Request#accept_encoding.
+ #
+ # Example:
+ #
+ # select_best_encoding(%w(compress gzip identity),
+ # [["compress", 0.5], ["gzip", 1.0]])
+ # # => "gzip"
+ #
+ # To reduce denial of service potential, only the first 16
+ # acceptable encodings are considered.
@@ -198,0 +277,2 @@
+ # Only process the first 16 encodings
+ accept_encoding = accept_encoding[0...16]
@@ -199,0 +280 @@
+ wildcard_seen = false
@@ -205,2 +286,5 @@
- (available_encodings - accept_encoding.map(&:first)).each do |m2|
- expanded_accept_encoding << [m2, q, preference]
+ unless wildcard_seen
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
+ expanded_accept_encoding << [m2, q, preference]
+ end
+ wildcard_seen = true
@@ -214 +298,7 @@
- .sort_by { |_, q, p| [-q, p] }
+ .sort do |(_, q1, p1), (_, q2, p2)|
+ if r = (q1 <=> q2).nonzero?
+ -r
+ else
+ (p1 <=> p2).nonzero? || 0
+ end
+ end
@@ -402,2 +492,2 @@
- def byte_ranges(env, size)
- get_byte_ranges env['HTTP_RANGE'], size
+ def byte_ranges(env, size, max_ranges: 100)
+ get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
@@ -406 +496 @@
- def get_byte_ranges(http_range, size)
+ def get_byte_ranges(http_range, size, max_ranges: 100)
@@ -410,0 +501,2 @@
+ byte_range = $1
+ return nil if byte_range.count(',') >= max_ranges
@@ -412 +504 @@
- $1.split(/,[ \t]*/).each do |range_spec|
+ byte_range.split(/,[ \t]*/).each do |range_spec|
lib/rack/version.rb
--- /tmp/d20260402-519-yrm4vn/rack-3.2.3/lib/rack/version.rb 2026-04-02 18:50:26.089718659 +0000
+++ /tmp/d20260402-519-yrm4vn/rack-3.2.6/lib/rack/version.rb 2026-04-02 18:50:26.096718653 +0000
@@ -9 +9 @@
- VERSION = "3.2.3"
+ VERSION = "3.2.6" |
Contributor
gem compare --diff rack 3.2.3 3.2.6Compared versions: ["3.2.3", "3.2.6"]
DIFFERENT files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md
--- /tmp/d20260402-519-3lzjdb/rack-3.2.3/CHANGELOG.md 2026-04-02 18:50:26.611712139 +0000
+++ /tmp/d20260402-519-3lzjdb/rack-3.2.6/CHANGELOG.md 2026-04-02 18:50:26.621712178 +0000
@@ -4,0 +5,35 @@
+## [3.2.6] - 2026-04-01
+
+### Security
+
+- [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) Root directory disclosure via unescaped regex interpolation in `Rack::Directory`.
+- [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.
+- [CVE-2026-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) Forwarded header semicolon injection enables Host and Scheme spoofing.
+- [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
+- [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
+- [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
+- [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
+- [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
+- [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`.
+- [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
+- [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
+- [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.
+- [CVE-2026-26962](https://github.com/advisories/GHSA-rx22-g9mx-qrhv) Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
+
+## [3.2.5] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+### Fixed
+
+- Fix `Rack::MockResponse#body` when the body is a Proc. ([#2420](https://github.com/rack/rack/pull/2420), [#2423](https://github.com/rack/rack/pull/2423), [@tavianator](https://github.com/tavianator), [@ioquatix])
+
+## [3.2.4] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
+
@@ -76,0 +112,13 @@
+## [3.1.20] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+## [3.1.19] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
+
@@ -459,0 +508,13 @@
+
+## [2.2.22] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+## [2.2.21] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
lib/rack/directory.rb
--- /tmp/d20260402-519-3lzjdb/rack-3.2.3/lib/rack/directory.rb 2026-04-02 18:50:26.615712155 +0000
+++ /tmp/d20260402-519-3lzjdb/rack-3.2.6/lib/rack/directory.rb 2026-04-02 18:50:26.624712190 +0000
@@ -20 +20 @@
- DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
+ DIR_FILE = "<tr><td class='name'><a href='./%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
@@ -54 +54 @@
- show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
+ show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
@@ -84,0 +85 @@
+ @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
@@ -121 +122,3 @@
- return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
+
+ expanded_path = ::File.expand_path(::File.join(@root, path_info))
+ return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)
lib/rack/files.rb
--- /tmp/d20260402-519-3lzjdb/rack-3.2.3/lib/rack/files.rb 2026-04-02 18:50:26.615712155 +0000
+++ /tmp/d20260402-519-3lzjdb/rack-3.2.6/lib/rack/files.rb 2026-04-02 18:50:26.624712190 +0000
@@ -197 +197 @@
- CONTENT_LENGTH => body.size.to_s,
+ CONTENT_LENGTH => body.bytesize.to_s,
lib/rack/mock_response.rb
--- /tmp/d20260402-519-3lzjdb/rack-3.2.3/lib/rack/mock_response.rb 2026-04-02 18:50:26.617712163 +0000
+++ /tmp/d20260402-519-3lzjdb/rack-3.2.6/lib/rack/mock_response.rb 2026-04-02 18:50:26.626712198 +0000
@@ -2,0 +3 @@
+require 'stringio'
@@ -85,2 +86,10 @@
- @body.each do |chunk|
- buffer << chunk
+ begin
+ if @body.respond_to?(:each)
+ @body.each do |chunk|
+ buffer << chunk
+ end
+ else
+ @body.call(StringIO.new(buffer))
+ end
+ ensure
+ @body.close if @body.respond_to?(:close)
lib/rack/multipart/parser.rb
--- /tmp/d20260402-519-3lzjdb/rack-3.2.3/lib/rack/multipart/parser.rb 2026-04-02 18:50:26.618712167 +0000
+++ /tmp/d20260402-519-3lzjdb/rack-3.2.6/lib/rack/multipart/parser.rb 2026-04-02 18:50:26.626712198 +0000
@@ -36 +36 @@
- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
+ MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
@@ -82,0 +83,7 @@
+ bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
+ PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
+ private_constant :PARSER_BYTESIZE_LIMIT
+
+ CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
+ private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
+
@@ -119 +126,9 @@
- data[1]
+
+ unless data[1].empty?
+ raise Error, "whitespace between boundary parameter name and equal sign"
+ end
+ if data.post_match.match?(/boundary\s*=/i)
+ raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
+ end
+
+ data[2]
@@ -127,0 +143,4 @@
+ if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
+ raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
+ end
+
@@ -243,0 +263,2 @@
+ @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
+ @content_disposition_quoted_escapes = 0
@@ -254,0 +276 @@
+ @total_bytes_read &&= nil if io.is_a?(BoundedIO)
@@ -292,0 +315,6 @@
+ if @total_bytes_read
+ @total_bytes_read += content.bytesize
+ if @total_bytes_read > PARSER_BYTESIZE_LIMIT
+ raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
+ end
+ end
@@ -341,0 +370,3 @@
+ OBS_UNFOLD = /\r\n([ \t])/
+ private_constant :OBS_UNFOLD
+
@@ -345,0 +377,2 @@
+ content_type.gsub!(OBS_UNFOLD, '\1') if content_type
+
@@ -348,0 +382,3 @@
+ # Implement OBS unfolding (RFC 5322 Section 2.2.3)
+ disposition.gsub!(OBS_UNFOLD, '\1')
+
@@ -385,0 +422,5 @@
+ @content_disposition_quoted_escapes += 1
+ if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
+ raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
+ end
+
@@ -454 +495 @@
- raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
+ raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
lib/rack/request.rb
--- /tmp/d20260402-519-3lzjdb/rack-3.2.3/lib/rack/request.rb 2026-04-02 18:50:26.618712167 +0000
+++ /tmp/d20260402-519-3lzjdb/rack-3.2.6/lib/rack/request.rb 2026-04-02 18:50:26.628712206 +0000
@@ -726,2 +726,2 @@
- # Match any other printable string (except square brackets) as a hostname
- (?<address>[[[:graph:]&&[^\[\]]]]*?)
+ # Match characters allowed by RFC 3986 Section 3.2.2
+ (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
lib/rack/sendfile.rb
--- /tmp/d20260402-519-3lzjdb/rack-3.2.3/lib/rack/sendfile.rb 2026-04-02 18:50:26.619712171 +0000
+++ /tmp/d20260402-519-3lzjdb/rack-3.2.6/lib/rack/sendfile.rb 2026-04-02 18:50:26.628712206 +0000
@@ -54 +54 @@
- # that it maps to. The middleware performs a simple substitution on the
+ # that it maps to. The middleware performs a case-insensitive substitution on the
@@ -189 +189 @@
- new_path = path.sub(/\A#{internal}/i, external)
+ new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
lib/rack/static.rb
--- /tmp/d20260402-519-3lzjdb/rack-3.2.3/lib/rack/static.rb 2026-04-02 18:50:26.620712175 +0000
+++ /tmp/d20260402-519-3lzjdb/rack-3.2.6/lib/rack/static.rb 2026-04-02 18:50:26.629712210 +0000
@@ -95,0 +96,3 @@
+ if @urls.kind_of?(Array)
+ @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
+ end
@@ -118 +121 @@
- @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
+ @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
@@ -167,0 +171,2 @@
+ path = ::Rack::Utils.unescape_path(path)
+
@@ -175 +179,0 @@
- path = ::Rack::Utils.unescape(path)
@@ -178 +182 @@
- /\.(#{rule.join('|')})\z/.match?(path)
+ /\.#{Regexp.union(rule)}\z/.match?(path)
lib/rack/utils.rb
--- /tmp/d20260402-519-3lzjdb/rack-3.2.3/lib/rack/utils.rb 2026-04-02 18:50:26.620712175 +0000
+++ /tmp/d20260402-519-3lzjdb/rack-3.2.6/lib/rack/utils.rb 2026-04-02 18:50:26.629712210 +0000
@@ -149,3 +149,2 @@
- def forwarded_values(forwarded_header)
- return nil unless forwarded_header
- forwarded_header = forwarded_header.to_s.gsub("\n", ";")
+ ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze
+ private_constant :ALLOWED_FORWARED_PARAMS
@@ -153,5 +152,59 @@
- forwarded_header.split(';').each_with_object({}) do |field, values|
- field.split(',').each do |pair|
- pair = pair.split('=').map(&:strip).join('=')
- return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
- (values[$1.downcase.to_sym] ||= []) << $2
+ def forwarded_values(forwarded_header)
+ return unless forwarded_header
+ header = forwarded_header.to_s.tr("\n", ";")
+ header.sub!(/\A[\s;,]+/, '')
+ num_params = num_escapes = 0
+ max_params = max_escapes = 1024
+ params = {}
+
+ # Parse parameter list
+ while i = header.index('=')
+ # Only parse up to max parameters, to avoid potential denial of service
+ num_params += 1
+ return if num_params > max_params
+
+ # Found end of parameter name, ensure forward progress in loop
+ param = header.slice!(0, i+1)
+
+ # Remove ending equals and preceding whitespace from parameter name
+ param.chomp!('=')
+ param.strip!
+ param.downcase!
+ return unless param = ALLOWED_FORWARED_PARAMS[param]
+
+ if header[0] == '"'
+ # Parameter value is quoted, parse it, handling backslash escapes
+ header.slice!(0, 1)
+ value = String.new
+
+ while i = header.index(/(["\\])/)
+ c = $1
+
+ # Append all content until ending quote or escape
+ value << header.slice!(0, i)
+
+ # Remove either backslash or ending quote,
+ # ensures forward progress in loop
+ header.slice!(0, 1)
+
+ # stop parsing parameter value if found ending quote
+ break if c == '"'
+
+ # Only allow up to max escapes, to avoid potential denial of service
+ num_escapes += 1
+ return if num_escapes > max_escapes
+ escaped_char = header.slice!(0, 1)
+ value << escaped_char
+ end
+ else
+ if i = header.index(/[;,]/)
+ # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
+ value = header.slice!(0, i)
+ value.sub!(/[\s;,]+\z/, '')
+ else
+ # If no ending semicolon, assume remainder of line is value and stop parsing
+ header.strip!
+ value = header
+ header = ''
+ end
+ value.lstrip!
@@ -158,0 +212,5 @@
+
+ (params[param] ||= []) << value
+
+ # skip trailing semicolons/commas/whitespace, to proceed to next parameter
+ header.sub!(/\A[\s;,]+/, '') unless header.empty?
@@ -159,0 +218,2 @@
+
+ params
@@ -195,0 +256,18 @@
+ # Given an array of available encoding strings, and an array of
+ # acceptable encodings for a request, where each element of the
+ # acceptable encodings array is an array where the first element
+ # is an encoding name and the second element is the numeric
+ # priority for the encoding, return the available encoding with
+ # the highest priority.
+ #
+ # The accept_encoding argument is typically generated by calling
+ # Request#accept_encoding.
+ #
+ # Example:
+ #
+ # select_best_encoding(%w(compress gzip identity),
+ # [["compress", 0.5], ["gzip", 1.0]])
+ # # => "gzip"
+ #
+ # To reduce denial of service potential, only the first 16
+ # acceptable encodings are considered.
@@ -198,0 +277,2 @@
+ # Only process the first 16 encodings
+ accept_encoding = accept_encoding[0...16]
@@ -199,0 +280 @@
+ wildcard_seen = false
@@ -205,2 +286,5 @@
- (available_encodings - accept_encoding.map(&:first)).each do |m2|
- expanded_accept_encoding << [m2, q, preference]
+ unless wildcard_seen
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
+ expanded_accept_encoding << [m2, q, preference]
+ end
+ wildcard_seen = true
@@ -214 +298,7 @@
- .sort_by { |_, q, p| [-q, p] }
+ .sort do |(_, q1, p1), (_, q2, p2)|
+ if r = (q1 <=> q2).nonzero?
+ -r
+ else
+ (p1 <=> p2).nonzero? || 0
+ end
+ end
@@ -402,2 +492,2 @@
- def byte_ranges(env, size)
- get_byte_ranges env['HTTP_RANGE'], size
+ def byte_ranges(env, size, max_ranges: 100)
+ get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
@@ -406 +496 @@
- def get_byte_ranges(http_range, size)
+ def get_byte_ranges(http_range, size, max_ranges: 100)
@@ -410,0 +501,2 @@
+ byte_range = $1
+ return nil if byte_range.count(',') >= max_ranges
@@ -412 +504 @@
- $1.split(/,[ \t]*/).each do |range_spec|
+ byte_range.split(/,[ \t]*/).each do |range_spec|
lib/rack/version.rb
--- /tmp/d20260402-519-3lzjdb/rack-3.2.3/lib/rack/version.rb 2026-04-02 18:50:26.620712175 +0000
+++ /tmp/d20260402-519-3lzjdb/rack-3.2.6/lib/rack/version.rb 2026-04-02 18:50:26.629712210 +0000
@@ -9 +9 @@
- VERSION = "3.2.3"
+ VERSION = "3.2.6" |
Contributor
gem compare --diff rack 3.2.3 3.2.6Compared versions: ["3.2.3", "3.2.6"]
DIFFERENT files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md
--- /tmp/d20260402-542-l9gziq/rack-3.2.3/CHANGELOG.md 2026-04-02 18:50:32.129164248 +0000
+++ /tmp/d20260402-542-l9gziq/rack-3.2.6/CHANGELOG.md 2026-04-02 18:50:32.137164183 +0000
@@ -4,0 +5,35 @@
+## [3.2.6] - 2026-04-01
+
+### Security
+
+- [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) Root directory disclosure via unescaped regex interpolation in `Rack::Directory`.
+- [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.
+- [CVE-2026-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) Forwarded header semicolon injection enables Host and Scheme spoofing.
+- [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
+- [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
+- [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
+- [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
+- [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
+- [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`.
+- [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
+- [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
+- [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.
+- [CVE-2026-26962](https://github.com/advisories/GHSA-rx22-g9mx-qrhv) Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
+
+## [3.2.5] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+### Fixed
+
+- Fix `Rack::MockResponse#body` when the body is a Proc. ([#2420](https://github.com/rack/rack/pull/2420), [#2423](https://github.com/rack/rack/pull/2423), [@tavianator](https://github.com/tavianator), [@ioquatix])
+
+## [3.2.4] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
+
@@ -76,0 +112,13 @@
+## [3.1.20] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+## [3.1.19] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
+
@@ -459,0 +508,13 @@
+
+## [2.2.22] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+## [2.2.21] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
lib/rack/directory.rb
--- /tmp/d20260402-542-l9gziq/rack-3.2.3/lib/rack/directory.rb 2026-04-02 18:50:32.132164223 +0000
+++ /tmp/d20260402-542-l9gziq/rack-3.2.6/lib/rack/directory.rb 2026-04-02 18:50:32.139164167 +0000
@@ -20 +20 @@
- DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
+ DIR_FILE = "<tr><td class='name'><a href='./%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
@@ -54 +54 @@
- show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
+ show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
@@ -84,0 +85 @@
+ @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
@@ -121 +122,3 @@
- return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
+
+ expanded_path = ::File.expand_path(::File.join(@root, path_info))
+ return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)
lib/rack/files.rb
--- /tmp/d20260402-542-l9gziq/rack-3.2.3/lib/rack/files.rb 2026-04-02 18:50:32.132164223 +0000
+++ /tmp/d20260402-542-l9gziq/rack-3.2.6/lib/rack/files.rb 2026-04-02 18:50:32.139164167 +0000
@@ -197 +197 @@
- CONTENT_LENGTH => body.size.to_s,
+ CONTENT_LENGTH => body.bytesize.to_s,
lib/rack/mock_response.rb
--- /tmp/d20260402-542-l9gziq/rack-3.2.3/lib/rack/mock_response.rb 2026-04-02 18:50:32.134164208 +0000
+++ /tmp/d20260402-542-l9gziq/rack-3.2.6/lib/rack/mock_response.rb 2026-04-02 18:50:32.141164152 +0000
@@ -2,0 +3 @@
+require 'stringio'
@@ -85,2 +86,10 @@
- @body.each do |chunk|
- buffer << chunk
+ begin
+ if @body.respond_to?(:each)
+ @body.each do |chunk|
+ buffer << chunk
+ end
+ else
+ @body.call(StringIO.new(buffer))
+ end
+ ensure
+ @body.close if @body.respond_to?(:close)
lib/rack/multipart/parser.rb
--- /tmp/d20260402-542-l9gziq/rack-3.2.3/lib/rack/multipart/parser.rb 2026-04-02 18:50:32.134164208 +0000
+++ /tmp/d20260402-542-l9gziq/rack-3.2.6/lib/rack/multipart/parser.rb 2026-04-02 18:50:32.141164152 +0000
@@ -36 +36 @@
- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
+ MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
@@ -82,0 +83,7 @@
+ bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
+ PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
+ private_constant :PARSER_BYTESIZE_LIMIT
+
+ CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
+ private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
+
@@ -119 +126,9 @@
- data[1]
+
+ unless data[1].empty?
+ raise Error, "whitespace between boundary parameter name and equal sign"
+ end
+ if data.post_match.match?(/boundary\s*=/i)
+ raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
+ end
+
+ data[2]
@@ -127,0 +143,4 @@
+ if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
+ raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
+ end
+
@@ -243,0 +263,2 @@
+ @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
+ @content_disposition_quoted_escapes = 0
@@ -254,0 +276 @@
+ @total_bytes_read &&= nil if io.is_a?(BoundedIO)
@@ -292,0 +315,6 @@
+ if @total_bytes_read
+ @total_bytes_read += content.bytesize
+ if @total_bytes_read > PARSER_BYTESIZE_LIMIT
+ raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
+ end
+ end
@@ -341,0 +370,3 @@
+ OBS_UNFOLD = /\r\n([ \t])/
+ private_constant :OBS_UNFOLD
+
@@ -345,0 +377,2 @@
+ content_type.gsub!(OBS_UNFOLD, '\1') if content_type
+
@@ -348,0 +382,3 @@
+ # Implement OBS unfolding (RFC 5322 Section 2.2.3)
+ disposition.gsub!(OBS_UNFOLD, '\1')
+
@@ -385,0 +422,5 @@
+ @content_disposition_quoted_escapes += 1
+ if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
+ raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
+ end
+
@@ -454 +495 @@
- raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
+ raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
lib/rack/request.rb
--- /tmp/d20260402-542-l9gziq/rack-3.2.3/lib/rack/request.rb 2026-04-02 18:50:32.135164200 +0000
+++ /tmp/d20260402-542-l9gziq/rack-3.2.6/lib/rack/request.rb 2026-04-02 18:50:32.142164144 +0000
@@ -726,2 +726,2 @@
- # Match any other printable string (except square brackets) as a hostname
- (?<address>[[[:graph:]&&[^\[\]]]]*?)
+ # Match characters allowed by RFC 3986 Section 3.2.2
+ (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
lib/rack/sendfile.rb
--- /tmp/d20260402-542-l9gziq/rack-3.2.3/lib/rack/sendfile.rb 2026-04-02 18:50:32.135164200 +0000
+++ /tmp/d20260402-542-l9gziq/rack-3.2.6/lib/rack/sendfile.rb 2026-04-02 18:50:32.142164144 +0000
@@ -54 +54 @@
- # that it maps to. The middleware performs a simple substitution on the
+ # that it maps to. The middleware performs a case-insensitive substitution on the
@@ -189 +189 @@
- new_path = path.sub(/\A#{internal}/i, external)
+ new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
lib/rack/static.rb
--- /tmp/d20260402-542-l9gziq/rack-3.2.3/lib/rack/static.rb 2026-04-02 18:50:32.136164192 +0000
+++ /tmp/d20260402-542-l9gziq/rack-3.2.6/lib/rack/static.rb 2026-04-02 18:50:32.143164136 +0000
@@ -95,0 +96,3 @@
+ if @urls.kind_of?(Array)
+ @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
+ end
@@ -118 +121 @@
- @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
+ @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
@@ -167,0 +171,2 @@
+ path = ::Rack::Utils.unescape_path(path)
+
@@ -175 +179,0 @@
- path = ::Rack::Utils.unescape(path)
@@ -178 +182 @@
- /\.(#{rule.join('|')})\z/.match?(path)
+ /\.#{Regexp.union(rule)}\z/.match?(path)
lib/rack/utils.rb
--- /tmp/d20260402-542-l9gziq/rack-3.2.3/lib/rack/utils.rb 2026-04-02 18:50:32.136164192 +0000
+++ /tmp/d20260402-542-l9gziq/rack-3.2.6/lib/rack/utils.rb 2026-04-02 18:50:32.143164136 +0000
@@ -149,3 +149,2 @@
- def forwarded_values(forwarded_header)
- return nil unless forwarded_header
- forwarded_header = forwarded_header.to_s.gsub("\n", ";")
+ ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze
+ private_constant :ALLOWED_FORWARED_PARAMS
@@ -153,5 +152,59 @@
- forwarded_header.split(';').each_with_object({}) do |field, values|
- field.split(',').each do |pair|
- pair = pair.split('=').map(&:strip).join('=')
- return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
- (values[$1.downcase.to_sym] ||= []) << $2
+ def forwarded_values(forwarded_header)
+ return unless forwarded_header
+ header = forwarded_header.to_s.tr("\n", ";")
+ header.sub!(/\A[\s;,]+/, '')
+ num_params = num_escapes = 0
+ max_params = max_escapes = 1024
+ params = {}
+
+ # Parse parameter list
+ while i = header.index('=')
+ # Only parse up to max parameters, to avoid potential denial of service
+ num_params += 1
+ return if num_params > max_params
+
+ # Found end of parameter name, ensure forward progress in loop
+ param = header.slice!(0, i+1)
+
+ # Remove ending equals and preceding whitespace from parameter name
+ param.chomp!('=')
+ param.strip!
+ param.downcase!
+ return unless param = ALLOWED_FORWARED_PARAMS[param]
+
+ if header[0] == '"'
+ # Parameter value is quoted, parse it, handling backslash escapes
+ header.slice!(0, 1)
+ value = String.new
+
+ while i = header.index(/(["\\])/)
+ c = $1
+
+ # Append all content until ending quote or escape
+ value << header.slice!(0, i)
+
+ # Remove either backslash or ending quote,
+ # ensures forward progress in loop
+ header.slice!(0, 1)
+
+ # stop parsing parameter value if found ending quote
+ break if c == '"'
+
+ # Only allow up to max escapes, to avoid potential denial of service
+ num_escapes += 1
+ return if num_escapes > max_escapes
+ escaped_char = header.slice!(0, 1)
+ value << escaped_char
+ end
+ else
+ if i = header.index(/[;,]/)
+ # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
+ value = header.slice!(0, i)
+ value.sub!(/[\s;,]+\z/, '')
+ else
+ # If no ending semicolon, assume remainder of line is value and stop parsing
+ header.strip!
+ value = header
+ header = ''
+ end
+ value.lstrip!
@@ -158,0 +212,5 @@
+
+ (params[param] ||= []) << value
+
+ # skip trailing semicolons/commas/whitespace, to proceed to next parameter
+ header.sub!(/\A[\s;,]+/, '') unless header.empty?
@@ -159,0 +218,2 @@
+
+ params
@@ -195,0 +256,18 @@
+ # Given an array of available encoding strings, and an array of
+ # acceptable encodings for a request, where each element of the
+ # acceptable encodings array is an array where the first element
+ # is an encoding name and the second element is the numeric
+ # priority for the encoding, return the available encoding with
+ # the highest priority.
+ #
+ # The accept_encoding argument is typically generated by calling
+ # Request#accept_encoding.
+ #
+ # Example:
+ #
+ # select_best_encoding(%w(compress gzip identity),
+ # [["compress", 0.5], ["gzip", 1.0]])
+ # # => "gzip"
+ #
+ # To reduce denial of service potential, only the first 16
+ # acceptable encodings are considered.
@@ -198,0 +277,2 @@
+ # Only process the first 16 encodings
+ accept_encoding = accept_encoding[0...16]
@@ -199,0 +280 @@
+ wildcard_seen = false
@@ -205,2 +286,5 @@
- (available_encodings - accept_encoding.map(&:first)).each do |m2|
- expanded_accept_encoding << [m2, q, preference]
+ unless wildcard_seen
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
+ expanded_accept_encoding << [m2, q, preference]
+ end
+ wildcard_seen = true
@@ -214 +298,7 @@
- .sort_by { |_, q, p| [-q, p] }
+ .sort do |(_, q1, p1), (_, q2, p2)|
+ if r = (q1 <=> q2).nonzero?
+ -r
+ else
+ (p1 <=> p2).nonzero? || 0
+ end
+ end
@@ -402,2 +492,2 @@
- def byte_ranges(env, size)
- get_byte_ranges env['HTTP_RANGE'], size
+ def byte_ranges(env, size, max_ranges: 100)
+ get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
@@ -406 +496 @@
- def get_byte_ranges(http_range, size)
+ def get_byte_ranges(http_range, size, max_ranges: 100)
@@ -410,0 +501,2 @@
+ byte_range = $1
+ return nil if byte_range.count(',') >= max_ranges
@@ -412 +504 @@
- $1.split(/,[ \t]*/).each do |range_spec|
+ byte_range.split(/,[ \t]*/).each do |range_spec|
lib/rack/version.rb
--- /tmp/d20260402-542-l9gziq/rack-3.2.3/lib/rack/version.rb 2026-04-02 18:50:32.136164192 +0000
+++ /tmp/d20260402-542-l9gziq/rack-3.2.6/lib/rack/version.rb 2026-04-02 18:50:32.143164136 +0000
@@ -9 +9 @@
- VERSION = "3.2.3"
+ VERSION = "3.2.6" |
Contributor
gem compare --diff rack 3.2.3 3.2.6Compared versions: ["3.2.3", "3.2.6"]
DIFFERENT files:
3.2.3->3.2.6:
* Changed:
CHANGELOG.md
--- /tmp/d20260402-561-isx0ea/rack-3.2.3/CHANGELOG.md 2026-04-02 18:51:11.242264684 +0000
+++ /tmp/d20260402-561-isx0ea/rack-3.2.6/CHANGELOG.md 2026-04-02 18:51:11.255264826 +0000
@@ -4,0 +5,35 @@
+## [3.2.6] - 2026-04-01
+
+### Security
+
+- [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) Root directory disclosure via unescaped regex interpolation in `Rack::Directory`.
+- [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.
+- [CVE-2026-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) Forwarded header semicolon injection enables Host and Scheme spoofing.
+- [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
+- [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
+- [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
+- [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
+- [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
+- [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`.
+- [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
+- [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
+- [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.
+- [CVE-2026-26962](https://github.com/advisories/GHSA-rx22-g9mx-qrhv) Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
+
+## [3.2.5] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+### Fixed
+
+- Fix `Rack::MockResponse#body` when the body is a Proc. ([#2420](https://github.com/rack/rack/pull/2420), [#2423](https://github.com/rack/rack/pull/2423), [@tavianator](https://github.com/tavianator), [@ioquatix])
+
+## [3.2.4] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
+
@@ -76,0 +112,13 @@
+## [3.1.20] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+## [3.1.19] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
+
@@ -459,0 +508,13 @@
+
+## [2.2.22] - 2026-02-16
+
+### Security
+
+- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
+- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
+
+## [2.2.21] - 2025-11-03
+
+### Fixed
+
+- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
lib/rack/directory.rb
--- /tmp/d20260402-561-isx0ea/rack-3.2.3/lib/rack/directory.rb 2026-04-02 18:51:11.246264728 +0000
+++ /tmp/d20260402-561-isx0ea/rack-3.2.6/lib/rack/directory.rb 2026-04-02 18:51:11.258264859 +0000
@@ -20 +20 @@
- DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
+ DIR_FILE = "<tr><td class='name'><a href='./%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
@@ -54 +54 @@
- show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
+ show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
@@ -84,0 +85 @@
+ @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
@@ -121 +122,3 @@
- return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
+
+ expanded_path = ::File.expand_path(::File.join(@root, path_info))
+ return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)
lib/rack/files.rb
--- /tmp/d20260402-561-isx0ea/rack-3.2.3/lib/rack/files.rb 2026-04-02 18:51:11.247264739 +0000
+++ /tmp/d20260402-561-isx0ea/rack-3.2.6/lib/rack/files.rb 2026-04-02 18:51:11.258264859 +0000
@@ -197 +197 @@
- CONTENT_LENGTH => body.size.to_s,
+ CONTENT_LENGTH => body.bytesize.to_s,
lib/rack/mock_response.rb
--- /tmp/d20260402-561-isx0ea/rack-3.2.3/lib/rack/mock_response.rb 2026-04-02 18:51:11.249264761 +0000
+++ /tmp/d20260402-561-isx0ea/rack-3.2.6/lib/rack/mock_response.rb 2026-04-02 18:51:11.260264881 +0000
@@ -2,0 +3 @@
+require 'stringio'
@@ -85,2 +86,10 @@
- @body.each do |chunk|
- buffer << chunk
+ begin
+ if @body.respond_to?(:each)
+ @body.each do |chunk|
+ buffer << chunk
+ end
+ else
+ @body.call(StringIO.new(buffer))
+ end
+ ensure
+ @body.close if @body.respond_to?(:close)
lib/rack/multipart/parser.rb
--- /tmp/d20260402-561-isx0ea/rack-3.2.3/lib/rack/multipart/parser.rb 2026-04-02 18:51:11.251264783 +0000
+++ /tmp/d20260402-561-isx0ea/rack-3.2.6/lib/rack/multipart/parser.rb 2026-04-02 18:51:11.261264892 +0000
@@ -36 +36 @@
- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
+ MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
@@ -82,0 +83,7 @@
+ bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
+ PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
+ private_constant :PARSER_BYTESIZE_LIMIT
+
+ CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
+ private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
+
@@ -119 +126,9 @@
- data[1]
+
+ unless data[1].empty?
+ raise Error, "whitespace between boundary parameter name and equal sign"
+ end
+ if data.post_match.match?(/boundary\s*=/i)
+ raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
+ end
+
+ data[2]
@@ -127,0 +143,4 @@
+ if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
+ raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
+ end
+
@@ -243,0 +263,2 @@
+ @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
+ @content_disposition_quoted_escapes = 0
@@ -254,0 +276 @@
+ @total_bytes_read &&= nil if io.is_a?(BoundedIO)
@@ -292,0 +315,6 @@
+ if @total_bytes_read
+ @total_bytes_read += content.bytesize
+ if @total_bytes_read > PARSER_BYTESIZE_LIMIT
+ raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
+ end
+ end
@@ -341,0 +370,3 @@
+ OBS_UNFOLD = /\r\n([ \t])/
+ private_constant :OBS_UNFOLD
+
@@ -345,0 +377,2 @@
+ content_type.gsub!(OBS_UNFOLD, '\1') if content_type
+
@@ -348,0 +382,3 @@
+ # Implement OBS unfolding (RFC 5322 Section 2.2.3)
+ disposition.gsub!(OBS_UNFOLD, '\1')
+
@@ -385,0 +422,5 @@
+ @content_disposition_quoted_escapes += 1
+ if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
+ raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
+ end
+
@@ -454 +495 @@
- raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
+ raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
lib/rack/request.rb
--- /tmp/d20260402-561-isx0ea/rack-3.2.3/lib/rack/request.rb 2026-04-02 18:51:11.251264783 +0000
+++ /tmp/d20260402-561-isx0ea/rack-3.2.6/lib/rack/request.rb 2026-04-02 18:51:11.262264903 +0000
@@ -726,2 +726,2 @@
- # Match any other printable string (except square brackets) as a hostname
- (?<address>[[[:graph:]&&[^\[\]]]]*?)
+ # Match characters allowed by RFC 3986 Section 3.2.2
+ (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
lib/rack/sendfile.rb
--- /tmp/d20260402-561-isx0ea/rack-3.2.3/lib/rack/sendfile.rb 2026-04-02 18:51:11.253264805 +0000
+++ /tmp/d20260402-561-isx0ea/rack-3.2.6/lib/rack/sendfile.rb 2026-04-02 18:51:11.262264903 +0000
@@ -54 +54 @@
- # that it maps to. The middleware performs a simple substitution on the
+ # that it maps to. The middleware performs a case-insensitive substitution on the
@@ -189 +189 @@
- new_path = path.sub(/\A#{internal}/i, external)
+ new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
lib/rack/static.rb
--- /tmp/d20260402-561-isx0ea/rack-3.2.3/lib/rack/static.rb 2026-04-02 18:51:11.253264805 +0000
+++ /tmp/d20260402-561-isx0ea/rack-3.2.6/lib/rack/static.rb 2026-04-02 18:51:11.263264914 +0000
@@ -95,0 +96,3 @@
+ if @urls.kind_of?(Array)
+ @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
+ end
@@ -118 +121 @@
- @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
+ @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
@@ -167,0 +171,2 @@
+ path = ::Rack::Utils.unescape_path(path)
+
@@ -175 +179,0 @@
- path = ::Rack::Utils.unescape(path)
@@ -178 +182 @@
- /\.(#{rule.join('|')})\z/.match?(path)
+ /\.#{Regexp.union(rule)}\z/.match?(path)
lib/rack/utils.rb
--- /tmp/d20260402-561-isx0ea/rack-3.2.3/lib/rack/utils.rb 2026-04-02 18:51:11.254264815 +0000
+++ /tmp/d20260402-561-isx0ea/rack-3.2.6/lib/rack/utils.rb 2026-04-02 18:51:11.263264914 +0000
@@ -149,3 +149,2 @@
- def forwarded_values(forwarded_header)
- return nil unless forwarded_header
- forwarded_header = forwarded_header.to_s.gsub("\n", ";")
+ ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze
+ private_constant :ALLOWED_FORWARED_PARAMS
@@ -153,5 +152,59 @@
- forwarded_header.split(';').each_with_object({}) do |field, values|
- field.split(',').each do |pair|
- pair = pair.split('=').map(&:strip).join('=')
- return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
- (values[$1.downcase.to_sym] ||= []) << $2
+ def forwarded_values(forwarded_header)
+ return unless forwarded_header
+ header = forwarded_header.to_s.tr("\n", ";")
+ header.sub!(/\A[\s;,]+/, '')
+ num_params = num_escapes = 0
+ max_params = max_escapes = 1024
+ params = {}
+
+ # Parse parameter list
+ while i = header.index('=')
+ # Only parse up to max parameters, to avoid potential denial of service
+ num_params += 1
+ return if num_params > max_params
+
+ # Found end of parameter name, ensure forward progress in loop
+ param = header.slice!(0, i+1)
+
+ # Remove ending equals and preceding whitespace from parameter name
+ param.chomp!('=')
+ param.strip!
+ param.downcase!
+ return unless param = ALLOWED_FORWARED_PARAMS[param]
+
+ if header[0] == '"'
+ # Parameter value is quoted, parse it, handling backslash escapes
+ header.slice!(0, 1)
+ value = String.new
+
+ while i = header.index(/(["\\])/)
+ c = $1
+
+ # Append all content until ending quote or escape
+ value << header.slice!(0, i)
+
+ # Remove either backslash or ending quote,
+ # ensures forward progress in loop
+ header.slice!(0, 1)
+
+ # stop parsing parameter value if found ending quote
+ break if c == '"'
+
+ # Only allow up to max escapes, to avoid potential denial of service
+ num_escapes += 1
+ return if num_escapes > max_escapes
+ escaped_char = header.slice!(0, 1)
+ value << escaped_char
+ end
+ else
+ if i = header.index(/[;,]/)
+ # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
+ value = header.slice!(0, i)
+ value.sub!(/[\s;,]+\z/, '')
+ else
+ # If no ending semicolon, assume remainder of line is value and stop parsing
+ header.strip!
+ value = header
+ header = ''
+ end
+ value.lstrip!
@@ -158,0 +212,5 @@
+
+ (params[param] ||= []) << value
+
+ # skip trailing semicolons/commas/whitespace, to proceed to next parameter
+ header.sub!(/\A[\s;,]+/, '') unless header.empty?
@@ -159,0 +218,2 @@
+
+ params
@@ -195,0 +256,18 @@
+ # Given an array of available encoding strings, and an array of
+ # acceptable encodings for a request, where each element of the
+ # acceptable encodings array is an array where the first element
+ # is an encoding name and the second element is the numeric
+ # priority for the encoding, return the available encoding with
+ # the highest priority.
+ #
+ # The accept_encoding argument is typically generated by calling
+ # Request#accept_encoding.
+ #
+ # Example:
+ #
+ # select_best_encoding(%w(compress gzip identity),
+ # [["compress", 0.5], ["gzip", 1.0]])
+ # # => "gzip"
+ #
+ # To reduce denial of service potential, only the first 16
+ # acceptable encodings are considered.
@@ -198,0 +277,2 @@
+ # Only process the first 16 encodings
+ accept_encoding = accept_encoding[0...16]
@@ -199,0 +280 @@
+ wildcard_seen = false
@@ -205,2 +286,5 @@
- (available_encodings - accept_encoding.map(&:first)).each do |m2|
- expanded_accept_encoding << [m2, q, preference]
+ unless wildcard_seen
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
+ expanded_accept_encoding << [m2, q, preference]
+ end
+ wildcard_seen = true
@@ -214 +298,7 @@
- .sort_by { |_, q, p| [-q, p] }
+ .sort do |(_, q1, p1), (_, q2, p2)|
+ if r = (q1 <=> q2).nonzero?
+ -r
+ else
+ (p1 <=> p2).nonzero? || 0
+ end
+ end
@@ -402,2 +492,2 @@
- def byte_ranges(env, size)
- get_byte_ranges env['HTTP_RANGE'], size
+ def byte_ranges(env, size, max_ranges: 100)
+ get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
@@ -406 +496 @@
- def get_byte_ranges(http_range, size)
+ def get_byte_ranges(http_range, size, max_ranges: 100)
@@ -410,0 +501,2 @@
+ byte_range = $1
+ return nil if byte_range.count(',') >= max_ranges
@@ -412 +504 @@
- $1.split(/,[ \t]*/).each do |range_spec|
+ byte_range.split(/,[ \t]*/).each do |range_spec|
lib/rack/version.rb
--- /tmp/d20260402-561-isx0ea/rack-3.2.3/lib/rack/version.rb 2026-04-02 18:51:11.254264815 +0000
+++ /tmp/d20260402-561-isx0ea/rack-3.2.6/lib/rack/version.rb 2026-04-02 18:51:11.263264914 +0000
@@ -9 +9 @@
- VERSION = "3.2.3"
+ VERSION = "3.2.6" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bumps rack from 3.2.3 to 3.2.6.
Release notes
Sourced from rack's releases.
Changelog
Sourced from rack's changelog.
Commits
e1f22fdBump patch version.31989fdFix typo in test.d268165Fix test expectation.8f425deAdd Ruby v4.0 to the test matrix.bf83042Drop EOL Rubies from external tests.d50c4d3Implement OBS unfolding for multipart requests per RFC 5322 2.2.3bfb6914Limit the number of quoted escapes during multipart parsingb3e5945Add Content-Length size check in Rack::Multipart::Parser7a8f326Fix root prefix bug in Rack::Statica57bc14Only do a simple substitution on the x-accel-mapping pathsDependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor versionwill close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)You can disable automated security fix PRs for this repo from the Security Alerts page.