Version in base suite: 3.1.20-0+deb13u1 Base version: ruby-rack_3.1.20-0+deb13u1 Target version: ruby-rack_3.1.20-0+deb13u2 Base file: /srv/ftp-master.debian.org/ftp/pool/main/r/ruby-rack/ruby-rack_3.1.20-0+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/r/ruby-rack/ruby-rack_3.1.20-0+deb13u2.dsc changelog | 33 +++++++++ patches/CVE-2026-26961.patch | 83 +++++++++++++++++++++++ patches/CVE-2026-26962.patch | 81 ++++++++++++++++++++++ patches/CVE-2026-32762.patch | 153 +++++++++++++++++++++++++++++++++++++++++++ patches/CVE-2026-34230.patch | 109 ++++++++++++++++++++++++++++++ patches/CVE-2026-34763.patch | 55 +++++++++++++++ patches/CVE-2026-34785.patch | 56 +++++++++++++++ patches/CVE-2026-34786.patch | 75 +++++++++++++++++++++ patches/CVE-2026-34826.patch | 69 +++++++++++++++++++ patches/CVE-2026-34827.patch | 98 +++++++++++++++++++++++++++ patches/CVE-2026-34829.patch | 141 +++++++++++++++++++++++++++++++++++++++ patches/CVE-2026-34830.patch | 59 ++++++++++++++++ patches/CVE-2026-34831.patch | 75 +++++++++++++++++++++ patches/CVE-2026-34835.patch | 108 ++++++++++++++++++++++++++++++ patches/series | 13 +++ 15 files changed, 1208 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp4ppq5rnm/ruby-rack_3.1.20-0+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp4ppq5rnm/ruby-rack_3.1.20-0+deb13u2.dsc: no acceptable signature found diff -Nru ruby-rack-3.1.20/debian/changelog ruby-rack-3.1.20/debian/changelog --- ruby-rack-3.1.20/debian/changelog 2026-03-10 04:14:22.000000000 +0000 +++ ruby-rack-3.1.20/debian/changelog 2026-09-01 04:35:17.000000000 +0000 @@ -1,3 +1,36 @@ +ruby-rack (3.1.20-0+deb13u2) trixie-security; urgency=high + + * Team upload + * CVE-2026-26961: Greedy multipart boundary parsing can cause parser + differentials and WAF bypass. + * CVE-2026-26962: Improper unfolding of folded multipart headers + preserves CRLF in parsed parameter values. + * CVE-2026-32762: `Forwarded` header semicolon injection enables + `Host` and `Scheme` spoofing + * CVE-2026-34230: Quadratic complexity in + `Rack::Utils.select_best_encoding` via wildcard `Accept-Encoding` + header. + * CVE-2026-34763: Root directory disclosure via unescaped regex + interpolation + * CVE-2026-34785: `Rack::Static` prefix matching can expose + unintended files under the static root. + * CVE-2026-34786: `Rack::Static` `header_rules` bypass via + URL-encoded path mismatch. + * CVE-2026-34826: Multipart byte range processing allows denial of + service via excessive overlapping ranges + * CVE-2026-34827: Multipart header parsing allows denial of service + via escape-heavy quoted parameters + * CVE-2026-34829: Multipart parsing without `Content-Length` header + allows unbounded chunked file uploads + * CVE-2026-34830: `Rack::Sendfile` header-based `X-Accel-Mapping` + regex injection enables unauthorized `X-Accel-Redirect`. + * CVE-2026-34831: `Content-Length` mismatch in `Rack::Files` + error responses. + * CVE-2026-34835: `Rack::Request` accepts invalid Host characters, + enabling host allowlist bypass. + + -- Abhijith PA Tue, 01 Sep 2026 10:05:17 +0530 + ruby-rack (3.1.20-0+deb13u1) trixie-security; urgency=high * New upstream version 3.1.20. diff -Nru ruby-rack-3.1.20/debian/patches/CVE-2026-26961.patch ruby-rack-3.1.20/debian/patches/CVE-2026-26961.patch --- ruby-rack-3.1.20/debian/patches/CVE-2026-26961.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/CVE-2026-26961.patch 2026-09-01 04:35:17.000000000 +0000 @@ -0,0 +1,83 @@ +From 10626530f3c54a0cd54bee1150e851aa238249e4 Mon Sep 17 00:00:00 2001 +From: Jeremy Evans +Date: Thu, 5 Feb 2026 18:19:45 -0800 +Subject: [PATCH] Raise error for multipart requests with multiple boundary + parameters + +RFC 1341 specifies there should be a single boundary parameter. +Requests with multiple boundary parameters are unlikely to be +legitimate, and likely are attempts to exploit parsing differences +between rack and web application firewalls. + +* Disallow whitespace between boundary and = when parsing multipart boundaries + +Rack has historically not accepted these. To avoid security issues +when parsing multiple boundaries, check for boundary cases that may +have whitespace, but explicitly disallow the parsing if there is +whitespace. +--- + CHANGELOG.md | 1 + + lib/rack/multipart/parser.rb | 12 ++++++++++-- + test/spec_multipart.rb | 23 +++++++++++++++++++++++ + 3 files changed, 34 insertions(+), 2 deletions(-) + +--- a/lib/rack/multipart/parser.rb ++++ b/lib/rack/multipart/parser.rb +@@ -33,7 +33,7 @@ module Rack + EOL = "\r\n" + FWS = /[ \t]+(?:\r\n[ \t]+)?/ # whitespace with optional folding + HEADER_VALUE = "(?:[^\r\n]|\r\n[ \t])*" # anything but a non-folding CRLF +- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni ++ MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni + MULTIPART_CONTENT_TYPE = /^Content-Type:#{FWS}?(#{HEADER_VALUE})/ni + MULTIPART_CONTENT_DISPOSITION = /^Content-Disposition:#{FWS}?(#{HEADER_VALUE})/ni + MULTIPART_CONTENT_ID = /^Content-ID:#{FWS}?(#{HEADER_VALUE})/ni +@@ -104,7 +104,15 @@ module Rack + return unless content_type + data = content_type.match(MULTIPART) + return unless data +- 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] + end + + def self.parse(io, content_length, content_type, tmpfile, bufsize, qp) +--- a/test/spec_multipart.rb ++++ b/test/spec_multipart.rb +@@ -41,6 +41,29 @@ describe Rack::Multipart do + }.must_raise Rack::Multipart::BoundaryTooLongError + end + ++ it "raises an exception if there are multiple boundries" do ++ env = multipart_fixture(:content_type_and_no_filename) ++ env["CONTENT_TYPE"] += "; Boundary=FooBar42x" ++ env = Rack::MockRequest.env_for("/", env) ++ lambda { ++ Rack::Multipart.parse_multipart(env) ++ }.must_raise Rack::Multipart::BoundaryTooLongError ++ ++ env = multipart_fixture(:content_type_and_no_filename) ++ env["CONTENT_TYPE"] = "#{env["CONTENT_TYPE"].sub("boundary=", "boundary =")}; Boundary=FooBar42x" ++ env = Rack::MockRequest.env_for("/", env) ++ lambda { ++ Rack::Multipart.parse_multipart(env) ++ }.must_raise Rack::Multipart::Error ++ ++ env = multipart_fixture(:content_type_and_no_filename) ++ env["CONTENT_TYPE"] = "#{env["CONTENT_TYPE"].sub("boundary=", "boundary =")}; Boundary =FooBar42x" ++ env = Rack::MockRequest.env_for("/", env) ++ lambda { ++ Rack::Multipart.parse_multipart(env) ++ }.must_raise Rack::Multipart::Error ++ end ++ + it "raises a bad request exception if no body is given but content type indicates a multipart body" do + env = Rack::MockRequest.env_for("/", "CONTENT_TYPE" => 'multipart/form-data; boundary=BurgerBurger', :input => nil) + lambda { diff -Nru ruby-rack-3.1.20/debian/patches/CVE-2026-26962.patch ruby-rack-3.1.20/debian/patches/CVE-2026-26962.patch --- ruby-rack-3.1.20/debian/patches/CVE-2026-26962.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/CVE-2026-26962.patch 2026-09-01 04:35:17.000000000 +0000 @@ -0,0 +1,81 @@ +From ae320b46617e9131c34ad77ea15f1c3b036c43e6 Mon Sep 17 00:00:00 2001 +From: Animesh Roy +Date: Thu, 13 Aug 2026 11:56:36 +0530 +Subject: [PATCH] Backport OBS unfolding for multipart requests to 3-1-stable + (#2486) + +* Implement OBS unfolding for multipart requests per RFC 5322 2.2.3 + +Backport of d50c4d3d from main, for both the Content-Disposition and Content-Type lines. + +Without it, a folded multipart header leaves the CRLF embedded in parsed parameter values such as filename, so the value used does not match what the sender expressed. This branch was outside the affected range recorded on GHSA-rx22-g9mx-qrhv, but the parser here accepts folded headers and preserves the fold, so the correctness issue applies. + +Includes the regression test from the same upstream commit. + +Co-authored-by: "William T. Nelson" <35801+wtn@users.noreply.github.com> + +* Update changelog for multipart obs-fold fix + +--------- + +Co-authored-by: Jeremy Evans +Co-authored-by: "William T. Nelson" <35801+wtn@users.noreply.github.com> +Co-authored-by: Samuel Williams +--- + lib/rack/multipart/parser.rb | 6 ++++++ + test/spec_multipart.rb | 24 ++++++++++++++++++++++++ + 3 files changed, 36 insertions(+) + +--- a/lib/rack/multipart/parser.rb ++++ b/lib/rack/multipart/parser.rb +@@ -340,13 +340,19 @@ module Rack + + CONTENT_DISPOSITION_MAX_PARAMS = 16 + CONTENT_DISPOSITION_MAX_BYTES = 1536 ++ OBS_UNFOLD = /\r\n([ \t])/ ++ private_constant :OBS_UNFOLD + def handle_mime_head + if @sbuf.scan_until(@head_regex) + head = @sbuf[1] + content_type = head[MULTIPART_CONTENT_TYPE, 1] ++ content_type.gsub!(OBS_UNFOLD, '\\1') if content_type + if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) && + disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES + ++ # Implement OBS unfolding (RFC 5322 Section 2.2.3) ++ disposition.gsub!(OBS_UNFOLD, '\\1') ++ + # ignore actual content-disposition value (should always be form-data) + i = disposition.index(';') + disposition.slice!(0, i+1) +--- a/test/spec_multipart.rb ++++ b/test/spec_multipart.rb +@@ -1285,4 +1285,28 @@ content-type: image/png\r + f.write(params["image/png"][0]) + f.length.must_equal 26473 + end ++ ++ it "prevents CRLF injection in parameter values via obs-fold" do ++ data = <<~EOF ++ --AaB03x\r ++ Content-Disposition: form-data; name="upload"; filename="test\r ++ \t.txt"\r ++ Content-Type: application/octet-stream;\r ++ name="file.php"\r ++ \r ++ \r ++ --AaB03x--\r ++ EOF ++ ++ options = { ++ "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x", ++ "CONTENT_LENGTH" => data.length.to_s, ++ :input => StringIO.new(data) ++ } ++ env = Rack::MockRequest.env_for("/", options) ++ params = Rack::Multipart.parse_multipart(env) ++ params["upload"][:filename].must_equal "test\t.txt" ++ params["upload"][:type].must_equal 'application/octet-stream; name="file.php"' ++ end ++ + end diff -Nru ruby-rack-3.1.20/debian/patches/CVE-2026-32762.patch ruby-rack-3.1.20/debian/patches/CVE-2026-32762.patch --- ruby-rack-3.1.20/debian/patches/CVE-2026-32762.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/CVE-2026-32762.patch 2026-09-01 04:35:17.000000000 +0000 @@ -0,0 +1,153 @@ +From 9df5d34d4f496b22b8d07e919447e9dfa3240d41 Mon Sep 17 00:00:00 2001 +From: Samuel Williams +Date: Tue, 31 Mar 2026 16:45:31 +1300 +Subject: [PATCH] Parse Forwarded header instead of using regexp scan + +`;` and `,` are allowed as characters inside a quoted value of a +forwarded parameter. So you cannot safely split on those and then +try to remove quotes. + +Switch to using a parser based on the one used for parsing +multipart content-disposition. +--- + lib/rack/utils.rb | 76 +++++++++++++++++++++++++++++++++++++++++----- + test/spec_utils.rb | 28 ++++++++++++++++- + 3 files changed, 96 insertions(+), 9 deletions(-) + +diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb +index 0a6c4b4a8..1843949ee 100644 +--- a/lib/rack/utils.rb ++++ b/lib/rack/utils.rb +@@ -146,17 +146,77 @@ def q_values(q_value_header) + end + end + ++ ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze ++ private_constant :ALLOWED_FORWARED_PARAMS ++ + def forwarded_values(forwarded_header) +- return nil unless forwarded_header +- forwarded_header = forwarded_header.to_s.gsub("\n", ";") +- +- 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 ++ 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! + end ++ ++ (params[param] ||= []) << value ++ ++ # skip trailing semicolons/commas/whitespace, to proceed to next parameter ++ header.sub!(/\A[\s;,]+/, '') unless header.empty? + end ++ ++ params + end + module_function :forwarded_values + +diff --git a/test/spec_utils.rb b/test/spec_utils.rb +index 74c7b1cb7..d4461aa22 100644 +--- a/test/spec_utils.rb ++++ b/test/spec_utils.rb +@@ -437,7 +437,7 @@ def initialize(*) + proto: [ 'https' ] + }) + +- Rack::Utils.forwarded_values('for=3.4.5.6; proto=http, proto=https').must_equal({ ++ Rack::Utils.forwarded_values("for=3.4.5.6\nproto=http, proto=https").must_equal({ + for: [ '3.4.5.6' ], + proto: [ 'http', 'https' ] + }) +@@ -447,7 +447,33 @@ def initialize(*) + proto: [ 'http', 'https' ] + }) + ++ Rack::Utils.forwarded_values('for="3.4.5.6;host=evil.com"; proto=http, proto=https; for=1.2.3.4').must_equal({ ++ for: [ '3.4.5.6;host=evil.com', '1.2.3.4' ], ++ proto: [ 'http', 'https' ] ++ }) ++ ++ Rack::Utils.forwarded_values('for=" 3.4.5.6\"; ";; proto=http, proto=https; for=1.2.3.4').must_equal({ ++ for: [ ' 3.4.5.6"; ', '1.2.3.4' ], ++ proto: [ 'http', 'https' ] ++ }) ++ ++ Rack::Utils.forwarded_values('proto=http, proto=https; for=1.2.3.4; for=" 3.4.5.6\"; "').must_equal({ ++ for: [ '1.2.3.4', ' 3.4.5.6"; ' ], ++ proto: [ 'http', 'https' ] ++ }) ++ ++ Rack::Utils.forwarded_values('proto=http, proto=https; for=1.2.3.4; for=" 3.4.5.6\"; "; ').must_equal({ ++ for: [ '1.2.3.4', ' 3.4.5.6"; ' ], ++ proto: [ 'http', 'https' ] ++ }) ++ + Rack::Utils.forwarded_values('for=3.4.5.6; foo=bar').must_be_nil ++ ++ Rack::Utils.forwarded_values('for=a;' * 1024).must_equal({for: ["a"]*1024}) ++ Rack::Utils.forwarded_values('for="a' + "\\\\" * 1024 + 'b"').must_equal({for: ['a' + ("\\" * 1024) + 'b']}) ++ ++ Rack::Utils.forwarded_values('for=a;' * 1025).must_be_nil ++ Rack::Utils.forwarded_values('for="a' + "\\\\" * 1025 + 'b"').must_be_nil + end + + it "select best quality match" do diff -Nru ruby-rack-3.1.20/debian/patches/CVE-2026-34230.patch ruby-rack-3.1.20/debian/patches/CVE-2026-34230.patch --- ruby-rack-3.1.20/debian/patches/CVE-2026-34230.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/CVE-2026-34230.patch 2026-09-01 04:35:17.000000000 +0000 @@ -0,0 +1,109 @@ +From 55db26e7f43d3d45e1476f02ada75e0503abc2f1 Mon Sep 17 00:00:00 2001 +From: Samuel Williams +Date: Tue, 31 Mar 2026 16:33:12 +1300 +Subject: [PATCH] Avoid O(n^2) algorithm in Rack::Utils.select_best_encoding + +If a wildcard has already been seen as an acceptable encoding, +ignore additional wildcards. + +Other improvements while here: + +* Only process up to 16 encodings. + +* Improve efficiency of candidate sorting. + +Add tests for: + +* Lower but non-zero wildcard priority + +* Multiple wildcards with different priorities +--- + lib/rack/utils.rb | 36 +++++++++++++++++++++++++++++++++--- + test/spec_utils.rb | 8 ++++---- + 3 files changed, 38 insertions(+), 7 deletions(-) + +--- a/lib/rack/utils.rb ++++ b/lib/rack/utils.rb +@@ -249,17 +249,41 @@ module Rack + end + end + ++ # 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. + def select_best_encoding(available_encodings, accept_encoding) + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + ++ # Only process the first 16 encodings ++ accept_encoding = accept_encoding[0...16] + expanded_accept_encoding = [] ++ wildcard_seen = false + + accept_encoding.each do |m, q| + preference = available_encodings.index(m) || available_encodings.size + + if m == "*" +- (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 + end + else + expanded_accept_encoding << [m, q, preference] +@@ -267,7 +291,13 @@ module Rack + end + + encoding_candidates = expanded_accept_encoding +- .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 + .map!(&:first) + + unless encoding_candidates.include?("identity") +--- a/test/spec_utils.rb ++++ b/test/spec_utils.rb +@@ -518,10 +518,7 @@ describe Rack::Utils do + end + + it "figure out which encodings are acceptable" do +- helper = lambda do |a, b| +- Rack::Request.new(Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => a)) +- Rack::Utils.select_best_encoding(a, b) +- end ++ helper = Rack::Utils.method(:select_best_encoding) + + helper.call(%w(), [["x", 1]]).must_be_nil + helper.call(%w(identity), [["identity", 0.0]]).must_be_nil +@@ -539,6 +536,9 @@ describe Rack::Utils do + + helper.call(%w(foo bar identity), [["foo", 0], ["bar", 0]]).must_equal "identity" + helper.call(%w(foo bar baz identity), [["*", 0], ["identity", 0.1]]).must_equal "identity" ++ helper.call(%w(foo bar baz identity), [["*", 0.1], ["identity", 0.2]]).must_equal "identity" ++ helper.call(%w(foo bar baz identity), [["*", 0.1], ["identity", 0.2], ["*", 0.3]]).must_equal "identity" ++ helper.call(%w(foo bar baz identity), [["*", 0.3], ["identity", 0.2], ["*", 0.1]]).must_equal "foo" + end + + it "should perform constant time string comparison" do diff -Nru ruby-rack-3.1.20/debian/patches/CVE-2026-34763.patch ruby-rack-3.1.20/debian/patches/CVE-2026-34763.patch --- ruby-rack-3.1.20/debian/patches/CVE-2026-34763.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/CVE-2026-34763.patch 2026-09-01 04:35:17.000000000 +0000 @@ -0,0 +1,55 @@ +From 29b17c58e55539b5b9c1afd0d86266e54150193f Mon Sep 17 00:00:00 2001 +From: Haruki Oyama +Date: Mon, 30 Mar 2026 23:56:08 +0200 +Subject: [PATCH] Root directory disclosure via unescaped regex interpolation + in `Rack::Directory`. + +Escape the root path before interpolating into a regular expression, +preventing RegexpError when the root contains metacharacters and +avoiding path disclosure when regex silently mismatches. +--- + lib/rack/directory.rb | 2 +- + test/spec_directory.rb | 23 +++++++++++++++++++++++ + 3 files changed, 30 insertions(+), 1 deletion(-) + +--- a/lib/rack/directory.rb ++++ b/lib/rack/directory.rb +@@ -51,7 +51,7 @@ table { width:100%%; } + class DirectoryBody < Struct.new(:root, :path, :files) + # Yield strings for each part of the directory entry + def each +- show_path = Utils.escape_html(path.sub(/^#{root}/, '')) ++ show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, '')) + yield(DIR_PAGE_HEADER % [ show_path, show_path ]) + + unless path.chomp('/') == root +--- a/test/spec_directory.rb ++++ b/test/spec_directory.rb +@@ -260,4 +260,27 @@ describe Rack::Directory do + res.must_be :not_found? + res.body.must_be :empty? + end ++ ++ it "handles root paths containing regex metacharacters" do ++ Dir.mktmpdir do |tmpdir| ++ # Create a directory with a name that contains regex metacharacters: ++ root = File.join(tmpdir, "plus+root") ++ FileUtils.mkdir(root) ++ ++ # Create a file in the directory: ++ File.write(File.join(root, "file.txt"), "test") ++ ++ # Make a request to the directory app: ++ app = Rack::Lint.new(Rack::Directory.new(root)) ++ res = Rack::MockRequest.new(app).get("/") ++ res.must_be :ok? ++ ++ # This should not leak the root directory: ++ res.body.wont_include root ++ res.body.wont_include Rack::Utils.escape_html(tmpdir) ++ ++ # This is always okay: ++ res.body.must_include "file.txt" ++ end ++ end + end diff -Nru ruby-rack-3.1.20/debian/patches/CVE-2026-34785.patch ruby-rack-3.1.20/debian/patches/CVE-2026-34785.patch --- ruby-rack-3.1.20/debian/patches/CVE-2026-34785.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/CVE-2026-34785.patch 2026-09-01 04:35:17.000000000 +0000 @@ -0,0 +1,56 @@ +From a17cb99b3440a4db09fb920407adf5ead127704c Mon Sep 17 00:00:00 2001 +From: Jeremy Evans +Date: Thu, 5 Mar 2026 20:31:51 -0800 +Subject: [PATCH] Fix root prefix bug in Rack::Static + +This is similar to the fix of CVE-2026-22860 for Rack::Directory. +--- + lib/rack/static.rb | 5 ++++- + test/spec_static.rb | 15 +++++++++++++++ + 3 files changed, 20 insertions(+), 1 deletion(-) + +--- a/lib/rack/static.rb ++++ b/lib/rack/static.rb +@@ -93,6 +93,9 @@ module Rack + def initialize(app, options = {}) + @app = app + @urls = options[:urls] || ["/favicon.ico"] ++ if @urls.kind_of?(Array) ++ @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze ++ end + @index = options[:index] + @gzip = options[:gzip] + @cascade = options[:cascade] +@@ -115,7 +118,7 @@ module Rack + end + + def route_file(path) +- @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) } + end + + def can_serve(path) +--- a/test/spec_static.rb ++++ b/test/spec_static.rb +@@ -237,6 +237,21 @@ describe Rack::Static do + res.headers['cache-control'].must_equal 'public, max-age=42' + 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::Static.new(proc { |env| [403, {}, ""] }, root: dir, urls: ["/root"]) ++ res = Rack::MockRequest.new(app).get("/root_test/test.txt") ++ ++ res.must_be :forbidden? ++ end ++ end ++ + it "expands the root path upon the middleware initialization" do + relative_path = STATIC_OPTIONS[:root].sub("#{Dir.pwd}/", '') + opts = { urls: [""], root: relative_path, index: 'index.html' } diff -Nru ruby-rack-3.1.20/debian/patches/CVE-2026-34786.patch ruby-rack-3.1.20/debian/patches/CVE-2026-34786.patch --- ruby-rack-3.1.20/debian/patches/CVE-2026-34786.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/CVE-2026-34786.patch 2026-09-01 04:35:17.000000000 +0000 @@ -0,0 +1,75 @@ +From 84937c38065d0a7630828fdd526201c5241a9619 Mon Sep 17 00:00:00 2001 +From: haruki0409 <76884995+haruki0409@users.noreply.github.com> +Date: Sun, 22 Mar 2026 17:25:59 +0900 +Subject: [PATCH] Fix `header_rules` bypass via URL-encoded paths. + +Decode path once in applicable_rules before matching, fixing: +- URL-encoded paths bypassing :fonts, Array, and Regexp header rules. +- Path mutation across rules when String rule unescapes inside find_all. +- Array rule values interpolated into regexp without Regexp.escape. +--- + lib/rack/static.rb | 5 +++-- + test/spec_static.rb | 29 +++++++++++++++++++++++++++++ + 3 files changed, 33 insertions(+), 2 deletions(-) + +--- a/lib/rack/static.rb ++++ b/lib/rack/static.rb +@@ -168,6 +168,8 @@ module Rack + + # Convert HTTP header rules to HTTP headers + def applicable_rules(path) ++ path = ::Rack::Utils.unescape_path(path) ++ + @header_rules.find_all do |rule, new_headers| + case rule + when :all +@@ -175,10 +177,9 @@ module Rack + when :fonts + /\.(?:ttf|otf|eot|woff2|woff|svg)\z/.match?(path) + when String +- path = ::Rack::Utils.unescape(path) + path.start_with?(rule) || path.start_with?('/' + rule) + when Array +- /\.(#{rule.join('|')})\z/.match?(path) ++ /\.#{Regexp.union(rule)}\z/.match?(path) + when Regexp + rule.match?(path) + else +--- a/test/spec_static.rb ++++ b/test/spec_static.rb +@@ -252,6 +252,35 @@ describe Rack::Static do + end + end + ++ it "applies :fonts header rules to URL-encoded paths" do ++ res = @header_request.get('/cgi/assets/fonts/font%2Eeot') ++ res.must_be :ok? ++ res.headers['cache-control'].must_equal 'public, max-age=200' ++ end ++ ++ it "applies Array header rules to URL-encoded paths" do ++ res = @header_request.get('/cgi/assets/images/image%2Epng') ++ res.must_be :ok? ++ res.headers['cache-control'].must_equal 'public, max-age=300' ++ end ++ ++ it "applies Regexp header rules to URL-encoded paths" do ++ res = @header_request.get('/cgi/assets/stylesheets/app%2Ecss') ++ res.must_be :ok? ++ res.headers['cache-control'].must_equal 'public, max-age=600' ++ end ++ ++ it "escapes Array rule entries when building regexp" do ++ opts = OPTIONS.merge(header_rules: [ ++ [['p.g'], { 'cache-control' => 'public, max-age=999' }] ++ ]) ++ request = Rack::MockRequest.new(static(DummyApp.new, opts)) ++ # "p.g" should not match "png" since the dot must be literal ++ res = request.get('/cgi/assets/images/image.png') ++ res.must_be :ok? ++ res.headers['cache-control'].must_be_nil ++ end ++ + it "expands the root path upon the middleware initialization" do + relative_path = STATIC_OPTIONS[:root].sub("#{Dir.pwd}/", '') + opts = { urls: [""], root: relative_path, index: 'index.html' } diff -Nru ruby-rack-3.1.20/debian/patches/CVE-2026-34826.patch ruby-rack-3.1.20/debian/patches/CVE-2026-34826.patch --- ruby-rack-3.1.20/debian/patches/CVE-2026-34826.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/CVE-2026-34826.patch 2026-09-01 04:35:17.000000000 +0000 @@ -0,0 +1,69 @@ +From 345a4cfa51f451e58b2931322998e04f3cf6dc0d Mon Sep 17 00:00:00 2001 +From: Jeremy Evans +Date: Thu, 12 Mar 2026 08:16:48 -0700 +Subject: [PATCH] Use a default limit of 100 byte ranges + +Allow exceeding this limit by passing max_ranges keyword argument. + +If the limit is exceeded, return nil, treating the request as not +requesting ranges. This seems better than returning [], which would +treat the request as requesting no ranges. We use [] when the total +size exceeds the size of the file, as such case is obviously a +problem. However, a request with more than the given number of +ranges is not obviously a problem. +--- + lib/rack/utils.rb | 10 ++++++---- + test/spec_utils.rb | 12 ++++++++++++ + 3 files changed, 19 insertions(+), 4 deletions(-) + +diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb +index 1843949ee..cbc9eda58 100644 +--- a/lib/rack/utils.rb ++++ b/lib/rack/utils.rb +@@ -496,17 +496,19 @@ def rfc2822(time) + # Parses the "Range:" header, if present, into an array of Range objects. + # Returns nil if the header is missing or syntactically invalid. + # Returns an empty array if none of the ranges are satisfiable. +- 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 + end + +- def get_byte_ranges(http_range, size) ++ def get_byte_ranges(http_range, size, max_ranges: 100) + # See + # Ignore Range when file size is 0 to avoid a 416 error. + return nil if size.zero? + return nil unless http_range && http_range =~ /bytes=([^;]+)/ ++ byte_range = $1 ++ return nil if byte_range.count(',') >= max_ranges + ranges = [] +- $1.split(/,\s*/).each do |range_spec| ++ byte_range.split(/,[ \t]*/).each do |range_spec| + return nil unless range_spec.include?('-') + range = range_spec.split('-') + r0, r1 = range[0], range[1] +diff --git a/test/spec_utils.rb b/test/spec_utils.rb +index d4461aa22..e233dc82f 100644 +--- a/test/spec_utils.rb ++++ b/test/spec_utils.rb +@@ -718,6 +718,18 @@ def initialize(*) + assert_equal [], Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=0-20,0-500" }, 500) + end + ++ it "returns an empty list if the number of ranges exceeds what is allowed" do ++ range = "bytes=#{Array.new(101) { |i| "#{i}=#{i}"}.join(',')}" ++ assert_nil Rack::Utils.byte_ranges({ "HTTP_RANGE" => range }, 500) ++ assert_nil Rack::Utils.get_byte_ranges(range, 500) ++ ++ assert_nil Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=0-0,1-1" }, 500, max_ranges: 1) ++ assert_nil Rack::Utils.get_byte_ranges("bytes=0-0,1-1", 500, max_ranges: 1) ++ ++ assert_equal [0..0], Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=0-0" }, 500, max_ranges: 1) ++ assert_equal [0..0], Rack::Utils.get_byte_ranges("bytes=0-0", 500, max_ranges: 1) ++ end ++ + it "parse simple byte ranges from env" do + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=123-456" }, 500).must_equal [(123..456)] + end diff -Nru ruby-rack-3.1.20/debian/patches/CVE-2026-34827.patch ruby-rack-3.1.20/debian/patches/CVE-2026-34827.patch --- ruby-rack-3.1.20/debian/patches/CVE-2026-34827.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/CVE-2026-34827.patch 2026-09-01 04:35:17.000000000 +0000 @@ -0,0 +1,98 @@ +From 17ce7836be1523a7b453f3c06fe070ad7c954708 Mon Sep 17 00:00:00 2001 +From: Jeremy Evans +Date: Thu, 5 Mar 2026 19:18:36 -0800 +Subject: [PATCH] Limit the number of quoted escapes during multipart parsing + +This sets a default limit of 8192 escapes, which can be modified +using the RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT +environment variable. +--- + lib/rack/multipart/parser.rb | 9 ++++++++ + test/spec_multipart.rb | 44 ++++++++++++++++++++++++++++++++++++ + 3 files changed, 54 insertions(+) + +--- a/lib/rack/multipart/parser.rb ++++ b/lib/rack/multipart/parser.rb +@@ -72,6 +72,9 @@ module Rack + 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 ++ + class BoundedIO # :nodoc: + def initialize(io, content_length) + @io = io +@@ -246,6 +249,7 @@ module Rack + @body_retained = nil + @retained_size = 0 + @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT) ++ @content_disposition_quoted_escapes = 0 + @collector = Collector.new tempfile + + @sbuf = StringScanner.new("".dup) +@@ -406,6 +410,11 @@ module Rack + # stop parsing parameter value if found ending quote + break if c == '"' + ++ @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 ++ + escaped_char = disposition.slice!(0, 1) + if param == 'filename' && escaped_char != '"' + # Possible IE uploaded filename, append both escape backslash and value +--- a/test/spec_multipart.rb ++++ b/test/spec_multipart.rb +@@ -648,6 +648,50 @@ describe Rack::Multipart do + x.must_equal "application/pdf"=>[""] + end + ++ quoted_escape_test_parse = lambda do |parts, escapes_per_part| ++ boundary = '---------------------------932620571087722842402766118' ++ escaped_quotes = '\\"' * (escapes_per_part/2) ++ unescaped_quotes = '"' * (escapes_per_part/2) ++ ++ data = StringIO.new ++ parts.times do |i| ++ data.write("--#{boundary}") ++ data.write("\r\n") ++ data.write("Content-Disposition: form-data; name=\"a#{i}#{escaped_quotes}\" filename=\"b#{i}#{escaped_quotes}\"") ++ data.write("\r\n") ++ data.write("content-type:application/pdf\r\n") ++ data.write("\r\n") ++ data.write("--#{boundary}--\r\n") ++ end ++ 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), unescaped_quotes] ++ end ++ ++ it "allows up to 8192 quoted escapes during parsing" do ++ parts = 32 ++ x, unescaped_quotes = quoted_escape_test_parse.call(parts, 256) ++ x.keys.must_equal Array.new(parts) {|i| "a#{i}#{unescaped_quotes}" } ++ parts.times do |i| ++ key = "a#{i}#{unescaped_quotes}" ++ v = x[key] ++ v[:filename].must_equal "b#{i}#{unescaped_quotes}" ++ v[:name].must_equal key ++ end ++ end ++ ++ it "disallows more than 8192 quoted escapes during parsing" do ++ proc{quoted_escape_test_parse.call(32, 258)}.must_raise Rack::Multipart::Error ++ proc{quoted_escape_test_parse.call(33, 256)}.must_raise Rack::Multipart::Error ++ end ++ + it 'raises an EOF error on content-length mismatch' do + env = Rack::MockRequest.env_for("/", multipart_fixture(:empty)) + env['rack.input'] = StringIO.new diff -Nru ruby-rack-3.1.20/debian/patches/CVE-2026-34829.patch ruby-rack-3.1.20/debian/patches/CVE-2026-34829.patch --- ruby-rack-3.1.20/debian/patches/CVE-2026-34829.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/CVE-2026-34829.patch 2026-09-01 04:35:17.000000000 +0000 @@ -0,0 +1,141 @@ +From 367a2a0ec6fbef605c9412dadfd5763b7867441f Mon Sep 17 00:00:00 2001 +From: Lio +Date: Sun, 8 Mar 2026 16:02:23 +0700 +Subject: [PATCH] Add Content-Length size check in Rack::Multipart::Parser + +Compare the declared `Content-Length` against a configurable maximum (`PARSER_BYTESIZE_LIMIT`) before any parsing begins. + +If it exceeds the limit, raise an exception immediately. +--- + lib/rack/multipart/parser.rb | 16 +++++++++++ + test/spec_multipart.rb | 55 ++++++++++++++++++++++++++++++++++++ + 3 files changed, 72 insertions(+) + +diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb +index fbe614c52..498824b76 100644 +--- a/lib/rack/multipart/parser.rb ++++ b/lib/rack/multipart/parser.rb +@@ -68,6 +68,10 @@ class Parser + BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024) + private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT + ++ 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 ++ + class BoundedIO # :nodoc: + def initialize(io, content_length) + @io = io +@@ -121,6 +125,10 @@ def self.parse(io, content_length, content_type, tmpfile, bufsize, qp) + boundary = parse_boundary content_type + return EMPTY unless boundary + ++ 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 ++ + if boundary.length > 70 + # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary. + # Most clients use no more than 55 characters. +@@ -237,6 +245,7 @@ def initialize(boundary, tempfile, bufsize, query_parser) + @mime_index = 0 + @body_retained = nil + @retained_size = 0 ++ @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT) + @collector = Collector.new tempfile + + @sbuf = StringScanner.new("".dup) +@@ -248,6 +257,7 @@ def initialize(boundary, tempfile, bufsize, query_parser) + end + + def parse(io) ++ @total_bytes_read &&= nil if io.is_a?(BoundedIO) + outbuf = String.new + read_data(io, outbuf) + +@@ -291,6 +301,12 @@ def dequote(str) # From WEBrick::HTTPUtils + def read_data(io, outbuf) + content = io.read(@bufsize, outbuf) + handle_empty_content!(content) ++ 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 + @sbuf.concat(content) + end + +diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb +index ef659b2cd..3c6054118 100644 +--- a/test/spec_multipart.rb ++++ b/test/spec_multipart.rb +@@ -29,6 +29,18 @@ def multipart_file(name) + File.join(File.dirname(__FILE__), "multipart", name.to_s) + end + ++ def with_multipart_limit(limit) ++ previous = Rack::Multipart::Parser.send(:const_get, :PARSER_BYTESIZE_LIMIT) ++ begin ++ Rack::Multipart::Parser.send(:remove_const, :PARSER_BYTESIZE_LIMIT) ++ Rack::Multipart::Parser.const_set(:PARSER_BYTESIZE_LIMIT, limit) ++ yield ++ ensure ++ Rack::Multipart::Parser.send(:remove_const, :PARSER_BYTESIZE_LIMIT) ++ Rack::Multipart::Parser.const_set(:PARSER_BYTESIZE_LIMIT, previous) ++ end ++ end ++ + it "returns nil if the content type is not multipart" do + env = Rack::MockRequest.env_for("/", "CONTENT_TYPE" => 'application/x-www-form-urlencoded', :input => "") + Rack::Multipart.parse_multipart(env).must_be_nil +@@ -64,6 +76,49 @@ def multipart_file(name) + }.must_raise Rack::Multipart::Error + end + ++ it "raises an exception if Content-Length exceeds total bytesize limit" do ++ with_multipart_limit(1024) do ++ env = Rack::MockRequest.env_for("/", ++ "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x", ++ "CONTENT_LENGTH" => "2048", ++ :input => StringIO.new("--AaB03x--\r\n")) ++ ++ lambda { ++ Rack::Multipart.parse_multipart(env) ++ }.must_raise(Rack::Multipart::Error) ++ end ++ end ++ ++ it "allows requests within the total bytesize limit" do ++ with_multipart_limit(1024 * 1024) do ++ env = Rack::MockRequest.env_for("/", multipart_fixture(:text)) ++ params = Rack::Multipart.parse_multipart(env) ++ params["submit-name"].must_equal "Larry" ++ end ++ end ++ ++ it "skips total bytesize check when there is no limit" do ++ with_multipart_limit(nil) do ++ env = Rack::MockRequest.env_for("/", multipart_fixture(:text)) ++ params = Rack::Multipart.parse_multipart(env) ++ params["submit-name"].must_equal "Larry" ++ end ++ end ++ ++ it "enforces total bytesize limit during streaming when Content-Length is absent" do ++ with_multipart_limit(1) do ++ # Even without Content-Length, the streaming check catches oversized uploads ++ fixture = multipart_fixture(:text) ++ fixture.delete("CONTENT_LENGTH") ++ env = Rack::MockRequest.env_for("/", fixture) ++ env.delete("CONTENT_LENGTH") ++ ++ lambda { ++ Rack::Multipart.parse_multipart(env) ++ }.must_raise Rack::Multipart::Error ++ end ++ end ++ + it "raises a bad request exception if no body is given but content type indicates a multipart body" do + env = Rack::MockRequest.env_for("/", "CONTENT_TYPE" => 'multipart/form-data; boundary=BurgerBurger', :input => nil) + lambda { diff -Nru ruby-rack-3.1.20/debian/patches/CVE-2026-34830.patch ruby-rack-3.1.20/debian/patches/CVE-2026-34830.patch --- ruby-rack-3.1.20/debian/patches/CVE-2026-34830.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/CVE-2026-34830.patch 2026-09-01 04:35:17.000000000 +0000 @@ -0,0 +1,59 @@ +From 59a0966a484f2903833fa3e4c81919d3c645738d Mon Sep 17 00:00:00 2001 +From: Jeremy Evans +Date: Tue, 10 Mar 2026 07:31:03 -0700 +Subject: [PATCH] Only do a simple substitution on the x-accel-mapping paths + +Mention the substitution is case insensitive in the documentation, +since if the file system is case sensitive, this would be unexpected. +--- + lib/rack/sendfile.rb | 4 ++-- + test/spec_sendfile.rb | 14 ++++++++++++++ + 3 files changed, 17 insertions(+), 2 deletions(-) + +diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb +index 69a0c5574..e3bd35ec3 100644 +--- a/lib/rack/sendfile.rb ++++ b/lib/rack/sendfile.rb +@@ -51,7 +51,7 @@ module Rack + # + # The `x-accel-mapping` header should specify the location on the file system, + # followed by an equals sign (=), followed name of the private URL pattern +- # 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 + # resulting path. + # + # To enable `x-accel-redirect`, you must configure the middleware explicitly: +@@ -186,7 +186,7 @@ def map_accel_path(env, path) + # Safe to use header: explicit config + no app mappings: + mapping.split(',').map(&:strip).each do |m| + internal, external = m.split('=', 2).map(&:strip) +- new_path = path.sub(/\A#{internal}/i, external) ++ new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external) + return new_path unless path == new_path + end + +diff --git a/test/spec_sendfile.rb b/test/spec_sendfile.rb +index dfe3fbeb6..acef281b4 100644 +--- a/test/spec_sendfile.rb ++++ b/test/spec_sendfile.rb +@@ -102,6 +102,20 @@ def open_file(path) + end + end + ++ it "does not do a regexp substitution on the internal path" do ++ tmpdir = Dir.tmpdir.dup ++ tmpdir[1..2] = ".*" ++ headers = { ++ 'HTTP_X_ACCEL_MAPPING' => "#{tmpdir}/=/foo/bar/" ++ } ++ request(headers, sendfile_body, [], 'X-Accel-Redirect') do |response| ++ response.must_be :ok? ++ response.body.must_be :empty? ++ response.headers['content-length'].must_equal '0' ++ response.headers['x-accel-redirect'].must_equal '/tmp/rack_sendfile' ++ end ++ end ++ + it "sets x-accel-redirect response header to percent-encoded path" do + headers = { + 'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/foo/bar%/" diff -Nru ruby-rack-3.1.20/debian/patches/CVE-2026-34831.patch ruby-rack-3.1.20/debian/patches/CVE-2026-34831.patch --- ruby-rack-3.1.20/debian/patches/CVE-2026-34831.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/CVE-2026-34831.patch 2026-09-01 04:35:17.000000000 +0000 @@ -0,0 +1,75 @@ +From c3645d377f0335a779812bf3f36e238d87d9b4e6 Mon Sep 17 00:00:00 2001 +From: Samuel Williams +Date: Wed, 1 Apr 2026 12:50:41 +1300 +Subject: [PATCH] Use `String#bytesize` for `Content-Length` in error + responses. + +`String#size` returns character count, not byte count. For responses +containing multi-byte UTF-8 characters, this produces an incorrect +`Content-Length` value, violating RFC 9110 Section 8.6. +--- + lib/rack/files.rb | 2 +- + test/spec_files.rb | 37 +++++++++++++++++++++++++++++++++++++ + 3 files changed, 39 insertions(+), 1 deletion(-) + +diff --git a/lib/rack/files.rb b/lib/rack/files.rb +index 5b8353f5b..247a6e434 100644 +--- a/lib/rack/files.rb ++++ b/lib/rack/files.rb +@@ -194,7 +194,7 @@ def fail(status, body, headers = {}) + status, + { + CONTENT_TYPE => "text/plain", +- CONTENT_LENGTH => body.size.to_s, ++ CONTENT_LENGTH => body.bytesize.to_s, + "x-cascade" => "pass" + }.merge!(headers), + [body] +diff --git a/test/spec_files.rb b/test/spec_files.rb +index d220cd01a..cc1f4dcaf 100644 +--- a/test/spec_files.rb ++++ b/test/spec_files.rb +@@ -155,6 +155,43 @@ def files(*args) + res.must_be :not_found? + end + ++ it "uses bytesize not size for Content-Length in error responses with multibyte UTF-8" do ++ app = Rack::Files.new(DOCROOT) ++ ++ # Create env directly with UTF-8 encoded PATH_INFO (not ASCII-8BIT like MockRequest forces): ++ env = { ++ "REQUEST_METHOD" => "GET", ++ "PATH_INFO" => "/cgi/caf%C3%A9", # URL-encoded "café" ++ "SCRIPT_NAME" => "", ++ "QUERY_STRING" => "", ++ "SERVER_NAME" => "example.org", ++ "SERVER_PORT" => "80", ++ "rack.url_scheme" => "http", ++ "rack.input" => StringIO.new, ++ "rack.errors" => StringIO.new ++ } ++ ++ # Verify PATH_INFO is UTF-8: ++ assert_equal Encoding::UTF_8, env["PATH_INFO"].encoding ++ ++ status, headers, body = app.call(env) ++ ++ # Should be 404 not found: ++ assert_equal 404, status ++ ++ # Extract body content: ++ body_str = String.new ++ body.each { |part| body_str << part } ++ ++ # The body "File not found: /cgi/café\n" has 26 chars but 27 bytes: ++ assert_equal 26, body_str.size # character count ++ assert_equal 27, body_str.bytesize # byte count ++ ++ # Content-Length must be 27 (bytes), not 26 (characters): ++ # (This will FAIL if using .size instead of .bytesize) ++ headers["content-length"].must_equal "27" ++ end ++ + it "detect SystemCallErrors" do + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi") + diff -Nru ruby-rack-3.1.20/debian/patches/CVE-2026-34835.patch ruby-rack-3.1.20/debian/patches/CVE-2026-34835.patch --- ruby-rack-3.1.20/debian/patches/CVE-2026-34835.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/CVE-2026-34835.patch 2026-09-01 04:35:17.000000000 +0000 @@ -0,0 +1,108 @@ +From c49558af795b4c1978d16db071c8344db05a2b0d Mon Sep 17 00:00:00 2001 +From: Jeremy Evans +Date: Thu, 12 Mar 2026 07:52:21 -0700 +Subject: [PATCH] Change Rack::Request::AUTHORITY to only match RFC allowed + characters + +RFC 9110 specifies that allowed characters in a Host header come +from RFC 3986 Section 3.2.2, which provides the following ABNF: + +``` + host = IP-literal / IPv4address / reg-name + + reg-name = *( unreserved / pct-encoded / sub-delims ) + + unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + + pct-encoded = "%" HEXDIG HEXDIG + + sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + / "*" / "+" / "," / ";" / "=" +``` + +This limits the allowed characters to those characters. + +This breaks a spec that tests for internationalized domain names. +Such a spec is incorrect as internationalized domain names must be +encoded via punycode in Host headers, so update the specs to +correctly test for the punycode versions. +--- + lib/rack/request.rb | 4 ++-- + test/spec_request.rb | 40 ++++++++++++++++++++++++++++++++-------- + 3 files changed, 35 insertions(+), 10 deletions(-) + +diff --git a/lib/rack/request.rb b/lib/rack/request.rb +index 007437b6a..47ee27ef1 100644 +--- a/lib/rack/request.rb ++++ b/lib/rack/request.rb +@@ -728,8 +728,8 @@ def split_header(value) + # Match IPv6 as a string of hex digits and colons in square brackets + \[(?
#{ipv6})\] + | +- # Match any other printable string (except square brackets) as a hostname +- (?
[[[:graph:]&&[^\[\]]]]*?) ++ # Match characters allowed by RFC 3986 Section 3.2.2 ++ (?
[-a-zA-Z0-9._~%!$&'()*+,;=]*?) + ) + (:(?\d+))? + \z +diff --git a/test/spec_request.rb b/test/spec_request.rb +index ee7a16617..e8b6d42fd 100644 +--- a/test/spec_request.rb ++++ b/test/spec_request.rb +@@ -148,23 +148,47 @@ class RackRequestTest < Minitest::Spec + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "♡.com") +- req.host.must_equal "♡.com" +- req.hostname.must_equal "♡.com" ++ req.host.must_be_nil ++ req.hostname.must_be_nil ++ ++ # Punycode conversion of ♡.com ++ req = make_request \ ++ Rack::MockRequest.env_for("/", "HTTP_HOST" => "xn--c6h.com") ++ req.host.must_equal "xn--c6h.com" ++ req.hostname.must_equal "xn--c6h.com" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "♡.com:80") +- req.host.must_equal "♡.com" +- req.hostname.must_equal "♡.com" ++ req.host.must_be_nil ++ req.hostname.must_be_nil ++ ++ # Punycode conversion of ♡.com:80 ++ req = make_request \ ++ Rack::MockRequest.env_for("/", "HTTP_HOST" => "xn--c6h.com:80") ++ req.host.must_equal "xn--c6h.com" ++ req.hostname.must_equal "xn--c6h.com" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "nic.谷歌") +- req.host.must_equal "nic.谷歌" +- req.hostname.must_equal "nic.谷歌" ++ req.host.must_be_nil ++ req.hostname.must_be_nil ++ ++ # Punycode conversion of nic.谷歌 ++ req = make_request \ ++ Rack::MockRequest.env_for("/", "HTTP_HOST" => "nic.xn--flw351e") ++ req.host.must_equal "nic.xn--flw351e" ++ req.hostname.must_equal "nic.xn--flw351e" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "nic.谷歌:80") +- req.host.must_equal "nic.谷歌" +- req.hostname.must_equal "nic.谷歌" ++ req.host.must_be_nil ++ req.hostname.must_be_nil ++ ++ # Punycode conversion of nic.谷歌:80 ++ req = make_request \ ++ Rack::MockRequest.env_for("/", "HTTP_HOST" => "nic.xn--flw351e:80") ++ req.host.must_equal "nic.xn--flw351e" ++ req.hostname.must_equal "nic.xn--flw351e" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "technically_invalid.example.com") diff -Nru ruby-rack-3.1.20/debian/patches/series ruby-rack-3.1.20/debian/patches/series --- ruby-rack-3.1.20/debian/patches/series 2026-03-10 04:14:22.000000000 +0000 +++ ruby-rack-3.1.20/debian/patches/series 2026-09-01 04:35:17.000000000 +0000 @@ -1 +1,14 @@ skip-unreadable-dir-test.patch +CVE-2026-26961.patch +CVE-2026-26962.patch +CVE-2026-32762.patch +CVE-2026-34230.patch +CVE-2026-34763.patch +CVE-2026-34785.patch +CVE-2026-34786.patch +CVE-2026-34826.patch +CVE-2026-34829.patch +CVE-2026-34827.patch +CVE-2026-34830.patch +CVE-2026-34831.patch +CVE-2026-34835.patch