activestorage 7.2.3 → 7.2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94213ce5408fd1a4c67c6bf4854d2f2c55289813ef0297dfc33fb3325ec5dfd4
4
- data.tar.gz: 73de432409f6bb50456de14131e01c649b0172fcd5c0d5178b61b385cc236e37
3
+ metadata.gz: 8d9ba18be252a1779cf9e9bb80dfdea0a23601c5e396c559285f0dac62a93d68
4
+ data.tar.gz: f2d0022eb4a41e9e22b94dee0acbfe65d8983b6c74408d3cd4480949064136d4
5
5
  SHA512:
6
- metadata.gz: 48271c643e3a2a6ffbabdc398863f9452d3cdc2c01540b94a28f512fcc828d1792d13fd36ec05c890e3b02dc7d6fc33439c792eebbda2ab720479032307c947a
7
- data.tar.gz: 65acfc493d0aa029d4b533dd086cdeb9e35c04c6b4b4b6ee678101516bfc9428ffc477be92d5d49c5c1f6f43f6bed29b2c7ccbacf11f650e27fae34a779bb672
6
+ metadata.gz: 6a75369b13ef789b1b68cf8f6ec54b91b94f09208fae23c2df39d5bdf9c2f0f8f078fcea74013235d737ac4bfb618517618694f18f473bb2807612c623b44411
7
+ data.tar.gz: 2b9bdd8c74524d0669a42330326253a0d21f00a3a6ab560db46f68d3ef7824cb8e858a6ac225de29932ad3755af238708a7ddbbda8562e07764b4208d204e7d8
data/CHANGELOG.md CHANGED
@@ -1,3 +1,97 @@
1
+ ## Rails 7.2.3.2 (July 29, 2026) ##
2
+
3
+ * Disable libvips's unfuzzed image loaders and savers.
4
+
5
+ libvips flags some of its loaders and savers as "unfuzzed" or "untrusted", meaning they are only
6
+ safe for trusted content. Active Storage will call `Vips.block_untrusted(true)` to disable them
7
+ while booting. An application that needs a specific loader or saver may re-enable it in an
8
+ initializer.
9
+
10
+ This is a breaking change for applications that process image types with an unfuzzed loader or
11
+ saver. Variant transformation of BMP, ICO, and PSD attachments will raise `Vips::Error`, and
12
+ analysis of these and other types such as SVG, JPEG XL, JPEG 2000, and Netpbm will no longer
13
+ record `width` and `height`. Requesting an unfuzzed output format, typically FITS, JXL, or
14
+ anything delegated to ImageMagick, will also raise `Vips::Error`. Attaching, storing, and
15
+ downloading are unchanged.
16
+
17
+ An application seeing `Vips::Error` raised during image transformation may wish to remove the
18
+ affected content types from `config.active_storage.variable_content_types` in an initializer.
19
+ Active Storage will then treat those attachments as not variable and will not generate variants
20
+ for them. This most often matters to an application that transforms images during a request
21
+ rather than in a background job, where the failure surfaces as an error response instead of a
22
+ failed job.
23
+
24
+ ```ruby
25
+ Rails.application.config.active_storage.variable_content_types -=
26
+ %w[ image/bmp image/vnd.microsoft.icon image/vnd.adobe.photoshop ]
27
+ ```
28
+
29
+ Applications using the `:mini_magick` variant processor will see no change in how their
30
+ attachments are processed, but the loaders and savers will be disabled process-wide whenever
31
+ ruby-vips is installed, and the version requirements below will still apply. Such an application
32
+ may remove ruby-vips from its Gemfile to avoid both.
33
+
34
+ The minimum supported version of libvips is now 8.13, and the minimum supported version of
35
+ ruby-vips is now 2.2.1. These are the earliest versions that are capable of disabling untrusted
36
+ operations. When ruby-vips is installed and either minimum is not met, Active Storage will raise
37
+ a `RuntimeError` while booting rather than run in an unsecurable environment.
38
+
39
+ [GHSA-xr9x-r78c-5hrm]
40
+ [CVE-2026-66066]
41
+
42
+ *Mike Dalessio*
43
+
44
+ ## Rails 7.2.3.1 (March 23, 2026) ##
45
+
46
+ * Filter user supplied metadata in DirectUploadController
47
+
48
+ [CVE-2026-33173]
49
+
50
+ *Jean Boussier*
51
+
52
+ * Configurable maxmimum streaming chunk size
53
+
54
+ Makes sure that byte ranges for blobs don't exceed 100mb by default.
55
+ Content ranges that are too big can result in denial of service.
56
+
57
+ [CVE-2026-33174]
58
+
59
+ *Gannon McGibbon*
60
+
61
+ * Limit range requests to a single range
62
+
63
+ [CVE-2026-33658]
64
+
65
+ *Jean Boussier*
66
+
67
+ * Prevent path traversal in `DiskService`.
68
+
69
+ `DiskService#path_for` now raises an `InvalidKeyError` when passed keys with dot segments (".",
70
+ ".."), or if the resolved path is outside the storage root directory.
71
+
72
+ `#path_for` also now consistently raises `InvalidKeyError` if the key is invalid in any way, for
73
+ example containing null bytes or having an incompatible encoding. Previously, the exception
74
+ raised may have been `ArgumentError` or `Encoding::CompatibilityError`.
75
+
76
+ `DiskController` now explicitly rescues `InvalidKeyError` with appropriate HTTP status codes.
77
+
78
+ [CVE-2026-33195]
79
+
80
+ *Mike Dalessio*
81
+
82
+ * Prevent glob injection in `DiskService#delete_prefixed`.
83
+
84
+ Escape glob metacharacters in the resolved path before passing to `Dir.glob`.
85
+
86
+ Note that this change breaks any existing code that is relying on `delete_prefixed` to expand
87
+ glob metacharacters. This change presumes that is unintended behavior (as other storage services
88
+ do not respect these metacharacters).
89
+
90
+ [CVE-2026-33202]
91
+
92
+ *Mike Dalessio*
93
+
94
+
1
95
  ## Rails 7.2.3 (October 28, 2025) ##
2
96
 
3
97
  * Fix `config.active_storage.touch_attachment_records` to work with eager loading.
@@ -17,6 +17,8 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
17
17
  end
18
18
  rescue Errno::ENOENT
19
19
  head :not_found
20
+ rescue ActiveStorage::InvalidKeyError
21
+ head :not_found
20
22
  end
21
23
 
22
24
  def update
@@ -32,6 +34,8 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
32
34
  end
33
35
  rescue ActiveStorage::IntegrityError
34
36
  head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
37
+ rescue ActiveStorage::InvalidKeyError
38
+ head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
35
39
  end
36
40
 
37
41
  private
@@ -14,7 +14,8 @@ module ActiveStorage::Streaming
14
14
  def send_blob_byte_range_data(blob, range_header, disposition: nil)
15
15
  ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)
16
16
 
17
- return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)
17
+ return head(:range_not_satisfiable) unless ranges_valid?(ranges)
18
+ return head(:range_not_satisfiable) if ranges.length > ActiveStorage.streaming_max_ranges
18
19
 
19
20
  if ranges.length == 1
20
21
  range = ranges.first
@@ -51,6 +52,12 @@ module ActiveStorage::Streaming
51
52
  )
52
53
  end
53
54
 
55
+ def ranges_valid?(ranges)
56
+ return false if ranges.blank? || ranges.all?(&:blank?)
57
+
58
+ ranges.sum { |range| range.end - range.begin } < ActiveStorage.streaming_chunk_max_size
59
+ end
60
+
54
61
  # Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
55
62
  # The content type and filename is set directly from the +blob+.
56
63
  def send_blob_stream(blob, disposition: nil) # :doc:
@@ -16,10 +16,18 @@
16
16
  # Blobs are intended to be immutable in as-so-far as their reference to a specific file goes. You're allowed to
17
17
  # update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
18
18
  # If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
19
+ #
20
+ # When using a custom +key+, the value is treated as trusted. Using untrusted user input
21
+ # as the key may result in unexpected behavior.
19
22
  class ActiveStorage::Blob < ActiveStorage::Record
20
23
  MINIMUM_TOKEN_LENGTH = 28
21
24
 
22
25
  has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
26
+
27
+ # FIXME: these property should never have been stored in the metadata.
28
+ # The blob table should be migrated to have dedicated columns for theses.
29
+ PROTECTED_METADATA = %w(analyzed identified composed)
30
+ private_constant :PROTECTED_METADATA
23
31
  store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
24
32
 
25
33
  class_attribute :services, default: {}
@@ -93,6 +101,9 @@ class ActiveStorage::Blob < ActiveStorage::Record
93
101
  # be saved before the upload begins to prevent the upload clobbering another due to key collisions.
94
102
  # When providing a content type, pass <tt>identify: false</tt> to bypass
95
103
  # automatic content type inference.
104
+ #
105
+ # The optional +key+ parameter is treated as trusted. Using untrusted user input
106
+ # as the key may result in unexpected behavior.
96
107
  def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
97
108
  create_after_unfurling!(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap do |blob|
98
109
  blob.upload_without_unfurling(io)
@@ -105,6 +116,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
105
116
  # Once the form using the direct upload is submitted, the blob can be associated with the right record using
106
117
  # the signed ID.
107
118
  def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
119
+ metadata = filter_metadata(metadata)
108
120
  create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name
109
121
  end
110
122
 
@@ -152,6 +164,15 @@ class ActiveStorage::Blob < ActiveStorage::Record
152
164
  combined_blob.save!
153
165
  end
154
166
  end
167
+
168
+ private
169
+ def filter_metadata(metadata)
170
+ if metadata.is_a?(Hash)
171
+ metadata.without(*PROTECTED_METADATA)
172
+ else
173
+ metadata
174
+ end
175
+ end
155
176
  end
156
177
 
157
178
  include Analyzable
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_storage/vips"
4
+
3
5
  module ActiveStorage
4
6
  # This analyzer relies on the third-party {ruby-vips}[https://github.com/libvips/ruby-vips] gem. Ruby-vips requires
5
7
  # the {libvips}[https://libvips.github.io/libvips/] system library.
@@ -122,6 +122,7 @@ module ActiveStorage
122
122
  ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
123
123
  ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
124
124
  ActiveStorage.track_variants = app.config.active_storage.track_variants || false
125
+ ActiveStorage.streaming_chunk_max_size = app.config.active_storage.streaming_chunk_max_size || 100.megabytes
125
126
  end
126
127
  end
127
128
 
@@ -26,4 +26,8 @@ module ActiveStorage
26
26
 
27
27
  # Raised when a Previewer is unable to generate a preview image.
28
28
  class PreviewError < Error; end
29
+
30
+ # Raised when a storage key resolves to a path outside the service's root
31
+ # directory, indicating a potential path traversal attack.
32
+ class InvalidKeyError < Error; end
29
33
  end
@@ -10,7 +10,7 @@ module ActiveStorage
10
10
  MAJOR = 7
11
11
  MINOR = 2
12
12
  TINY = 3
13
- PRE = nil
13
+ PRE = "2"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -60,7 +60,16 @@ module ActiveStorage
60
60
 
61
61
  def delete_prefixed(prefix)
62
62
  instrument :delete_prefixed, prefix: prefix do
63
- Dir.glob(path_for("#{prefix}*")).each do |path|
63
+ prefix_path = path_for(prefix)
64
+
65
+ # File.expand_path (called within path_for) strips trailing slashes.
66
+ # Restore trailing separator if the original prefix had one, so that
67
+ # the glob "prefix/*" matches files inside the directory, not siblings
68
+ # whose names start with the prefix string.
69
+ prefix_path += "/" if prefix.end_with?("/")
70
+
71
+ escaped = escape_glob_metacharacters(prefix_path)
72
+ Dir.glob("#{escaped}*").each do |path|
64
73
  FileUtils.rm_rf(path)
65
74
  end
66
75
  end
@@ -98,8 +107,39 @@ module ActiveStorage
98
107
  { "Content-Type" => content_type }
99
108
  end
100
109
 
110
+ # Every filesystem operation in DiskService resolves paths through this method (or through
111
+ # make_path_for, which delegates here). This is the primary filesystem security check: all
112
+ # path-traversal protection is enforced here. New methods that touch the filesystem MUST use
113
+ # path_for or make_path_for -- never construct paths from +root+ directly.
101
114
  def path_for(key) # :nodoc:
102
- File.join root, folder_for(key), key
115
+ if key.blank?
116
+ raise ActiveStorage::InvalidKeyError, "key is blank"
117
+ end
118
+
119
+ # Reject keys with dot segments as defense in depth. This prevents path traversal both outside
120
+ # and within the storage root. The root containment check below is a more fundamental check on
121
+ # path traversal outside of the disk service root.
122
+ begin
123
+ if key.split("/").intersect?(%w[. ..])
124
+ raise ActiveStorage::InvalidKeyError, "key has path traversal segments"
125
+ end
126
+ rescue Encoding::CompatibilityError
127
+ raise ActiveStorage::InvalidKeyError, "key has incompatible encoding"
128
+ end
129
+
130
+ begin
131
+ path = File.expand_path(File.join(root, folder_for(key), key))
132
+ rescue ArgumentError
133
+ # ArgumentError catches null bytes
134
+ raise ActiveStorage::InvalidKeyError, "key is an invalid string"
135
+ end
136
+
137
+ # The resolved path must be inside the root directory.
138
+ unless path.start_with?(File.expand_path(root) + "/")
139
+ raise ActiveStorage::InvalidKeyError, "key is outside of disk service root"
140
+ end
141
+
142
+ path
103
143
  end
104
144
 
105
145
  def compose(source_keys, destination_key, **)
@@ -156,6 +196,10 @@ module ActiveStorage
156
196
  [ key[0..1], key[2..3] ].join("/")
157
197
  end
158
198
 
199
+ def escape_glob_metacharacters(path)
200
+ path.gsub(/[\[\]*?{}\\]/) { |c| "\\#{c}" }
201
+ end
202
+
159
203
  def make_path_for(key)
160
204
  path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }
161
205
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_storage"
4
+ require "active_support/core_ext/string/filters"
5
+
6
+ begin
7
+ require "nokogiri"
8
+ rescue LoadError
9
+ # Ensure nokogiri is loaded before vips, which also depends on libxml2.
10
+ # See Nokogiri RFC: Stop exporting symbols:
11
+ # https://github.com/sparklemotion/nokogiri/discussions/2746
12
+ end
13
+
14
+ begin
15
+ gem "ruby-vips"
16
+ require "ruby-vips"
17
+ ActiveStorage::VIPS_AVAILABLE = true # :nodoc:
18
+ rescue LoadError => error
19
+ ActiveStorage::VIPS_AVAILABLE = false # :nodoc:
20
+ raise error unless error.message.match?(/libvips|ruby-vips/)
21
+ end
22
+
23
+ if ActiveStorage::VIPS_AVAILABLE
24
+ begin
25
+ # image_processing 2.0 calls Vips.block_untrusted(true) itself when it loads, so it has to load
26
+ # before the lines below. Leaving it to load later, when the transformer first asks for it,
27
+ # would disable the loaders again after an application's initializers had re-enabled them.
28
+ require "image_processing/vips"
29
+ rescue LoadError
30
+ # image_processing is only needed to generate variants, not to analyze blobs.
31
+ end
32
+
33
+ unless Vips.respond_to?(:block_untrusted)
34
+ raise <<~ERROR.squish
35
+ libvips's unfuzzed operations are not safe to use with untrusted content, and Active Storage
36
+ cannot disable them. Disabling them requires libvips 8.13 or later and ruby-vips 2.2.1 or
37
+ later. Please upgrade libvips and ruby-vips, or remove the ruby-vips gem from your Gemfile.
38
+ ERROR
39
+ end
40
+
41
+ Vips.block_untrusted(true)
42
+ end
@@ -27,6 +27,7 @@ require "active_record"
27
27
  require "active_support"
