Skip to content

Bump rack from 3.2.3 to 3.2.6#906

Open
dependabot[bot] wants to merge 1 commit intomainfrom
dependabot/bundler/rack-3.2.6
Open

Bump rack from 3.2.3 to 3.2.6#906
dependabot[bot] wants to merge 1 commit intomainfrom
dependabot/bundler/rack-3.2.6

Conversation

@dependabot
Copy link
Copy Markdown
Contributor

@dependabot dependabot bot commented on behalf of github Apr 2, 2026

Bumps rack from 3.2.3 to 3.2.6.

Release notes

Sourced from rack's releases.

v3.2.6

Full Changelog: rack/rack@v3.2.5...v3.2.6

v3.2.4

No release notes provided.

Changelog

Sourced from rack's changelog.

[3.2.6] - 2026-04-01

Security

  • CVE-2026-34763 Root directory disclosure via unescaped regex interpolation in Rack::Directory.
  • CVE-2026-34230 Avoid O(n^2) algorithm in Rack::Utils.select_best_encoding which could lead to denial of service.
  • CVE-2026-32762 Forwarded header semicolon injection enables Host and Scheme spoofing.
  • CVE-2026-26961 Raise error for multipart requests with multiple boundary parameters.
  • CVE-2026-34786 Rack::Static header_rules bypass via URL-encoded path mismatch.
  • CVE-2026-34831 Content-Length mismatch in Rack::Files error responses.
  • CVE-2026-34826 Multipart byte range processing allows denial of service via excessive overlapping ranges.
  • CVE-2026-34835 Rack::Request accepts invalid Host characters, enabling host allowlist bypass.
  • CVE-2026-34830 Rack::Sendfile header-based X-Accel-Mapping regex injection enables unauthorized X-Accel-Redirect.
  • CVE-2026-34785 Rack::Static prefix matching can expose unintended files under the static root.
  • CVE-2026-34829 Multipart parsing without Content-Length header allows unbounded chunked file uploads.
  • CVE-2026-34827 Multipart header parsing allows denial of service via escape-heavy quoted parameters.
  • CVE-2026-26962 Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.

[3.2.5] - 2026-02-16

Security

  • CVE-2026-25500 XSS injection via malicious filename in Rack::Directory.
  • CVE-2026-22860 Directory traversal via root prefix bypass in Rack::Directory.

Fixed

[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, @​alpaca-tc, @​willnet, @​krororo)
Commits
  • e1f22fd Bump patch version.
  • 31989fd Fix typo in test.
  • d268165 Fix test expectation.
  • 8f425de Add Ruby v4.0 to the test matrix.
  • bf83042 Drop EOL Rubies from external tests.
  • d50c4d3 Implement OBS unfolding for multipart requests per RFC 5322 2.2.3
  • bfb6914 Limit the number of quoted escapes during multipart parsing
  • b3e5945 Add Content-Length size check in Rack::Multipart::Parser
  • 7a8f326 Fix root prefix bug in Rack::Static
  • a57bc14 Only do a simple substitution on the x-accel-mapping paths
  • Additional commits viewable in compare view

Dependabot compatibility score

Dependabot 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 rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will 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 version will 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 dependency will 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.

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>
@dependabot dependabot bot added dependencies ruby Pull requests that update Ruby code labels Apr 2, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

4 similar comments
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

gem compare rack 3.2.3 3.2.6

Compared 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
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

gem compare rack 3.2.3 3.2.6

Compared 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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

gem compare rack 3.2.3 3.2.6

Compared 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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

gem compare rack 3.2.3 3.2.6

Compared 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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

gem compare rack 3.2.3 3.2.6

Compared 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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

gem compare --diff rack 3.2.3 3.2.6

Compared 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"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

gem compare --diff rack 3.2.3 3.2.6

Compared 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"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

gem compare --diff rack 3.2.3 3.2.6

Compared 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"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

gem compare --diff rack 3.2.3 3.2.6

Compared 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"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

gem compare --diff rack 3.2.3 3.2.6

Compared 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"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies ruby Pull requests that update Ruby code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants