activestorage 8.1.2 → 8.1.2.1

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: b700aaa6ff149a5ce6ff88794c94141d8d7e0895c6561578756727ecf5a3128b
4
- data.tar.gz: 2eecb99972e95b8495aafbf2458d775bae6a7bcabf4abbc1fc0dd2286b1b140e
3
+ metadata.gz: 95a8a27a89ab0f4be63e4f466fcf5a7eedf15ed806d7b061257ab56aafadc1a6
4
+ data.tar.gz: 4f7046ccc16df5fb3bd06080e914fb54c0ad2009d5dbcb1080dcab7bf945f1e3
5
5
  SHA512:
6
- metadata.gz: ddb19a88f6c95a4be6d198a7342e830a53d03a24c2f62e63ae2b41d1a94cb863dde34079d168a90e7c661736974691395605f991987ade4ff5164e187ad860e4
7
- data.tar.gz: 3499297b88322fa207ace2e9f140a53f678f88c7fbe06646d2924c2b29c17e1d8fd8723058382310d307960ba3813733fb5848f232f17c868f09c2d75be6b373
6
+ metadata.gz: f187b18f73b712921f1dc2252fe5a645ce83b84dad1dc97d6b24be9f136a634e2b0da3290d2a8f9284ae6134bfc8b62fc8e7a7b9f4974b9f3dee6a6ed0af17aa
7
+ data.tar.gz: 24b2da33d24f7f2d34623482b2a2c818e3d87d7dfa8a165e1ff6091134548e37b8dc9eb3908a69231d3e8e9fa3320f75db1a0dabcb54b048e4e543319ec2f555
data/CHANGELOG.md CHANGED
@@ -1,3 +1,55 @@
1
+ ## Rails 8.1.2.1 (March 23, 2026) ##
2
+
3
+ * Filter user supplied metadata in DirectUploadController
4
+
5
+ [CVE-2026-33173]
6
+
7
+ *Jean Boussier*
8
+
9
+ * Configurable maxmimum streaming chunk size
10
+
11
+ Makes sure that byte ranges for blobs don't exceed 100mb by default.
12
+ Content ranges that are too big can result in denial of service.
13
+
14
+ [CVE-2026-33174]
15
+
16
+ *Gannon McGibbon*
17
+
18
+ * Limit range requests to a single range
19
+
20
+ [CVE-2026-33658]
21
+
22
+ *Jean Boussier*
23
+
24
+
25
+ * Prevent path traversal in `DiskService`.
26
+
27
+ `DiskService#path_for` now raises an `InvalidKeyError` when passed keys with dot segments (".",
28
+ ".."), or if the resolved path is outside the storage root directory.
29
+
30
+ `#path_for` also now consistently raises `InvalidKeyError` if the key is invalid in any way, for
31
+ example containing null bytes or having an incompatible encoding. Previously, the exception
32
+ raised may have been `ArgumentError` or `Encoding::CompatibilityError`.
33
+
34
+ `DiskController` now explicitly rescues `InvalidKeyError` with appropriate HTTP status codes.
35
+
36
+ [CVE-2026-33195]
37
+
38
+ *Mike Dalessio*
39
+
40
+ * Prevent glob injection in `DiskService#delete_prefixed`.
41
+
42
+ Escape glob metacharacters in the resolved path before passing to `Dir.glob`.
43
+
44
+ Note that this change breaks any existing code that is relying on `delete_prefixed` to expand
45
+ glob metacharacters. This change presumes that is unintended behavior (as other storage services
46
+ do not respect these metacharacters).
47
+
48
+ [CVE-2026-33202]
49
+
50
+ *Mike Dalessio*
51
+
52
+
1
53
  ## Rails 8.1.2 (January 08, 2026) ##
2
54
 
3
55
  * Restore ADC when signing URLs with IAM for GCS
@@ -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: {}
@@ -92,6 +100,9 @@ class ActiveStorage::Blob < ActiveStorage::Record
92
100
  # be saved before the upload begins to prevent the upload clobbering another due to key collisions.
93
101
  # When providing a content type, pass <tt>identify: false</tt> to bypass
94
102
  # automatic content type inference.
103
+ #
104
+ # The optional +key+ parameter is treated as trusted. Using untrusted user input
105
+ # as the key may result in unexpected behavior.
95
106
  def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
