Version in base suite: 2.2.20-0+deb12u1 Base version: ruby-rack_2.2.20-0+deb12u1 Target version: ruby-rack_2.2.22-0+deb12u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/r/ruby-rack/ruby-rack_2.2.20-0+deb12u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/r/ruby-rack/ruby-rack_2.2.22-0+deb12u1.dsc CHANGELOG.md | 13 +++++++++++++ debian/changelog | 10 ++++++++++ lib/rack/directory.rb | 7 +++++-- lib/rack/multipart/parser.rb | 2 +- lib/rack/version.rb | 2 +- test/spec_directory.rb | 36 ++++++++++++++++++++++++++++++++++++ test/spec_multipart.rb | 42 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 108 insertions(+), 4 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpueay89w7/ruby-rack_2.2.20-0+deb12u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpueay89w7/ruby-rack_2.2.22-0+deb12u1.dsc: no acceptable signature found diff -Nru ruby-rack-2.2.20/CHANGELOG.md ruby-rack-2.2.22/CHANGELOG.md --- ruby-rack-2.2.20/CHANGELOG.md 2025-10-10 00:36:11.000000000 +0000 +++ ruby-rack-2.2.22/CHANGELOG.md 2026-02-16 03:36:10.000000000 +0000 @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/). +## Unreleased + +### 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)) + ## [2.2.20] - 2025-10-10 ### Security diff -Nru ruby-rack-2.2.20/debian/changelog ruby-rack-2.2.22/debian/changelog --- ruby-rack-2.2.20/debian/changelog 2025-10-23 08:54:27.000000000 +0000 +++ ruby-rack-2.2.22/debian/changelog 2026-03-05 12:04:17.000000000 +0000 @@ -1,3 +1,13 @@ +ruby-rack (2.2.22-0+deb12u1) bookworm-security; urgency=high + + * New upstream version 2.2.22. + - CVE-2026-25500: XSS injection via malicious filename + in `Rack::Directory`. (Closes: #1128480) + - CVE-2026-22860: Directory traversal via root prefix + bypass in `Rack::Directory`. (Closes: #1128479) + + -- Utkarsh Gupta Thu, 05 Mar 2026 17:34:17 +0530 + ruby-rack (2.2.20-0+deb12u1) bookworm-security; urgency=medium * New upstream version 2.2.20. diff -Nru ruby-rack-2.2.20/lib/rack/directory.rb ruby-rack-2.2.22/lib/rack/directory.rb --- ruby-rack-2.2.20/lib/rack/directory.rb 2025-10-10 00:36:11.000000000 +0000 +++ ruby-rack-2.2.22/lib/rack/directory.rb 2026-02-16 03:36:10.000000000 +0000 @@ -11,7 +11,7 @@ # If +app+ is not specified, a Rack::Files of the same +root+ will be used. class Directory - DIR_FILE = "%s%s%s%s\n" + DIR_FILE = "%s%s%s%s\n" DIR_PAGE_HEADER = <<-PAGE %s @@ -76,6 +76,7 @@ # Set the root directory and application for serving files. def initialize(root, app = nil) @root = ::File.expand_path(root) + @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}" @app = app || Files.new(@root) @head = Head.new(method(:get)) end @@ -112,7 +113,9 @@ # Rack response to use for requests with paths outside the root, or nil if path is inside the root. def check_forbidden(path_info) return unless path_info.include? ".." - 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) body = "Forbidden\n" [403, { CONTENT_TYPE => "text/plain", diff -Nru ruby-rack-2.2.20/lib/rack/multipart/parser.rb ruby-rack-2.2.22/lib/rack/multipart/parser.rb --- ruby-rack-2.2.20/lib/rack/multipart/parser.rb 2025-10-10 00:36:11.000000000 +0000 +++ ruby-rack-2.2.22/lib/rack/multipart/parser.rb 2026-02-16 03:36:10.000000000 +0000 @@ -314,7 +314,7 @@ else # We raise if the mime part header is too large, to avoid unbounded memory # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default) - raise EOFError, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT + raise EOFError, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT return :want_read end diff -Nru ruby-rack-2.2.20/lib/rack/version.rb ruby-rack-2.2.22/lib/rack/version.rb --- ruby-rack-2.2.20/lib/rack/version.rb 2025-10-10 00:36:11.000000000 +0000 +++ ruby-rack-2.2.22/lib/rack/version.rb 2026-02-16 03:36:10.000000000 +0000 @@ -20,7 +20,7 @@ VERSION.join(".") end - RELEASE = "2.2.20" + RELEASE = "2.2.22" # Return the Rack release as a dotted string. def self.release diff -Nru ruby-rack-2.2.20/test/spec_directory.rb ruby-rack-2.2.22/test/spec_directory.rb --- ruby-rack-2.2.20/test/spec_directory.rb 2025-10-10 00:36:11.000000000 +0000 +++ ruby-rack-2.2.22/test/spec_directory.rb 2026-02-16 03:36:10.000000000 +0000 @@ -32,6 +32,15 @@ end end + it "serve root directory index" do + res = Rack::MockRequest.new(Rack::Lint.new(app)). + get("/") + + res.must_be :ok? + assert_includes(res.body, '') + assert_includes(res.body, "href='./cgi") + end + it "serve directory indices" do res = Rack::MockRequest.new(Rack::Lint.new(app)). get("/cgi/") @@ -119,6 +128,33 @@ res.must_be :forbidden? end + it "not allow directory traversal via root prefix bypass" do + Dir.mktmpdir do |dir| + root = File.join(dir, "root") + outside = "#{root}_test" + FileUtils.mkdir_p(root) + FileUtils.mkdir_p(outside) + FileUtils.touch(File.join(outside, "test.txt")) + + app = Rack::Directory.new(root) + res = Rack::MockRequest.new(app).get("/../#{File.basename(outside)}/") + + res.must_be :forbidden? + end + end + + it "not allow dir globs" do + Dir.mktmpdir do |dir| + weirds = "uploads/.?/.?" + full_dir = File.join(dir, weirds) + FileUtils.mkdir_p full_dir + FileUtils.touch File.join(dir, "secret.txt") + app = Rack::Directory.new(File.join(dir, "uploads")) + res = Rack::MockRequest.new(app).get("/.%3F") + refute_match "secret.txt", res.body + end + end + it "404 if it can't find the file" do res = Rack::MockRequest.new(Rack::Lint.new(app)). get("/cgi/blubb") diff -Nru ruby-rack-2.2.20/test/spec_multipart.rb ruby-rack-2.2.22/test/spec_multipart.rb --- ruby-rack-2.2.20/test/spec_multipart.rb 2025-10-10 00:36:11.000000000 +0000 +++ ruby-rack-2.2.22/test/spec_multipart.rb 2026-02-16 03:36:10.000000000 +0000 @@ -242,6 +242,48 @@ wr.close end + it "parses when the MIME head terminator straddles the BUFSIZE boundary" do + boundary = '------WebKitFormBoundaryysVLFAjttLkewYBx' + + data = StringIO.new + data.write("--#{boundary}") + data.write("\r\n") + + data.write('content-disposition: form-data; name="a"') + data.write("\r\n") + data.write("\r\n") + # Fill to the end of the first 1MB chunk so the header's `\r\n` is in the next chunk. + data.write("0" * (1024 * 1024 - 174)) + data.write("\r\n") + data.write("--#{boundary}") + data.write("\r\n") + data.write('content-disposition: form-data; name="b"') + # First 1MB chunk separator is here + data.write("\r\n") + data.write("\r\n") + data.write("0" * (1024 * 1024 - 88)) + data.write("\r\n") + data.write("--#{boundary}") + data.write("\r\n") + data.write('content-disposition: form-data; name="c"') + # Second 1MB chunk separator is here + data.write("\r\n") + data.write("\r\n") + data.write("hello") + data.write("\r\n") + data.write("--#{boundary}--\r\n") + data.rewind + + fixture = { + "CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}", + "CONTENT_LENGTH" => data.length.to_s, + :input => data, + } + + env = Rack::MockRequest.env_for '/', fixture + Rack::Multipart.parse_multipart(env).keys.must_equal(["a", "b", "c"]) + end + it "rejects excessive buffered mime data size in a single parameter" do rd, wr = IO.pipe def rd.rewind; end