28
28
  require "active_support/rails"
29
29
  require "active_support/core_ext/numeric/time"
30
+ require "active_support/core_ext/numeric/bytes"
30
31
 
31
32
  require "active_storage/version"
32
33
  require "active_storage/deprecator"
@@ -350,6 +351,7 @@ module ActiveStorage
350
351
  ]
351
352
  mattr_accessor :unsupported_image_processing_arguments
352
353
 
354
+ mattr_accessor :streaming_chunk_max_size, default: 100.megabytes
353
355
  mattr_accessor :service_urls_expire_in, default: 5.minutes
354
356
  mattr_accessor :touch_attachment_records, default: true
355
357
  mattr_accessor :urls_expire_in
@@ -360,6 +362,18 @@ module ActiveStorage
360
362
 
361
363
  mattr_accessor :track_variants, default: false
362
364
 
365
+ singleton_class.attr_accessor :checksum_implementation
366
+ @checksum_implementation = OpenSSL::Digest::MD5
367
+ begin
368
+ @checksum_implementation.hexdigest("test")
369
+ rescue # OpenSSL may have MD5 disabled
370
+ require "digest/md5"
371
+ @checksum_implementation = Digest::MD5
372
+ end
373
+
374
+ singleton_class.attr_accessor :streaming_max_ranges
375
+ @streaming_max_ranges = 1
376
+
363
377
  mattr_accessor :video_preview_arguments, default: "-y -vframes 1 -f image2"
364
378
 
365
379
  module Transformers
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activestorage
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.3
4
+ version: 7.2.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
@@ -15,56 +15,56 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 7.2.3
18
+ version: 7.2.3.2
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 7.2.3
25
+ version: 7.2.3.2
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: actionpack
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 7.2.3
32
+ version: 7.2.3.2
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 7.2.3
39
+ version: 7.2.3.2
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: activejob
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: 7.2.3
46
+ version: 7.2.3.2
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 7.2.3
53
+ version: 7.2.3.2
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: activerecord
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - '='
59
59
  - !ruby/object:Gem::Version
60
- version: 7.2.3
60
+ version: 7.2.3.2
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - '='
66
66
  - !ruby/object:Gem::Version
67
- version: 7.2.3
67
+ version: 7.2.3.2
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: marcel
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -183,16 +183,17 @@ files:
183
183
  - lib/active_storage/transformers/image_processing_transformer.rb
184
184
  - lib/active_storage/transformers/transformer.rb
185
185
  - lib/active_storage/version.rb
186
+ - lib/active_storage/vips.rb
186
187
  - lib/tasks/activestorage.rake
187
188
  homepage: https://rubyonrails.org
188
189
  licenses:
189
190
  - MIT
190
191
  metadata:
191
192
  bug_tracker_uri: https://github.com/rails/rails/issues
192
- changelog_uri: https://github.com/rails/rails/blob/v7.2.3/activestorage/CHANGELOG.md
193
- documentation_uri: https://api.rubyonrails.org/v7.2.3/
193
+ changelog_uri: https://github.com/rails/rails/blob/v7.2.3.2/activestorage/CHANGELOG.md
194
+ documentation_uri: https://api.rubyonrails.org/v7.2.3.2/
194
195
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
195
- source_code_uri: https://github.com/rails/rails/tree/v7.2.3/activestorage
196
+ source_code_uri: https://github.com/rails/rails/tree/v7.2.3.2/activestorage
196
197
  rubygems_mfa_required: 'true'
197
198
  rdoc_options: []
198
199
  require_paths:
@@ -208,7 +209,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
208
209
  - !ruby/object:Gem::Version
209
210
  version: '0'
210
211
  requirements: []
211
- rubygems_version: 3.6.9
212
+ rubygems_version: 4.0.16
212
213
  specification_version: 4
213
214
  summary: Local and cloud file storage framework.
214
215
  test_files: []