96
107
  create_after_unfurling!(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap do |blob|
97
108
  blob.upload_without_unfurling(io)
@@ -104,6 +115,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
104
115
  # Once the form using the direct upload is submitted, the blob can be associated with the right record using
105
116
  # the signed ID.
106
117
  def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
118
+ metadata = filter_metadata(metadata)
107
119
  create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name
108
120
  end
109
121
 
@@ -151,6 +163,15 @@ class ActiveStorage::Blob < ActiveStorage::Record
151
163
  combined_blob.save!
152
164
  end
153
165
  end
166
+
167
+ private
168
+ def filter_metadata(metadata)
169
+ if metadata.is_a?(Hash)
170
+ metadata.without(*PROTECTED_METADATA)
171
+ else
172
+ metadata
173
+ end
174
+ end
154
175
  end
155
176
 
156
177
  include Analyzable
@@ -149,6 +149,7 @@ module ActiveStorage
149
149
  ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
150
150
  ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
151
151
  ActiveStorage.track_variants = app.config.active_storage.track_variants || false
152
+ ActiveStorage.streaming_chunk_max_size = app.config.active_storage.streaming_chunk_max_size || 100.megabytes
152
153
  end
153
154
  end
154
155
 
@@ -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 = 8
11
11
  MINOR = 1
12
12
  TINY = 2
13
- PRE = nil
13
+ PRE = "1"
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
@@ -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"
@@ -352,6 +353,7 @@ module ActiveStorage
352
353
  ]
353
354
  mattr_accessor :unsupported_image_processing_arguments
354
355
 
356
+ mattr_accessor :streaming_chunk_max_size, default: 100.megabytes
355
357
  mattr_accessor :service_urls_expire_in, default: 5.minutes
356
358
  mattr_accessor :touch_attachment_records, default: true
357
359
  mattr_accessor :urls_expire_in
@@ -362,6 +364,18 @@ module ActiveStorage
362
364
 
363
365
  mattr_accessor :track_variants, default: false
364
366
 
367
+ singleton_class.attr_accessor :checksum_implementation
368
+ @checksum_implementation = OpenSSL::Digest::MD5
369
+ begin
370
+ @checksum_implementation.hexdigest("test")
371
+ rescue # OpenSSL may have MD5 disabled
372
+ require "digest/md5"
373
+ @checksum_implementation = Digest::MD5
374
+ end
375
+
376
+ singleton_class.attr_accessor :streaming_max_ranges
377
+ @streaming_max_ranges = 1
378
+
365
379
  mattr_accessor :video_preview_arguments, default: "-y -vframes 1 -f image2"
366
380
 
367
381
  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: 8.1.2
4
+ version: 8.1.2.1
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: 8.1.2
18
+ version: 8.1.2.1
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: 8.1.2
25
+ version: 8.1.2.1
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: 8.1.2
32
+ version: 8.1.2.1
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: 8.1.2
39
+ version: 8.1.2.1
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: 8.1.2
46
+ version: 8.1.2.1
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: 8.1.2
53
+ version: 8.1.2.1
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: 8.1.2
60
+ version: 8.1.2.1
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: 8.1.2
67
+ version: 8.1.2.1
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: marcel
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -192,10 +192,10 @@ licenses:
192
192
  - MIT
193
193
  metadata:
194
194
  bug_tracker_uri: https://github.com/rails/rails/issues
195
- changelog_uri: https://github.com/rails/rails/blob/v8.1.2/activestorage/CHANGELOG.md
196
- documentation_uri: https://api.rubyonrails.org/v8.1.2/
195
+ changelog_uri: https://github.com/rails/rails/blob/v8.1.2.1/activestorage/CHANGELOG.md
196
+ documentation_uri: https://api.rubyonrails.org/v8.1.2.1/
197
197
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
198
- source_code_uri: https://github.com/rails/rails/tree/v8.1.2/activestorage
198
+ source_code_uri: https://github.com/rails/rails/tree/v8.1.2.1/activestorage
199
199
  rubygems_mfa_required: 'true'
200
200
  rdoc_options: []
201
201
  require_paths:
@@ -211,7 +211,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
211
211
  - !ruby/object:Gem::Version
212
212
  version: '0'
213
213
  requirements: []
214
- rubygems_version: 4.0.3
214
+ rubygems_version: 4.0.6
215
215
  specification_version: 4
216
216
  summary: Local and cloud file storage framework.
217
217
  test_files: []