Version in base suite: 7.2.2.2+dfsg-2~deb13u1 Base version: rails_7.2.2.2+dfsg-2~deb13u1 Target version: rails_7.2.2.2+dfsg-2~deb13u2 Base file: /srv/ftp-master.debian.org/ftp/pool/main/r/rails/rails_7.2.2.2+dfsg-2~deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/r/rails/rails_7.2.2.2+dfsg-2~deb13u2.dsc changelog | 42 + patches/CVE-2026-33168-skip-blank-attribute-names.patch | 79 +++ patches/CVE-2026-33169-delimit-without-backtracking.patch | 69 ++ patches/CVE-2026-33170-preserve-unsafe-safebuffer.patch | 52 ++ patches/CVE-2026-33173-filter-direct-upload-metadata.patch | 68 ++ patches/CVE-2026-33174-limit-streaming-chunk-size.patch | 102 +++ patches/CVE-2026-33176-reject-scientific-notation.patch | 43 + patches/CVE-2026-33195-reject-disk-path-traversal.patch | 228 ++++++++ patches/CVE-2026-33202-escape-disk-globs.patch | 79 +++ patches/CVE-2026-33658-limit-streaming-ranges.patch | 87 +++ patches/CVE-2026-66066-block-untrusted-vips.patch | 336 +++++++++++++ patches/series | 10 12 files changed, 1195 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpm2qcui9t/rails_7.2.2.2+dfsg-2~deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpm2qcui9t/rails_7.2.2.2+dfsg-2~deb13u2.dsc: no acceptable signature found diff -Nru rails-7.2.2.2+dfsg/debian/changelog rails-7.2.2.2+dfsg/debian/changelog --- rails-7.2.2.2+dfsg/debian/changelog 2025-12-01 20:45:40.000000000 +0000 +++ rails-7.2.2.2+dfsg/debian/changelog 2026-10-04 21:13:12.000000000 +0000 @@ -1,3 +1,45 @@ +rails (2:7.2.2.2+dfsg-2~deb13u2) trixie-security; urgency=high + + * CVE-2026-66066: disable libvips unfuzzed loaders and savers when + Active Storage loads ruby-vips. BMP, ICO, and PSD variants then + raise Vips::Error (Closes: #1143080). + * CVE-2026-33168: skip blank HTML attribute names in tag helpers. + A blank attribute name bypasses escaping and produces malformed + HTML. A crafted value can be read as a separate attribute, possibly + leading to XSS (Closes: #1132035). + * CVE-2026-33169: delimit numbers without the backtracking expression. + The lookahead regular expression and gsub! can take quadratic time + on a long digit string (Closes: #1132035). + * CVE-2026-33170: keep an unsafe SafeBuffer unsafe after %. + SafeBuffer#% does not copy the html_safe flag. Formatting a buffer + mutated in place, with untrusted arguments, reports html_safe? and + bypasses ERB escaping (Closes: #1132035). + * CVE-2026-33173: drop analyzed, identified, and composed from + direct uploads. + Direct upload metadata can set those flags, skip MIME detection, + and claim a safe content_type for other content (Closes: #1132035). + * CVE-2026-33174: reject Active Storage byte ranges larger than 100MB. + The proxy loads the whole requested byte range into memory. A large + or unbounded Range header can exhaust memory. Ranges whose total + size reaches 100MB are now rejected (Closes: #1132035). + * CVE-2026-33176: do not expand scientific notation in number helpers. + Strings such as 1e10000 are expanded by BigDecimal into a huge + representation, which can exhaust memory and CPU (Closes: #1132035). + * CVE-2026-33195: reject disk keys that leave the Active Storage root. + DiskService#path_for does not keep the resolved path inside the + storage root. A key containing ../ can read, write, or delete files + outside it (Closes: #1132035). + * CVE-2026-33202: escape glob metacharacters in + DiskService#delete_prefixed. + The prefix is passed to Dir.glob without escaping. A key containing + glob metacharacters can delete a different file (Closes: #1132035). + * CVE-2026-33658: allow only one byte range on Active Storage proxies. + The proxy does not limit how many ranges a Range header may contain. + Thousands of small ranges use much more CPU than one request for the + same file (Closes: #1132035). + + -- Simon Quigley Sun, 04 Oct 2026 16:13:12 -0500 + rails (2:7.2.2.2+dfsg-2~deb13u1) trixie-security; urgency=medium * Team upload diff -Nru rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33168-skip-blank-attribute-names.patch rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33168-skip-blank-attribute-names.patch --- rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33168-skip-blank-attribute-names.patch 1970-01-01 00:00:00.000000000 +0000 +++ rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33168-skip-blank-attribute-names.patch 2026-10-04 21:13:12.000000000 +0000 @@ -0,0 +1,79 @@ +Description: Skip blank HTML attribute names in tag helpers + A blank attribute name bypasses escaping and can be read as a second + attribute. tag_options now skips blank keys, including blank data and + aria keys. A blank attribute value is unchanged. +Origin: upstream, https://github.com/rails/rails/commit/0b6f8002b52b9c606fd6be9e7915d9f944cf539c +Bug: https://github.com/rails/rails/security/advisories/GHSA-v55j-83pf-r9cq +Bug-Debian: https://bugs.debian.org/1132035 +Author: Mike Dalessio +Applied-Upstream: 0b6f8002b52b9c606fd6be9e7915d9f944cf539c +Last-Update: 2026-10-04 +--- +--- a/actionview/lib/action_view/helpers/tag_helper.rb ++++ b/actionview/lib/action_view/helpers/tag_helper.rb +@@ -263,16 +263,19 @@ module ActionView + output = +"" + sep = " " + options.each_pair do |key, value| ++ next if key.blank? ++ + type = TAG_TYPES[key] + if type == :data && value.is_a?(Hash) + value.each_pair do |k, v| +- next if v.nil? ++ next if k.blank? || v.nil? ++ + output << sep + output << prefix_tag_option(key, k, v, escape) + end + elsif type == :aria && value.is_a?(Hash) + value.each_pair do |k, v| +- next if v.nil? ++ next if k.blank? || v.nil? + + case v + when Array, Hash +--- a/actionview/test/template/tag_helper_test.rb ++++ b/actionview/test/template/tag_helper_test.rb +@@ -107,6 +107,27 @@ class TagHelperTest < ActionView::TestCa + assert_equal "

", tag("p", included: "") + end + ++ def test_tag_options_rejects_blank_key ++ assert_equal "

", tag("p", "" => "value") ++ assert_equal "

", tag("p", nil => "value") ++ assert_equal '

', tag("p", "" => "value", "class" => "a") ++ assert_equal '

', tag("p", nil => "value", "class" => "a") ++ end ++ ++ def test_tag_options_rejects_blank_data_key ++ assert_equal "

", tag("p", data: { "" => "value" }) ++ assert_equal "

", tag("p", data: { nil => "value" }) ++ assert_equal '

', tag("p", data: { "" => "value", "x" => "y" }) ++ assert_equal '

', tag("p", data: { nil => "value", "x" => "y" }) ++ end ++ ++ def test_tag_options_rejects_blank_aria_key ++ assert_equal "

", tag("p", aria: { "" => "value" }) ++ assert_equal "

", tag("p", aria: { nil => "value" }) ++ assert_equal '

', tag("p", aria: { "" => "value", "x" => "y" }) ++ assert_equal '

', tag("p", aria: { nil => "value", "x" => "y" }) ++ end ++ + def test_tag_builder_options_accepts_blank_option + assert_equal "

", tag.p(included: "") + end +@@ -205,6 +226,13 @@ class TagHelperTest < ActionView::TestCa + tag("the-name", { COMMON_DANGEROUS_CHARS => "the value" }, false, false) + end + ++ def test_tag_with_blank_attribute_name_generates_valid_markup ++ html = tag("img", "src" => "/nonexistent.png", "" => "/onerror=alert(1)") ++ fragment = Nokogiri::HTML5::DocumentFragment.parse(html) ++ attrs = fragment.at_css("img").attribute_nodes.map(&:name) ++ assert_equal [ "src" ], attrs ++ end ++ + def test_tag_builder_with_dangerous_unknown_attribute_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "", diff -Nru rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33169-delimit-without-backtracking.patch rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33169-delimit-without-backtracking.patch --- rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33169-delimit-without-backtracking.patch 1970-01-01 00:00:00.000000000 +0000 +++ rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33169-delimit-without-backtracking.patch 2026-10-04 21:13:12.000000000 +0000 @@ -0,0 +1,69 @@ +Description: Delimit numbers without the backtracking expression + NumberToDelimitedConverter grouped digits with a regular expression + that backtracks on a long run of digits. The default path now walks + the digit string. A caller-supplied delimiter_pattern is unchanged. + The default was still the old expression after the first upstream + commit; 33fbedb1 makes the linear path the default, and that + correction is part of this patch. +Origin: upstream, https://github.com/rails/rails/commit/b54a4b373c6f042cab6ee2033246b1c9ecc38974 +Bug: https://github.com/rails/rails/security/advisories/GHSA-cg4j-q9v8-6v38 +Bug-Debian: https://bugs.debian.org/1132035 +Author: Jean Boussier +Applied-Upstream: b54a4b373c6f042cab6ee2033246b1c9ecc38974, 33fbedb1b169048cd4d03eb45469d6bde62b385f +Last-Update: 2026-10-04 +--- +--- a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb ++++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb +@@ -7,8 +7,6 @@ module ActiveSupport + class NumberToDelimitedConverter < NumberConverter # :nodoc: + self.validate_float = true + +- DEFAULT_DELIMITER_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/ +- + def convert + parts.join(options[:separator]) + end +@@ -16,14 +14,29 @@ module ActiveSupport + private + def parts + left, right = number.to_s.split(".") +- left.gsub!(delimiter_pattern) do |digit_to_delimit| +- "#{digit_to_delimit}#{options[:delimiter]}" ++ if delimiter_pattern ++ left.gsub!(delimiter_pattern) do |digit_to_delimit| ++ "#{digit_to_delimit}#{options[:delimiter]}" ++ end ++ else ++ left_parts = [] ++ offset = left.size % 3 ++ if offset > 0 ++ left_parts << left[0, offset] ++ end ++ ++ (left.size / 3).times do |i| ++ left_parts << left[offset + (i * 3), 3] ++ end ++ ++ left = left_parts.join(options[:delimiter]) + end ++ + [left, right].compact + end + + def delimiter_pattern +- options.fetch(:delimiter_pattern, DEFAULT_DELIMITER_REGEX) ++ options[:delimiter_pattern] + end + end + end +--- a/activesupport/test/number_helper_test.rb ++++ b/activesupport/test/number_helper_test.rb +@@ -139,6 +139,8 @@ module ActiveSupport + assert_equal("12,345,678", number_helper.number_to_delimited(12345678)) + assert_equal("0", number_helper.number_to_delimited(0)) + assert_equal("123", number_helper.number_to_delimited(123)) ++ assert_equal("1,234", number_helper.number_to_delimited(1234)) ++ assert_equal("12,345", number_helper.number_to_delimited(12345)) + assert_equal("123,456", number_helper.number_to_delimited(123456)) + assert_equal("123,456.78", number_helper.number_to_delimited(123456.78)) + assert_equal("123,456.789", number_helper.number_to_delimited(123456.789)) diff -Nru rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33170-preserve-unsafe-safebuffer.patch rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33170-preserve-unsafe-safebuffer.patch --- rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33170-preserve-unsafe-safebuffer.patch 1970-01-01 00:00:00.000000000 +0000 +++ rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33170-preserve-unsafe-safebuffer.patch 2026-10-04 21:13:12.000000000 +0000 @@ -0,0 +1,52 @@ +Description: Keep an unsafe SafeBuffer unsafe after % + Formatting an unsafe buffer marked the result html_safe. The result + now keeps the receiver's html_safe flag. A safe format string stays + safe. +Origin: upstream, https://github.com/rails/rails/commit/c1ad0e8e1972032f3395853a5e99cea035035beb +Bug: https://github.com/rails/rails/security/advisories/GHSA-89vf-4333-qx8v +Bug-Debian: https://bugs.debian.org/1132035 +Author: Jean Boussier +Applied-Upstream: c1ad0e8e1972032f3395853a5e99cea035035beb +Last-Update: 2026-10-04 +--- +--- a/activesupport/lib/active_support/core_ext/string/output_safety.rb ++++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb +@@ -128,7 +128,9 @@ module ActiveSupport # :nodoc: + escaped_args = Array(args).map { |arg| explicit_html_escape_interpolated_argument(arg) } + end + +- self.class.new(super(escaped_args)) ++ new_safe_buffer = self.class.new(super(escaped_args)) ++ new_safe_buffer.instance_variable_set(:@html_safe, @html_safe) ++ new_safe_buffer + end + + attr_reader :html_safe +--- a/activesupport/test/safe_buffer_test.rb ++++ b/activesupport/test/safe_buffer_test.rb +@@ -155,10 +155,24 @@ class SafeBufferTest < ActiveSupport::Te + multiplied_safe_buffer = "
".html_safe * 2 + assert_predicate multiplied_safe_buffer, :html_safe? + +- multiplied_unsafe_buffer = @buffer.gsub("", "<>") * 2 ++ @buffer.gsub!("", "<>") ++ assert_not_predicate @buffer, :html_safe? ++ multiplied_unsafe_buffer = @buffer * 2 + assert_not_predicate multiplied_unsafe_buffer, :html_safe? + end + ++ test "Should preserve html_safe? status on format" do ++ safe_buffer = "
%{name}".html_safe ++ assert_predicate safe_buffer, :html_safe? ++ safe_buffer = safe_buffer % { name: "George" } ++ assert_predicate safe_buffer, :html_safe? ++ ++ unsafe_buffer = @buffer.gsub!("", "<%{name}>") ++ assert_not_predicate unsafe_buffer, :html_safe? ++ unsafe_buffer = unsafe_buffer % { name: "George" } ++ assert_not_predicate unsafe_buffer, :html_safe? ++ end ++ + test "Should concat as a normal string when safe" do + clean = "hello".html_safe + @buffer.gsub!("", "<>") diff -Nru rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33173-filter-direct-upload-metadata.patch rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33173-filter-direct-upload-metadata.patch --- rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33173-filter-direct-upload-metadata.patch 1970-01-01 00:00:00.000000000 +0000 +++ rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33173-filter-direct-upload-metadata.patch 2026-10-04 21:13:12.000000000 +0000 @@ -0,0 +1,68 @@ +Description: Drop internal keys from direct-upload metadata + analyzed, identified, and composed are Active Storage state. A direct + upload can no longer set them. Other metadata keys are kept. +Origin: upstream, https://github.com/rails/rails/commit/707c0f1f41f067fdf96d54e99d43b28dfaae7e53 +Bug: https://github.com/rails/rails/security/advisories/GHSA-qcfx-2mfw-w4cg +Bug-Debian: https://bugs.debian.org/1132035 +Author: Jean Boussier +Applied-Upstream: 707c0f1f41f067fdf96d54e99d43b28dfaae7e53 +Last-Update: 2026-10-04 +--- +--- a/activestorage/app/models/active_storage/blob.rb ++++ b/activestorage/app/models/active_storage/blob.rb +@@ -20,6 +20,11 @@ class ActiveStorage::Blob < ActiveStorag + MINIMUM_TOKEN_LENGTH = 28 + + has_secure_token :key, length: MINIMUM_TOKEN_LENGTH ++ ++ # FIXME: these property should never have been stored in the metadata. ++ # The blob table should be migrated to have dedicated columns for theses. ++ PROTECTED_METADATA = %w(analyzed identified composed) ++ private_constant :PROTECTED_METADATA + store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON + + class_attribute :services, default: {} +@@ -105,6 +110,7 @@ class ActiveStorage::Blob < ActiveStorag + # Once the form using the direct upload is submitted, the blob can be associated with the right record using + # the signed ID. + def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil) ++ metadata = filter_metadata(metadata) + create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name + end + +@@ -168,6 +174,15 @@ class ActiveStorage::Blob < ActiveStorag + raise RuntimeError, "Missing Active Storage service name. Specify Active Storage service name for config.active_storage.service in config/environments/#{Rails.env}.rb" + end + end ++ ++ private ++ def filter_metadata(metadata) ++ if metadata.is_a?(Hash) ++ metadata.without(*PROTECTED_METADATA) ++ else ++ metadata ++ end ++ end + end + + include Analyzable +--- a/activestorage/test/controllers/direct_uploads_controller_test.rb ++++ b/activestorage/test/controllers/direct_uploads_controller_test.rb +@@ -145,8 +145,16 @@ class ActiveStorage::DiskDirectUploadsCo + "library_ID" => "12345" + } + ++ protected_metadata = { ++ "analyzed" => "yolo", ++ "identified" => 42, ++ "composed" => "maybe", ++ } ++ ++ all_metadata = metadata.merge(protected_metadata) ++ + post rails_direct_uploads_url, params: { blob: { +- filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } } ++ filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: all_metadata } } + + response.parsed_body.tap do |details| + assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"]) diff -Nru rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33174-limit-streaming-chunk-size.patch rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33174-limit-streaming-chunk-size.patch --- rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33174-limit-streaming-chunk-size.patch 1970-01-01 00:00:00.000000000 +0000 +++ rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33174-limit-streaming-chunk-size.patch 2026-10-04 21:13:12.000000000 +0000 @@ -0,0 +1,102 @@ +Description: Reject Active Storage byte ranges larger than 100MB + A single range or several ranges whose total size is at least + config.active_storage.streaming_chunk_max_size now get 416. The + default is 100 megabytes. Smaller ranges still stream. +Origin: upstream, https://github.com/rails/rails/commit/8159a9c3de3f27a2bcf2866b8bf9ceb9075e229b +Bug: https://github.com/rails/rails/security/advisories/GHSA-r46p-8f7g-vvvg +Bug-Debian: https://bugs.debian.org/1132035 +Author: Gannon McGibbon +Applied-Upstream: 8159a9c3de3f27a2bcf2866b8bf9ceb9075e229b +Last-Update: 2026-10-04 +--- +--- a/activestorage/app/controllers/concerns/active_storage/streaming.rb ++++ b/activestorage/app/controllers/concerns/active_storage/streaming.rb +@@ -14,7 +14,7 @@ module ActiveStorage::Streaming + def send_blob_byte_range_data(blob, range_header, disposition: nil) + ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size) + +- return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?) ++ return head(:range_not_satisfiable) unless ranges_valid?(ranges) + + if ranges.length == 1 + range = ranges.first +@@ -51,6 +51,12 @@ module ActiveStorage::Streaming + ) + end + ++ def ranges_valid?(ranges) ++ return false if ranges.blank? || ranges.all?(&:blank?) ++ ++ ranges.sum { |range| range.end - range.begin } < ActiveStorage.streaming_chunk_max_size ++ end ++ + # Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+. + # The content type and filename is set directly from the +blob+. + def send_blob_stream(blob, disposition: nil) # :doc: +--- a/activestorage/lib/active_storage.rb ++++ b/activestorage/lib/active_storage.rb +@@ -27,6 +27,7 @@ require "active_record" + require "active_support" + require "active_support/rails" + require "active_support/core_ext/numeric/time" ++require "active_support/core_ext/numeric/bytes" + + require "active_storage/version" + require "active_storage/deprecator" +@@ -350,6 +351,7 @@ module ActiveStorage + ] + mattr_accessor :unsupported_image_processing_arguments + ++ mattr_accessor :streaming_chunk_max_size, default: 100.megabytes + mattr_accessor :service_urls_expire_in, default: 5.minutes + mattr_accessor :touch_attachment_records, default: true + mattr_accessor :urls_expire_in +--- a/activestorage/lib/active_storage/engine.rb ++++ b/activestorage/lib/active_storage/engine.rb +@@ -119,6 +119,7 @@ module ActiveStorage + ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream" + ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2" + ActiveStorage.track_variants = app.config.active_storage.track_variants || false ++ ActiveStorage.streaming_chunk_max_size = app.config.active_storage.streaming_chunk_max_size || 100.megabytes + end + end + +--- a/activestorage/test/controllers/blobs/proxy_controller_test.rb ++++ b/activestorage/test/controllers/blobs/proxy_controller_test.rb +@@ -70,6 +70,20 @@ class ActiveStorage::Blobs::ProxyControl + assert_response :range_not_satisfiable + end + ++ test "Byte Range is too big" do ++ with_streaming_chunk_max_size(1.kilobyte) do ++ get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg")), headers: { "Range" => "bytes=0-" } ++ assert_response :range_not_satisfiable ++ end ++ end ++ ++ test "Byte Range is too big overall" do ++ with_streaming_chunk_max_size(8.bytes) do ++ get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg")), headers: { "Range" => "bytes=0-5,6-12" } ++ assert_response :range_not_satisfiable ++ end ++ end ++ + test "multiple Byte Ranges" do + boundary = SecureRandom.hex + SecureRandom.stub :hex, boundary do +@@ -105,6 +119,15 @@ class ActiveStorage::Blobs::ProxyControl + request = ActionController::TestRequest.create({}) + assert_instance_of ActionController::Live::Response, ActiveStorage::Blobs::ProxyController.make_response!(request) + end ++ ++ private ++ def with_streaming_chunk_max_size(size) ++ old_size = ActiveStorage.streaming_chunk_max_size ++ ActiveStorage.streaming_chunk_max_size = size ++ yield ++ ensure ++ ActiveStorage.streaming_chunk_max_size = old_size ++ end + end + + class ActiveStorage::Blobs::ExpiringProxyControllerTest < ActionDispatch::IntegrationTest diff -Nru rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33176-reject-scientific-notation.patch rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33176-reject-scientific-notation.patch --- rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33176-reject-scientific-notation.patch 1970-01-01 00:00:00.000000000 +0000 +++ rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33176-reject-scientific-notation.patch 2026-10-04 21:13:12.000000000 +0000 @@ -0,0 +1,43 @@ +Description: Do not expand scientific notation in number helpers + BigDecimal accepts d and e notation, so a short string can become a + huge number. Those strings are left as text. Ordinary decimals are + unchanged. +Origin: upstream, https://github.com/rails/rails/commit/ebd6be18120d1136511eb516338e27af25ac0a1a +Bug: https://github.com/rails/rails/security/advisories/GHSA-2j26-frm8-cmj9 +Bug-Debian: https://bugs.debian.org/1132035 +Author: Jean Boussier +Applied-Upstream: ebd6be18120d1136511eb516338e27af25ac0a1a +Last-Update: 2026-10-04 +--- +--- a/activesupport/lib/active_support/number_helper/number_converter.rb ++++ b/activesupport/lib/active_support/number_helper/number_converter.rb +@@ -180,7 +180,7 @@ module ActiveSupport + when Float, Rational + number.to_d(0) + when String +- BigDecimal(number, exception: false) ++ BigDecimal(number, exception: false) unless number.to_s.match?(/[de]/i) + else + number.to_d rescue nil + end +--- a/activesupport/test/number_helper_test.rb ++++ b/activesupport/test/number_helper_test.rb +@@ -458,6 +458,18 @@ module ActiveSupport + assert_equal "x", number_helper.number_to_human("x") + end + end ++ ++ def test_number_helpers_with_scientific_notation ++ [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| ++ assert_equal "$123481223d98989", number_helper.number_to_currency("123481223d98989") ++ assert_equal "$11288E822220222", number_helper.number_to_currency("11288E822220222") ++ assert_equal "-$888E89789", number_helper.number_to_currency("-888E89789") ++ ++ assert_equal "123481223d98989%", number_helper.number_to_percentage("123481223d98989") ++ assert_equal "11288E822220222%", number_helper.number_to_percentage("11288E822220222") ++ assert_equal "-888E89789%", number_helper.number_to_percentage("-888E89789") ++ end ++ end + end + end + end diff -Nru rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33195-reject-disk-path-traversal.patch rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33195-reject-disk-path-traversal.patch --- rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33195-reject-disk-path-traversal.patch 1970-01-01 00:00:00.000000000 +0000 +++ rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33195-reject-disk-path-traversal.patch 2026-10-04 21:13:12.000000000 +0000 @@ -0,0 +1,228 @@ +Description: Reject Active Storage disk keys that leave the storage root + DiskService#path_for raises InvalidKeyError for a blank key, a dot + segment, a null byte, or a path outside the service root. The disk + controller answers 404 or 422. A normal key still resolves inside the + root. Custom keys are trusted input. +Origin: upstream, https://github.com/rails/rails/commit/4933c1e3b8c1bb04925d60347be9f69270392f2c +Bug: https://github.com/rails/rails/security/advisories/GHSA-9xrj-h377-fr87 +Bug-Debian: https://bugs.debian.org/1132035 +Author: Mike Dalessio +Applied-Upstream: 4933c1e3b8c1bb04925d60347be9f69270392f2c +Last-Update: 2026-10-04 +--- +--- a/activestorage/app/controllers/active_storage/disk_controller.rb ++++ b/activestorage/app/controllers/active_storage/disk_controller.rb +@@ -17,6 +17,8 @@ class ActiveStorage::DiskController < Ac + end + rescue Errno::ENOENT + head :not_found ++ rescue ActiveStorage::InvalidKeyError ++ head :not_found + end + + def update +@@ -32,6 +34,8 @@ class ActiveStorage::DiskController < Ac + end + rescue ActiveStorage::IntegrityError + head :unprocessable_entity ++ rescue ActiveStorage::InvalidKeyError ++ head :unprocessable_entity + end + + private +--- a/activestorage/app/models/active_storage/blob.rb ++++ b/activestorage/app/models/active_storage/blob.rb +@@ -16,6 +16,9 @@ + # Blobs are intended to be immutable in as-so-far as their reference to a specific file goes. You're allowed to + # update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file. + # If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one. ++# ++# When using a custom +key+, the value is treated as trusted. Using untrusted user input ++# as the key may result in unexpected behavior. + class ActiveStorage::Blob < ActiveStorage::Record + MINIMUM_TOKEN_LENGTH = 28 + +@@ -98,6 +101,9 @@ class ActiveStorage::Blob < ActiveStorag + # be saved before the upload begins to prevent the upload clobbering another due to key collisions. + # When providing a content type, pass identify: false to bypass + # automatic content type inference. ++ # ++ # The optional +key+ parameter is treated as trusted. Using untrusted user input ++ # as the key may result in unexpected behavior. + def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) + create_after_unfurling!(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap do |blob| + blob.upload_without_unfurling(io) +--- a/activestorage/lib/active_storage/errors.rb ++++ b/activestorage/lib/active_storage/errors.rb +@@ -26,4 +26,8 @@ module ActiveStorage + + # Raised when a Previewer is unable to generate a preview image. + class PreviewError < Error; end ++ ++ # Raised when a storage key resolves to a path outside the service's root ++ # directory, indicating a potential path traversal attack. ++ class InvalidKeyError < Error; end + end +--- a/activestorage/lib/active_storage/service/disk_service.rb ++++ b/activestorage/lib/active_storage/service/disk_service.rb +@@ -98,8 +98,39 @@ module ActiveStorage + { "Content-Type" => content_type } + end + ++ # Every filesystem operation in DiskService resolves paths through this method (or through ++ # make_path_for, which delegates here). This is the primary filesystem security check: all ++ # path-traversal protection is enforced here. New methods that touch the filesystem MUST use ++ # path_for or make_path_for -- never construct paths from +root+ directly. + def path_for(key) # :nodoc: +- File.join root, folder_for(key), key ++ if key.blank? ++ raise ActiveStorage::InvalidKeyError, "key is blank" ++ end ++ ++ # Reject keys with dot segments as defense in depth. This prevents path traversal both outside ++ # and within the storage root. The root containment check below is a more fundamental check on ++ # path traversal outside of the disk service root. ++ begin ++ if key.split("/").intersect?(%w[. ..]) ++ raise ActiveStorage::InvalidKeyError, "key has path traversal segments" ++ end ++ rescue Encoding::CompatibilityError ++ raise ActiveStorage::InvalidKeyError, "key has incompatible encoding" ++ end ++ ++ begin ++ path = File.expand_path(File.join(root, folder_for(key), key)) ++ rescue ArgumentError ++ # ArgumentError catches null bytes ++ raise ActiveStorage::InvalidKeyError, "key is an invalid string" ++ end ++ ++ # The resolved path must be inside the root directory. ++ unless path.start_with?(File.expand_path(root) + "/") ++ raise ActiveStorage::InvalidKeyError, "key is outside of disk service root" ++ end ++ ++ path + end + + def compose(source_keys, destination_key, **) +--- a/activestorage/test/controllers/disk_controller_test.rb ++++ b/activestorage/test/controllers/disk_controller_test.rb +@@ -72,6 +72,26 @@ class ActiveStorage::DiskControllerTest + end + end + ++ test "showing blob with path traversal key returns not found" do ++ encoded_key = ActiveStorage.verifier.generate( ++ { key: "../../etc/passwd", disposition: "inline", content_type: "text/plain", service_name: "local" }, ++ purpose: :blob_key ++ ) ++ get rails_disk_service_url(encoded_key: encoded_key, filename: "hello.txt") ++ assert_response :not_found ++ end ++ ++ test "directly uploading blob with path traversal key returns unprocessable entity" do ++ data = "hello" ++ encoded_token = ActiveStorage.verifier.generate( ++ { key: "../../etc/passwd", content_type: "text/plain", content_length: data.size, checksum: OpenSSL::Digest::MD5.base64digest(data), service_name: "local" }, ++ purpose: :blob_token ++ ) ++ put update_rails_disk_service_url(encoded_token: encoded_token), ++ params: data, headers: { "Content-Type" => "text/plain" } ++ assert_response :unprocessable_entity ++ end ++ + test "directly uploading blob with integrity" do + data = "Something else entirely!" + blob = create_blob_before_direct_upload byte_size: data.size, checksum: OpenSSL::Digest::MD5.base64digest(data) +--- a/activestorage/test/models/attached/one_test.rb ++++ b/activestorage/test/models/attached/one_test.rb +@@ -68,6 +68,15 @@ class ActiveStorage::OneAttachedTest < A + assert_equal "town.jpg", @user.avatar.filename.to_s + end + ++ test "attaching a new blob from a Hash with a path traversal key raises on Disk service" do ++ assert_raises ActiveStorage::InvalidKeyError do ++ @user.avatar.attach key: "../../etc/passwd", io: StringIO.new("malicious"), filename: "exploit.txt", content_type: "text/plain" ++ end ++ ensure ++ ActiveStorage::Attachment.where(blob: ActiveStorage::Blob.where(key: "../../etc/passwd")).delete_all ++ ActiveStorage::Blob.where(key: "../../etc/passwd").delete_all ++ end ++ + test "attaching a new blob from a Hash to an existing record passes record" do + hash = { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg" } + blob = ActiveStorage::Blob.build_after_unfurling(**hash) +--- a/activestorage/test/models/blob_test.rb ++++ b/activestorage/test/models/blob_test.rb +@@ -84,6 +84,17 @@ class ActiveStorage::BlobTest < ActiveSu + assert_equal data, blob.download + end + ++ test "create_and_upload! with a path traversal key raises on Disk service" do ++ assert_raises ActiveStorage::InvalidKeyError do ++ ActiveStorage::Blob.create_and_upload!( ++ key: "../../etc/passwd", ++ io: StringIO.new("malicious content"), ++ filename: "exploit.txt", ++ content_type: "text/plain" ++ ) ++ end ++ end ++ + test "create_and_upload accepts a record for overrides" do + assert_nothing_raised do + create_blob(record: User.new) +--- a/activestorage/test/service/disk_service_test.rb ++++ b/activestorage/test/service/disk_service_test.rb +@@ -70,6 +70,39 @@ class ActiveStorage::Service::DiskServic + assert_equal tmp_config.dig(:tmp, :root), @service.root + end + ++ test "path_for raises InvalidKeyError for basic traversal" do ++ assert_raises ActiveStorage::InvalidKeyError do ++ @service.path_for("../../etc/cron.d/evil") ++ end ++ end ++ ++ test "path_for raises InvalidKeyError for null byte injection" do ++ assert_raises ActiveStorage::InvalidKeyError do ++ @service.path_for("validkey\x00.jpg") ++ end ++ end ++ ++ test "path_for raises InvalidKeyError for empty key" do ++ assert_raises ActiveStorage::InvalidKeyError do ++ @service.path_for("") ++ end ++ ++ assert_raises ActiveStorage::InvalidKeyError do ++ @service.path_for(nil) ++ end ++ end ++ ++ test "path_for returns path within root for a slash-containing key" do ++ path = @service.path_for("avatars/123/photo") ++ assert path.start_with?(File.expand_path(@service.root) + "/") ++ end ++ ++ test "delete_prefixed raises InvalidKeyError for traversal prefix" do ++ assert_raises ActiveStorage::InvalidKeyError do ++ @service.delete_prefixed("../../etc/cron.d/") ++ end ++ end ++ + test "can change root" do + tmp_path_2 = File.join(Dir.tmpdir, "active_storage_2") + @service.root = tmp_path_2 +--- a/guides/source/active_storage_overview.md ++++ b/guides/source/active_storage_overview.md +@@ -598,6 +598,8 @@ There is an additional parameter `key` t + in your S3 Bucket. AWS S3 otherwise uses a random key to name your files. This + approach is helpful if you want to organize your S3 Bucket files better. + ++NOTE: The `key` parameter is treated as trusted. Using untrusted user input as the key may result in unexpected behavior. ++ + ```ruby + @message.images.attach( + io: File.open('/path/to/file'), diff -Nru rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33202-escape-disk-globs.patch rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33202-escape-disk-globs.patch --- rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33202-escape-disk-globs.patch 1970-01-01 00:00:00.000000000 +0000 +++ rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33202-escape-disk-globs.patch 2026-10-04 21:13:12.000000000 +0000 @@ -0,0 +1,79 @@ +Description: Escape glob metacharacters in DiskService#delete_prefixed + delete_prefixed passed the key prefix to Dir.glob. A prefix containing + glob metacharacters could delete a different file. Those characters are + now escaped. A prefix that relied on glob expansion no longer does. +Origin: upstream, https://github.com/rails/rails/commit/fa19073546360856e9f4dab221fc2c5d73a45e82 +Bug: https://github.com/rails/rails/security/advisories/GHSA-73f9-jhhh-hr5m +Bug-Debian: https://bugs.debian.org/1132035 +Author: Mike Dalessio +Applied-Upstream: fa19073546360856e9f4dab221fc2c5d73a45e82 +Last-Update: 2026-10-04 +--- +--- a/activestorage/lib/active_storage/service/disk_service.rb ++++ b/activestorage/lib/active_storage/service/disk_service.rb +@@ -60,7 +60,16 @@ module ActiveStorage + + def delete_prefixed(prefix) + instrument :delete_prefixed, prefix: prefix do +- Dir.glob(path_for("#{prefix}*")).each do |path| ++ prefix_path = path_for(prefix) ++ ++ # File.expand_path (called within path_for) strips trailing slashes. ++ # Restore trailing separator if the original prefix had one, so that ++ # the glob "prefix/*" matches files inside the directory, not siblings ++ # whose names start with the prefix string. ++ prefix_path += "/" if prefix.end_with?("/") ++ ++ escaped = escape_glob_metacharacters(prefix_path) ++ Dir.glob("#{escaped}*").each do |path| + FileUtils.rm_rf(path) + end + end +@@ -187,6 +196,10 @@ module ActiveStorage + [ key[0..1], key[2..3] ].join("/") + end + ++ def escape_glob_metacharacters(path) ++ path.gsub(/[\[\]*?{}\\]/) { |c| "\\#{c}" } ++ end ++ + def make_path_for(key) + path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) } + end +--- a/activestorage/test/service/disk_service_test.rb ++++ b/activestorage/test/service/disk_service_test.rb +@@ -103,6 +103,34 @@ class ActiveStorage::Service::DiskServic + end + end + ++ test "path_for escapes all glob metacharacters" do ++ assert_equal "\\[", @service.send(:escape_glob_metacharacters, "[") ++ assert_equal "\\]", @service.send(:escape_glob_metacharacters, "]") ++ assert_equal "\\*", @service.send(:escape_glob_metacharacters, "*") ++ assert_equal "\\?", @service.send(:escape_glob_metacharacters, "?") ++ assert_equal "\\{", @service.send(:escape_glob_metacharacters, "{") ++ assert_equal "\\}", @service.send(:escape_glob_metacharacters, "}") ++ assert_equal "\\\\", @service.send(:escape_glob_metacharacters, "\\") ++ assert_equal "hello", @service.send(:escape_glob_metacharacters, "hello") ++ end ++ ++ test "delete_prefixed with glob metacharacters only deletes matching files" do ++ base_key = SecureRandom.base58(24) ++ bracket_key = "#{base_key}[1]/file" ++ plain_key = "#{base_key}1/file" ++ ++ @service.upload(bracket_key, StringIO.new("bracket")) ++ @service.upload(plain_key, StringIO.new("plain")) ++ ++ @service.delete_prefixed("#{base_key}[1]/") ++ ++ assert @service.exist?(plain_key), "file should not be deleted" ++ assert_not @service.exist?(bracket_key), "file should be deleted" ++ ensure ++ @service.delete(bracket_key) rescue nil ++ @service.delete(plain_key) rescue nil ++ end ++ + test "can change root" do + tmp_path_2 = File.join(Dir.tmpdir, "active_storage_2") + @service.root = tmp_path_2 diff -Nru rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33658-limit-streaming-ranges.patch rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33658-limit-streaming-ranges.patch --- rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33658-limit-streaming-ranges.patch 1970-01-01 00:00:00.000000000 +0000 +++ rails-7.2.2.2+dfsg/debian/patches/CVE-2026-33658-limit-streaming-ranges.patch 2026-10-04 21:13:12.000000000 +0000 @@ -0,0 +1,87 @@ +Description: Allow only one byte range on Active Storage proxy requests + More than config.active_storage.streaming_max_ranges ranges now get + 416. The default is one range. The engine copies that setting onto + ActiveStorage.streaming_max_ranges. A single range still streams. +Origin: upstream, https://github.com/rails/rails/commit/b8a1665824a43d71cd6406cf9adcae842ceb1c22 +Bug: https://github.com/rails/rails/security/advisories/GHSA-p9fm-f462-ggrg +Bug-Debian: https://bugs.debian.org/1132035 +Author: Jean Boussier +Applied-Upstream: b8a1665824a43d71cd6406cf9adcae842ceb1c22 +Last-Update: 2026-10-04 +--- +--- a/activestorage/app/controllers/concerns/active_storage/streaming.rb ++++ b/activestorage/app/controllers/concerns/active_storage/streaming.rb +@@ -15,6 +15,7 @@ module ActiveStorage::Streaming + ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size) + + return head(:range_not_satisfiable) unless ranges_valid?(ranges) ++ return head(:range_not_satisfiable) if ranges.length > ActiveStorage.streaming_max_ranges + + if ranges.length == 1 + range = ranges.first +--- a/activestorage/lib/active_storage.rb ++++ b/activestorage/lib/active_storage.rb +@@ -362,6 +362,9 @@ module ActiveStorage + + mattr_accessor :track_variants, default: false + ++ singleton_class.attr_accessor :streaming_max_ranges ++ @streaming_max_ranges = 1 ++ + mattr_accessor :video_preview_arguments, default: "-y -vframes 1 -f image2" + + module Transformers +--- a/activestorage/test/controllers/blobs/proxy_controller_test.rb ++++ b/activestorage/test/controllers/blobs/proxy_controller_test.rb +@@ -85,6 +85,9 @@ class ActiveStorage::Blobs::ProxyControl + end + + test "multiple Byte Ranges" do ++ previous_streaming_max_ranges = ActiveStorage.streaming_max_ranges ++ ActiveStorage.streaming_max_ranges = 2 ++ + boundary = SecureRandom.hex + SecureRandom.stub :hex, boundary do + get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg")), headers: { "Range" => "bytes=5-9,13-17" } +@@ -110,6 +113,8 @@ class ActiveStorage::Blobs::ProxyControl + response.body + ) + end ++ ensure ++ ActiveStorage.streaming_max_ranges = previous_streaming_max_ranges + end + + test "uses a Live::Response" do +--- a/guides/source/configuring.md ++++ b/guides/source/configuring.md +@@ -3141,6 +3141,20 @@ The default value depends on the `config + Determines whether the Active Storage assets should be added to the asset pipeline precompilation. It + has no effect if Sprockets is not used. The default value is `true`. + ++#### `config.active_storage.streaming_max_ranges` ++ ++Defines how many ranges a byte range request may contain. ++ ++`ActiveStorage::Streaming` allows requesting partial resources using HTTP Range Requests, ++but that feature can be abused for denial of service attacks. ++ ++By default only a single range of byte is allowed, which allows for retries and the vast majority ++of use cases. If you need multiple byte range support, you can increase that setting. ++ ++| Starting with version | The default value is | ++| --------------------- | -------------------- | ++| (original) | `1` | ++ + ### Configuring Action Text + + #### `config.action_text.attachment_tag_name` +--- a/activestorage/lib/active_storage/engine.rb ++++ b/activestorage/lib/active_storage/engine.rb +@@ -120,6 +120,7 @@ module ActiveStorage + ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2" + ActiveStorage.track_variants = app.config.active_storage.track_variants || false + ActiveStorage.streaming_chunk_max_size = app.config.active_storage.streaming_chunk_max_size || 100.megabytes ++ ActiveStorage.streaming_max_ranges = app.config.active_storage.streaming_max_ranges || 1 + end + end + diff -Nru rails-7.2.2.2+dfsg/debian/patches/CVE-2026-66066-block-untrusted-vips.patch rails-7.2.2.2+dfsg/debian/patches/CVE-2026-66066-block-untrusted-vips.patch --- rails-7.2.2.2+dfsg/debian/patches/CVE-2026-66066-block-untrusted-vips.patch 1970-01-01 00:00:00.000000000 +0000 +++ rails-7.2.2.2+dfsg/debian/patches/CVE-2026-66066-block-untrusted-vips.patch 2026-10-04 21:13:12.000000000 +0000 @@ -0,0 +1,336 @@ +Description: Disable libvips unfuzzed loaders and savers + Active Storage passed uploaded files to libvips operations that libvips + marks unfuzzed. Call Vips.block_untrusted(true) while booting when + ruby-vips is installed. BMP, ICO, and PSD variants then raise + Vips::Error. Attaching and downloading those files is unchanged. +Origin: upstream, https://github.com/rails/rails/commit/d79b7f4aa17dec8ce4960fef05733c8c0c7ef49a +Bug: https://github.com/rails/rails/security/advisories/GHSA-xr9x-r78c-5hrm +Bug-Debian: https://bugs.debian.org/1143080 +Author: Mike Dalessio +Applied-Upstream: d79b7f4aa17dec8ce4960fef05733c8c0c7ef49a +Last-Update: 2026-10-02 +--- a/activestorage/lib/active_storage/analyzer/image_analyzer/vips.rb ++++ b/activestorage/lib/active_storage/analyzer/image_analyzer/vips.rb +@@ -1,5 +1,7 @@ + # frozen_string_literal: true + ++require "active_storage/vips" ++ + module ActiveStorage + # This analyzer relies on the third-party {ruby-vips}[https://github.com/libvips/ruby-vips] gem. Ruby-vips requires + # the {libvips}[https://libvips.github.io/libvips/] system library. +--- /dev/null ++++ b/activestorage/lib/active_storage/vips.rb +@@ -0,0 +1,42 @@ ++# frozen_string_literal: true ++ ++require "active_storage" ++require "active_support/core_ext/string/filters" ++ ++begin ++ require "nokogiri" ++rescue LoadError ++ # Ensure nokogiri is loaded before vips, which also depends on libxml2. ++ # See Nokogiri RFC: Stop exporting symbols: ++ # https://github.com/sparklemotion/nokogiri/discussions/2746 ++end ++ ++begin ++ gem "ruby-vips" ++ require "ruby-vips" ++ ActiveStorage::VIPS_AVAILABLE = true # :nodoc: ++rescue LoadError => error ++ ActiveStorage::VIPS_AVAILABLE = false # :nodoc: ++ raise error unless error.message.match?(/libvips|ruby-vips/) ++end ++ ++if ActiveStorage::VIPS_AVAILABLE ++ begin ++ # image_processing 2.0 calls Vips.block_untrusted(true) itself when it loads, so it has to load ++ # before the lines below. Leaving it to load later, when the transformer first asks for it, ++ # would disable the loaders again after an application's initializers had re-enabled them. ++ require "image_processing/vips" ++ rescue LoadError ++ # image_processing is only needed to generate variants, not to analyze blobs. ++ end ++ ++ unless Vips.respond_to?(:block_untrusted) ++ raise <<~ERROR.squish ++ libvips's unfuzzed operations are not safe to use with untrusted content, and Active Storage ++ cannot disable them. Disabling them requires libvips 8.13 or later and ruby-vips 2.2.1 or ++ later. Please upgrade libvips and ruby-vips, or remove the ruby-vips gem from your Gemfile. ++ ERROR ++ end ++ ++ Vips.block_untrusted(true) ++end +--- a/activestorage/test/analyzer/image_analyzer/vips_test.rb ++++ b/activestorage/test/analyzer/image_analyzer/vips_test.rb +@@ -26,13 +26,26 @@ class ActiveStorage::Analyzer::ImageAnal + end + end + +- test "analyzing an SVG image without an XML declaration" do ++ test "analyzing an SVG image without an XML declaration is skipped because the SVG loader is disabled by default" do + analyze_with_vips do + blob = create_file_blob(filename: "icon.svg", content_type: "image/svg+xml") + metadata = extract_metadata_from(blob) + +- assert_equal 792, metadata[:width] +- assert_equal 584, metadata[:height] ++ assert_nil metadata[:width] ++ assert_nil metadata[:height] ++ end ++ end ++ ++ test "analyzing an SVG image without an XML declaration when the SVG loader is enabled" do ++ analyze_with_vips do ++ blob = create_file_blob(filename: "icon.svg", content_type: "image/svg+xml") ++ ++ with_vips_loaders_enabled(*VIPS_SVG_LOADERS) do ++ metadata = extract_metadata_from(blob) ++ ++ assert_equal 792, metadata[:width] ++ assert_equal 584, metadata[:height] ++ end + end + end + +--- a/activestorage/test/models/variant_test.rb ++++ b/activestorage/test/models/variant_test.rb +@@ -70,26 +70,48 @@ class ActiveStorage::VariantTest < Activ + assert_equal 100, image.height + end + +- test "resized variation of PSD blob" do ++ test "resized variation of PSD blob raises because the ImageMagick loader is disabled by default" do + blob = create_file_blob(filename: "icon.psd", content_type: "image/vnd.adobe.photoshop") +- variant = blob.variant(resize_to_limit: [20, 20]).processed +- assert_match(/icon\.png/, variant.url) + +- image = read_image(variant) +- assert_equal "PNG", image.type +- assert_equal 20, image.width +- assert_equal 20, image.height ++ assert_raises(Vips::Error) do ++ blob.variant(resize_to_limit: [20, 20]).processed ++ end + end + +- test "resized variation of ICO blob" do ++ test "resized variation of PSD blob when the ImageMagick loader is enabled" do ++ blob = create_file_blob(filename: "icon.psd", content_type: "image/vnd.adobe.photoshop") ++ ++ with_vips_loaders_enabled(*VIPS_MAGICK_LOADERS) do ++ variant = blob.variant(resize_to_limit: [20, 20]).processed ++ assert_match(/icon\.png/, variant.url) ++ ++ image = read_image(variant) ++ assert_equal "PNG", image.type ++ assert_equal 20, image.width ++ assert_equal 20, image.height ++ end ++ end ++ ++ test "resized variation of ICO blob raises because the ImageMagick loader is disabled by default" do + blob = create_file_blob(filename: "favicon.ico", content_type: "image/vnd.microsoft.icon") +- variant = blob.variant(resize_to_limit: [20, 20]).processed +- assert_match(/icon\.png/, variant.url) + +- image = read_image(variant) +- assert_equal "PNG", image.type +- assert_equal 20, image.width +- assert_equal 20, image.height ++ assert_raises(Vips::Error) do ++ blob.variant(resize_to_limit: [20, 20]).processed ++ end ++ end ++ ++ test "resized variation of ICO blob when the ImageMagick loader is enabled" do ++ blob = create_file_blob(filename: "favicon.ico", content_type: "image/vnd.microsoft.icon") ++ ++ with_vips_loaders_enabled(*VIPS_MAGICK_LOADERS) do ++ variant = blob.variant(resize_to_limit: [20, 20]).processed ++ assert_match(/icon\.png/, variant.url) ++ ++ image = read_image(variant) ++ assert_equal "PNG", image.type ++ assert_equal 20, image.width ++ assert_equal 20, image.height ++ end + end + + test "resized variation of TIFF blob" do +@@ -103,15 +125,26 @@ class ActiveStorage::VariantTest < Activ + assert_equal 33, image.height + end + +- test "resized variation of BMP blob" do ++ test "resized variation of BMP blob raises because the ImageMagick loader is disabled by default" do + blob = create_file_blob(filename: "colors.bmp", content_type: "image/bmp") +- variant = blob.variant(resize_to_limit: [15, 15]).processed +- assert_match(/colors\.png/, variant.url) + +- image = read_image(variant) +- assert_equal "PNG", image.type +- assert_equal 15, image.width +- assert_equal 8, image.height ++ assert_raises(Vips::Error) do ++ blob.variant(resize_to_limit: [15, 15]).processed ++ end ++ end ++ ++ test "resized variation of BMP blob when the ImageMagick loader is enabled" do ++ blob = create_file_blob(filename: "colors.bmp", content_type: "image/bmp") ++ ++ with_vips_loaders_enabled(*VIPS_MAGICK_LOADERS) do ++ variant = blob.variant(resize_to_limit: [15, 15]).processed ++ assert_match(/colors\.png/, variant.url) ++ ++ image = read_image(variant) ++ assert_equal "PNG", image.type ++ assert_equal 15, image.width ++ assert_equal 8, image.height ++ end + end + + test "resized variation of WEBP blob" do +--- a/activestorage/test/test_helper.rb ++++ b/activestorage/test/test_helper.rb +@@ -87,6 +87,33 @@ class ActiveSupport::TestCase + ActiveStorage::Blob.service = previous_service + end + ++ # libvips names its ImageMagick loader after the major version it was built ++ # against, and Vips.block does nothing for a name it cannot find, so pass both. ++ VIPS_MAGICK_LOADERS = %w( VipsForeignLoadMagick VipsForeignLoadMagick7 ).freeze ++ VIPS_SVG_LOADERS = %w( VipsForeignLoadSvg ).freeze ++ ++ # libvips can set an operation's blocked state but not read it, so probe once here, before any ++ # test has had the chance to change it, by opening a file that only an unfuzzed loader reads. ++ VIPS_LOADERS_DISABLED_AT_BOOT = ++ if defined?(::Vips) ++ begin ++ ::Vips::Image.new_from_file(File.expand_path("fixtures/files/colors.bmp", __dir__)) ++ false ++ rescue ::Vips::Error ++ true ++ end ++ else ++ true ++ end ++ ++ def with_vips_loaders_enabled(*class_names) ++ class_names.each { |class_name| Vips.block(class_name, false) } ++ ++ yield ++ ensure ++ class_names.each { |class_name| Vips.block(class_name, VIPS_LOADERS_DISABLED_AT_BOOT) } ++ end ++ + def with_strict_loading_by_default(&block) + strict_loading_was = ActiveRecord::Base.strict_loading_by_default + ActiveRecord::Base.strict_loading_by_default = true +--- a/guides/source/active_storage_overview.md ++++ b/guides/source/active_storage_overview.md +@@ -1024,6 +1024,51 @@ specific options will need to be updated + [Vips]: https://www.rubydoc.info/gems/ruby-vips/Vips/Image + [`image_processing`]: https://github.com/janko/image_processing + ++#### Disabled Vips Image Loaders and Savers ++ ++libvips marks some of its image loaders and savers as ++[unfuzzed](https://www.libvips.org/2022/05/28/What's-new-in-8.13.html#blocking-of-unfuzzed-loaders), ++meaning they should not be used to process untrusted content. Active Storage disables all of them ++while your application boots, before any initializer runs. ++ ++Doing so requires libvips 8.13 or later and ruby-vips 2.2.1 or later, which are Active Storage's ++minimum supported versions. When ruby-vips is installed and either minimum is not met, Active ++Storage raises a `RuntimeError` while booting rather than run in an unsecurable environment. Upgrade ++libvips and ruby-vips. ++ ++Notably, ImageMagick is marked as an unfuzzed loader and is disabled by default. On many platforms, ++libvips relies on ImageMagick to read BMP, ICO, and PSD files, so attachments in those formats ++cannot be transformed by default on those platforms. Attaching, storing, and downloading them is ++unaffected, but generating a variant raises `Vips::Error`, and analysis does not record their ++`width` and `height`. ++ ++Analysis also does not record dimensions for other types that libvips reads with an unfuzzed ++loader, such as SVG, JPEG XL, JPEG 2000, and Netpbm. Those types are not in the default ++`ActiveStorage.variable_content_types`, so they are not variable in the first place. ++ ++The savers marked unfuzzed are typically FITS, JXL, and ImageMagick, so requesting one of those as ++a variant's output format, such as `variant(format: :jxl)`, also raises `Vips::Error`. ++ ++An application that needs BMP, ICO, or PSD variants of trusted inputs can re-enable the ImageMagick ++loader in an initializer: ++ ++```ruby ++# config/initializers/vips.rb ++Vips.block("VipsForeignLoadMagick", false) # Note this is dangerous for untrusted content! ++``` ++ ++Operation names and which of them are marked unfuzzed are both platform-dependent, so run `vips -l` ++to see the class hierarchy for your build. ++ ++WARNING: Re-enabling unfuzzed image loaders and savers is dangerous. You should only attempt this ++after researching how your distribution builds libvips and which library delegates are enabled. ++ ++WARNING: Even re-enabling only the ImageMagick loader, as illustrated above, is dangerous. libvips ++delegates many more file types to ImageMagick than just BMP, ICO, and PSD, so re-enabling it exposes ++a much larger attack surface. Do this only when all image uploads are trusted content, and only ++alongside a strict [ImageMagick security ++policy](https://imagemagick.org/script/security-policy.php). ++ + ### Previewing Files + + Some non-image files can be previewed: that is, they can be presented as images. +--- a/activestorage/CHANGELOG.md ++++ b/activestorage/CHANGELOG.md +@@ -1,3 +1,46 @@ ++## security update 2:7.2.2.2+dfsg-2~deb13u2 ## ++ ++* Disable libvips's unfuzzed image loaders and savers. ++ ++ libvips flags some of its loaders and savers as "unfuzzed" or "untrusted", meaning they are only ++ safe for trusted content. Active Storage will call `Vips.block_untrusted(true)` to disable them ++ while booting. An application that needs a specific loader or saver may re-enable it in an ++ initializer. ++ ++ This is a breaking change for applications that process image types with an unfuzzed loader or ++ saver. Variant transformation of BMP, ICO, and PSD attachments will raise `Vips::Error`, and ++ analysis of these and other types such as SVG, JPEG XL, JPEG 2000, and Netpbm will no longer ++ record `width` and `height`. Requesting an unfuzzed output format, typically FITS, JXL, or ++ anything delegated to ImageMagick, will also raise `Vips::Error`. Attaching, storing, and ++ downloading are unchanged. ++ ++ An application seeing `Vips::Error` raised during image transformation may wish to remove the ++ affected content types from `config.active_storage.variable_content_types` in an initializer. ++ Active Storage will then treat those attachments as not variable and will not generate variants ++ for them. This most often matters to an application that transforms images during a request ++ rather than in a background job, where the failure surfaces as an error response instead of a ++ failed job. ++ ++ ```ruby ++ Rails.application.config.active_storage.variable_content_types -= ++ %w[ image/bmp image/vnd.microsoft.icon image/vnd.adobe.photoshop ] ++ ``` ++ ++ Applications using the `:mini_magick` variant processor will see no change in how their ++ attachments are processed, but the loaders and savers will be disabled process-wide whenever ++ ruby-vips is installed, and the version requirements below will still apply. Such an application ++ may remove ruby-vips from its Gemfile to avoid both. ++ ++ The minimum supported version of libvips is now 8.13, and the minimum supported version of ++ ruby-vips is now 2.2.1. These are the earliest versions that are capable of disabling untrusted ++ operations. When ruby-vips is installed and either minimum is not met, Active Storage will raise ++ a `RuntimeError` while booting rather than run in an unsecurable environment. ++ ++ [GHSA-xr9x-r78c-5hrm] ++ [CVE-2026-66066] ++ ++ *Mike Dalessio* ++ + ## Rails 7.2.2.2 (August 13, 2025) ## + + Remove dangerous transformations diff -Nru rails-7.2.2.2+dfsg/debian/patches/series rails-7.2.2.2+dfsg/debian/patches/series --- rails-7.2.2.2+dfsg/debian/patches/series 2025-12-01 20:45:40.000000000 +0000 +++ rails-7.2.2.2+dfsg/debian/patches/series 2026-10-04 21:13:12.000000000 +0000 @@ -19,3 +19,13 @@ #use-puma6.patch relax-irb.patch remove-sprockets-rails.patch +CVE-2026-66066-block-untrusted-vips.patch +CVE-2026-33168-skip-blank-attribute-names.patch +CVE-2026-33169-delimit-without-backtracking.patch +CVE-2026-33170-preserve-unsafe-safebuffer.patch +CVE-2026-33173-filter-direct-upload-metadata.patch +CVE-2026-33174-limit-streaming-chunk-size.patch +CVE-2026-33176-reject-scientific-notation.patch +CVE-2026-33195-reject-disk-path-traversal.patch +CVE-2026-33202-escape-disk-globs.patch +CVE-2026-33658-limit-streaming-ranges.patch