Hello, Bookworm rails is 2:6.1.7.10+dfsg-1~deb12u2. I put the seven no-dsa CVEs whose code is in Rails 6.1, and CVE-2026-66066 for applications that set the variant processor to :vips, into one update, 2:6.1.7.10+dfsg-1~deb12u3. The commits are pushed to Salsa and a debdiff is attached.
These are the ones I fixed:
- CVE-2026-66066: the vips processor calls vips_block_untrusted_set before it
opens an upload. ruby-vips 2.1.4 does not bind that function, so the patch
attaches it. BMP variants then raise Vips::Error. The MiniMagick default is
unchanged.
- CVE-2026-33168: a blank HTML attribute name skipped escaping. Blank names are
now skipped.
- CVE-2026-33169: delimiting a long digit string was quadratic. The default
path walks the digits. 1,234,567 still looks the same.
- CVE-2026-33170: formatting an unsafe SafeBuffer with % marked the result
safe. It stays unsafe.
- CVE-2026-33173: a direct upload could set analyzed and identified and skip
content-type detection. Those keys are dropped. Rails 6.1 has no composed flag.
- CVE-2026-33176: number helpers expanded scientific notation through BigDecimal.
number_to_currency("1E1000") stays "$1E1000".
- CVE-2026-33195: a disk key with ../ could leave the storage root. That now
raises InvalidKeyError.
- CVE-2026-33202: delete_prefixed passed the prefix to Dir.glob. Glob
metacharacters are escaped.
CVE-2026-66066 closes #1143080. Each of the other seven closes #1132035. That
bug was closed by 2:7.2.3.1+dfsg-1.
CVE-2026-33174 and CVE-2026-33658 stay out of this update. I asked the Security
Team to mark those two not-affected. There is no 6.1 commit for either of them.
CVE-2026-33174 is the proxy holding a whole Range in one string. On 7.2,
streaming.rb calls download_chunk, which does file.read(range.size). bytes=0-
on a large file is that allocation. Bookworm's blob proxy and representation
proxy do not have that file, and they do not read the Range header.
ActiveStorage::BaseController#stream writes the file out in 5MB chunks.
download_chunk exists, but outside the tests the only caller reads the first
4KB. The disk service URL does honor Range, through Rack::Files in ruby-rack
2.2.22, in 8KB reads. That is not this bug.
CVE-2026-33658 is the same proxy appending every range in the header onto one
buffer. Bookworm's proxy never calls Rack::Utils.get_byte_ranges. Rack::Files
on the disk URL does accept several ranges, and it walks them in 8KB reads
instead of building one string. There is no streaming_max_ranges method to
patch.
CVE-2026-66066 is in this update. The Bookworm default stays :mini_magick, and
the engine still loads image_analyzer.rb only. Setting the variant processor to
:vips used to leave libvips's unfuzzed loaders on, because ruby-vips 2.1.4
never calls vips_block_untrusted_set and image_processing 1.10.3 never calls
block_untrusted. The patch attaches that symbol from libvips 8.14.1 and calls
it when the vips processor runs. The 7.2 file calls Vips.block_untrusted and
raises at boot on 2.1.4, so this is not that file.
I built this in a Bookworm schroot. The packaged test suite was 344 runs and 0
failures on each of five runs, with 24 and 26 skips on the first two.
autopkgtest newapp and gem2deb-test-runner both passed. That runner only
executes Active Job, so on bookworm-testing I ran the same behavioral script as
Trixie, first on 2:6.1.7.10+dfsg-1~deb12u2 and then on this package.
On the archive package the seven were VULNERABLE in the same way as Trixie. A blank
attribute name emitted the onerror value. Delimiting 50,000 ones took 6.2 seconds. An
unsafe SafeBuffer became html_safe after %. number_to_currency("1E1000")
expanded to about 1,300 characters. path_for reached /etc/passwd. delete_prefixed with a
[1] prefix deleted the plain sibling and left the bracket key. Direct upload metadata was
not filtered. CVE-2026-33174 and CVE-2026-33658 were NOT_AFFECTED: there is no streaming
proxy. With the variant processor set to :vips, before the 66066 patch, an SVG and a BMP
both opened.
On this package those seven reported FIXED. 1,234,567 and $1,234.50 were
unchanged, the Indian delimiter pattern still worked, a blank attribute value
was kept, a safe buffer stayed safe, and avatars/123/photo stayed inside the
root. Traversal, blank, and null-byte keys were rejected. The bracket key was
the only one the glob prefix deleted. analyzed and identified were dropped and
the caption stayed. 50,000 ones took 0.005 seconds. CVE-2026-33174 and
CVE-2026-33658 were NOT_AFFECTED again. After the 66066 patch I ran the
variants through ImageProcessingTransformer#transform. PNG, JPEG, WebP, TIFF,
and GIF each resized to 8x6 as PNG, JPEG, and WebP. SVG, PPM, BMP, ICO, PSD,
and PDF were blocked. Saving a PNG variant as BMP was blocked. Twenty-five PNG
variants in one process still resized. The MiniMagick default still resized a
PNG and left svgload available.
Salsa pipeline: https://salsa.debian.org/ruby-team/rails/-/pipelines/1183706
Debusine work request:
https://debusine.debian.net/debian/developers/work-request/1418454/
Feedback is welcome. If nothing showstopping is raised, I plan to upload this
on the morning of Thursday, October 8th, 2026, or whenever the Trixie DSA is
issued for this set of CVEs, whatever comes later.
Best regards,
Simon Quigley
[email protected]
diff --git a/debian/changelog b/debian/changelog index 4930f272..07463773 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,37 @@ +rails (2:6.1.7.10+dfsg-1~deb12u3) bookworm-security; urgency=medium + + * Team upload. + * CVE-2026-66066: block libvips untrusted loaders when the variant + processor is :vips. BMP, ICO, and PSD variants then raise + Vips::Error. The MiniMagick default is unchanged (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 and identified 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-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). + + -- Simon Quigley <[email protected]> Sun, 04 Oct 2026 20:40:34 -0500 + rails (2:6.1.7.10+dfsg-1~deb12u2) bookworm-security; urgency=medium * Team upload diff --git a/debian/patches/CVE-2026-33168-skip-blank-attribute-names.patch b/debian/patches/CVE-2026-33168-skip-blank-attribute-names.patch new file mode 100644 index 00000000..388e50c3 --- /dev/null +++ b/debian/patches/CVE-2026-33168-skip-blank-attribute-names.patch @@ -0,0 +1,61 @@ +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. Rails 6.1 has the same + tag_options shape as the 7.2 fix. +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 <[email protected]> +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 +@@ -80,16 +80,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 +@@ -77,6 +77,22 @@ class TagHelperTest < ActionView::TestCa + assert_equal "<p included=\"\" />", tag("p", included: "") + end + ++ def test_tag_options_rejects_blank_key ++ assert_equal "<p />", tag("p", "" => "value") ++ assert_equal "<p />", tag("p", nil => "value") ++ assert_equal '<p class="a" />', tag("p", "" => "value", "class" => "a") ++ end ++ ++ def test_tag_options_rejects_blank_data_key ++ assert_equal "<p />", tag("p", data: { "" => "value" }) ++ assert_equal '<p data-x="y" />', tag("p", data: { "" => "value", "x" => "y" }) ++ end ++ ++ def test_tag_options_rejects_blank_aria_key ++ assert_equal "<p />", tag("p", aria: { "" => "value" }) ++ assert_equal '<p aria-x="y" />', tag("p", aria: { "" => "value", "x" => "y" }) ++ end ++ + def test_tag_builder_options_accepts_blank_option + assert_equal "<p included=\"\"></p>", tag.p(included: "") + end diff --git a/debian/patches/CVE-2026-33169-delimit-without-backtracking.patch b/debian/patches/CVE-2026-33169-delimit-without-backtracking.patch new file mode 100644 index 00000000..a022df65 --- /dev/null +++ b/debian/patches/CVE-2026-33169-delimit-without-backtracking.patch @@ -0,0 +1,67 @@ +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. + 33fbedb1 is included so the linear path is the default. +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 <[email protected]> +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 +@@ -118,6 +118,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 --git a/debian/patches/CVE-2026-33170-preserve-unsafe-safebuffer.patch b/debian/patches/CVE-2026-33170-preserve-unsafe-safebuffer.patch new file mode 100644 index 00000000..e3807aa1 --- /dev/null +++ b/debian/patches/CVE-2026-33170-preserve-unsafe-safebuffer.patch @@ -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. Rails 6.1 SafeBuffer#% did not copy the flag. +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 <[email protected]> +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 +@@ -259,7 +259,9 @@ module ActiveSupport #:nodoc: + escaped_args = Array(args).map { |arg| 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 + + def 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 = "<br />".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 = "<br />%{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 --git a/debian/patches/CVE-2026-33173-filter-direct-upload-metadata.patch b/debian/patches/CVE-2026-33173-filter-direct-upload-metadata.patch new file mode 100644 index 00000000..09b0e038 --- /dev/null +++ b/debian/patches/CVE-2026-33173-filter-direct-upload-metadata.patch @@ -0,0 +1,64 @@ +Description: Drop internal keys from direct-upload metadata + analyzed and identified are Active Storage state on Rails 6.1. + composed is not a metadata accessor in this version. A direct upload + can no longer set the internal keys. Other metadata keys are kept. + Hash#except is used because Rails 6.1 has no Hash#without. +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 <[email protected]> +Applied-Upstream: 707c0f1f41f067fdf96d54e99d43b28dfaae7e53 +Last-Update: 2026-10-04 +--- +--- a/activestorage/app/models/active_storage/blob.rb ++++ b/activestorage/app/models/active_storage/blob.rb +@@ -39,6 +39,10 @@ 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) ++ private_constant :PROTECTED_METADATA + store :metadata, accessors: [ :analyzed, :identified ], coder: ActiveRecord::Coders::JSON + + class_attribute :services, default: {} +@@ -124,6 +128,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 + +@@ -148,6 +153,12 @@ class ActiveStorage::Blob < ActiveStorag + def signed_id_verifier #:nodoc: + @signed_id_verifier ||= ActiveStorage.verifier + end ++ ++ private ++ def filter_metadata(metadata) ++ return metadata unless metadata.is_a?(Hash) ++ metadata.except(*PROTECTED_METADATA, *PROTECTED_METADATA.map(&:to_sym)) ++ end + end + + # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering. +--- a/activestorage/test/controllers/direct_uploads_controller_test.rb ++++ b/activestorage/test/controllers/direct_uploads_controller_test.rb +@@ -139,8 +139,14 @@ class ActiveStorage::DiskDirectUploadsCo + "library_ID": "12345" + } + ++ protected_metadata = { ++ "analyzed": "yolo", ++ "identified": 42, ++ } ++ 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 --git a/debian/patches/CVE-2026-33176-reject-scientific-notation.patch b/debian/patches/CVE-2026-33176-reject-scientific-notation.patch new file mode 100644 index 00000000..92c2afe4 --- /dev/null +++ b/debian/patches/CVE-2026-33176-reject-scientific-notation.patch @@ -0,0 +1,51 @@ +Description: Do not expand scientific notation in number helpers + Rails 6.1 parses helper input with Float and then BigDecimal, not the + 7.2 valid_bigdecimal gate. Strings containing d or e notation now stay + text, so a short string cannot expand into a huge number. 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 <[email protected]> +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 +@@ -174,6 +174,7 @@ module ActiveSupport + end + + def valid_float? ++ return false if number.is_a?(String) && number.match?(/[de]/i) + Float(number) + rescue ArgumentError, TypeError + false +--- a/activesupport/lib/active_support/number_helper/rounding_helper.rb ++++ b/activesupport/lib/active_support/number_helper/rounding_helper.rb +@@ -26,6 +26,9 @@ module ActiveSupport + def convert_to_decimal(number) + case number + when Float, String ++ if number.is_a?(String) && number.match?(/[de]/i) ++ raise ArgumentError, "scientific notation is not accepted" ++ end + BigDecimal(number.to_s) + when Rational + BigDecimal(number, digit_count(number.to_i) + options[:precision]) +--- a/activesupport/test/number_helper_test.rb ++++ b/activesupport/test/number_helper_test.rb +@@ -424,6 +424,14 @@ 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 "$1E1000", number_helper.number_to_currency("1E1000") ++ assert_equal "1E1000%", number_helper.number_to_percentage("1E1000") ++ assert_equal "$123481223d98989", number_helper.number_to_currency("123481223d98989") ++ end ++ end + end + end + end diff --git a/debian/patches/CVE-2026-33195-reject-disk-path-traversal.patch b/debian/patches/CVE-2026-33195-reject-disk-path-traversal.patch new file mode 100644 index 00000000..b343d57e --- /dev/null +++ b/debian/patches/CVE-2026-33195-reject-disk-path-traversal.patch @@ -0,0 +1,88 @@ +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 <[email protected]> +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 +@@ -31,6 +33,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 +@@ -14,6 +14,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 + # We use constant paths in the following include calls to avoid a gotcha of + # classic mode: If the parent application defines a top-level Analyzable, for +--- 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 +@@ -99,7 +99,29 @@ module ActiveStorage + end + + def path_for(key) #:nodoc: +- File.join root, folder_for(key), key ++ if key.blank? ++ raise ActiveStorage::InvalidKeyError, "key is blank" ++ end ++ ++ 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 ++ raise ActiveStorage::InvalidKeyError, "key is an invalid string" ++ end ++ ++ unless path.start_with?(File.expand_path(root) + "/") ++ raise ActiveStorage::InvalidKeyError, "key is outside of disk service root" ++ end ++ ++ path + end + + private diff --git a/debian/patches/CVE-2026-33202-escape-disk-globs.patch b/debian/patches/CVE-2026-33202-escape-disk-globs.patch new file mode 100644 index 00000000..004c3255 --- /dev/null +++ b/debian/patches/CVE-2026-33202-escape-disk-globs.patch @@ -0,0 +1,36 @@ +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 <[email protected]> +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 +@@ -58,7 +58,10 @@ 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) ++ prefix_path += "/" if prefix.end_with?("/") ++ escaped = escape_glob_metacharacters(prefix_path) ++ Dir.glob("#{escaped}*").each do |path| + FileUtils.rm_rf(path) + end + end +@@ -171,6 +174,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 diff --git a/debian/patches/CVE-2026-66066-block-untrusted-vips.patch b/debian/patches/CVE-2026-66066-block-untrusted-vips.patch new file mode 100644 index 00000000..4fe5ed18 --- /dev/null +++ b/debian/patches/CVE-2026-66066-block-untrusted-vips.patch @@ -0,0 +1,47 @@ +Description: Block libvips untrusted loaders when the vips processor is used + Rails 6.1 defaults to MiniMagick and has no vips analyzer. An application + that sets the variant processor to :vips still passes uploads to libvips. + ruby-vips 2.1.4 does not bind vips_block_untrusted_set, so attach that + symbol and call it before the first vips variant. BMP, ICO, and PSD + variants then raise Vips::Error. The MiniMagick default is unchanged. +Origin: vendor, based on 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: Simon Quigley <[email protected]> +Last-Update: 2026-10-04 +--- +--- a/activestorage/lib/active_storage/transformers/image_processing_transformer.rb ++++ b/activestorage/lib/active_storage/transformers/image_processing_transformer.rb +@@ -26,7 +26,31 @@ module ActiveStorage + end + + def processor +- ImageProcessing.const_get(ActiveStorage.variant_processor.to_s.camelize) ++ name = ActiveStorage.variant_processor ++ klass = ImageProcessing.const_get(name.to_s.camelize) ++ disable_libvips_untrusted_loaders if name.to_s == "vips" ++ klass ++ end ++ ++ # ruby-vips 2.1.4 does not bind vips_block_untrusted_set. libvips 8.13 ++ # and newer export it. Call it once the vips processor is loaded, and ++ # before that processor opens an upload. The MiniMagick default never ++ # reaches this method. ++ def disable_libvips_untrusted_loaders ++ return if self.class.instance_variable_get(:@libvips_untrusted_blocked) ++ ++ require "vips" ++ ++ if Vips.respond_to?(:block_untrusted) ++ Vips.block_untrusted(true) ++ else ++ unless Vips.respond_to?(:vips_block_untrusted_set) ++ Vips.attach_function :vips_block_untrusted_set, [:int], :void ++ end ++ Vips.vips_block_untrusted_set(1) ++ end ++ ++ self.class.instance_variable_set(:@libvips_untrusted_blocked, true) + end + + def operations diff --git a/debian/patches/series b/debian/patches/series index 751fd81a..91d78e1d 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -20,3 +20,11 @@ rails-ruby3.1.patch CVE-2024-54133.patch CVE-2025-24293.patch CVE-2025-55193.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-33176-reject-scientific-notation.patch +CVE-2026-33195-reject-disk-path-traversal.patch +CVE-2026-33202-escape-disk-globs.patch
OpenPGP_signature.asc
Description: OpenPGP digital signature
