activestorage 8.1.4 → 8.1.4.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: 363218fe84c4760d1243ee6f1325312eab52a4d1d7c33f512022e1a8ad4ed8a5
4
- data.tar.gz: 603439dba3d29a9f57b433232954d85d9ec70905ab6114d10e370b2136283258
3
+ metadata.gz: fdfb78984fbd5e8de07c60266b6996cf48474e219120bf89a916ea96aeb7a190
4
+ data.tar.gz: 752b81997b178d7cd7473b1c27d5199bb6e03bebc301b4660eee0f380cfa95c2
5
5
  SHA512:
6
- metadata.gz: 5277345f853e82ae059c49bde6c2664f84516dfbd4da948e7a48b437da363682104b56fb895b75e7010f7201c5ced03106297c24bba67977a2567b045b2faa60
7
- data.tar.gz: 27b8cbc33599414d28774b41d1d3891a1fb803e4316e217f24e47903f8a2d5e6f92fd7ffb2d50d9b02efddf26fa08ae6a199993dbbfc4cdad37aab813df1b906
6
+ metadata.gz: 9cd250255e0da480e917eafac48c4f36751e9c7e3c54c3b3cc63d1ab82cb5d12a30738b7489d1dbce08b4158a506605423612d6f29d2cee75f17afa291b0c01e
7
+ data.tar.gz: 4b185e00d70788a9a2f22f5e49681927177fcb9eb22918593a7b22d48a7f2de6448eb6732af5e699e75dd541c341888f2e357c9e167b99f44aeca2c38ce5a9fe
data/CHANGELOG.md CHANGED
@@ -1,3 +1,127 @@
1
+ ## Rails 8.1.4.1 (November 03, 2026) ##
2
+
3
+ * Preserve committed proxy responses when a storage error interrupts a
4
+ streamed download.
5
+
6
+ [GHSA-4v2w-w6g6-f8f8]
7
+
8
+ *Jeremy Daer*
9
+
10
+ * Derive `ActiveStorage::Filename#base` and `#extension` from the sanitized
11
+ filename.
12
+
13
+ This prevents null bytes and invalid byte sequences in stored filename
14
+ metadata from raising when a blob is opened or represented. Existing blob
15
+ records are handled without a data migration.
16
+
17
+ [GHSA-p2pm-g2h6-7j5j]
18
+
19
+ *Jeremy Daer*
20
+
21
+ * Ship an ImageMagick security policy and apply it to every ImageMagick
22
+ process MiniMagick spawns.
23
+
24
+ ImageMagick chooses a coder from the content of the file it is given, and
25
+ some coders act on files named inside the image rather than on the image
26
+ itself. Active Storage constrained none of them, so an uploaded file could
27
+ reach any coder the host's policy allowed, through the ImageMagick analyzer
28
+ at attach time, with no variant or `analyze` call in the application.
29
+
30
+ Active Storage now ships a `policy.xml` in
31
+ `ActiveStorage::ImageMagick::POLICY_DIRECTORY` and prepends that directory
32
+ to `MAGICK_CONFIGURE_PATH` through `MiniMagick.cli_env`. ImageMagick applies
33
+ every policy file on its search path, so the shipped policy adds to the
34
+ host's policy rather than replacing it. A `MAGICK_CONFIGURE_PATH` set in the
35
+ environment or in `MiniMagick.cli_env`, including by an initializer or
36
+ dotenv, is kept after the shipped directory.
37
+
38
+ The shipped policy denies the coders that script ImageMagick (`MSL`), read
39
+ or delete files other than the image (`TEXT`, `EPHEMERAL`), fetch remote
40
+ references (`URL`, `HTTP`, `HTTPS`, `FTP`), display to a screen (`SHOW`,
41
+ `WIN`, `PLT`), or hand the file to Ghostscript (the `PS`, `EPS`, `EPT` and
42
+ `PDF` coders, every alias of them, and the `gs` delegate), or reach a file
43
+ directly (`FILE`, `MPC`, `XPS`, `PCL`), and it refuses `@filename`
44
+ indirection. The policy applies to every ImageMagick command MiniMagick
45
+ runs, not only Active Storage's.
46
+
47
+ These applications must act:
48
+
49
+ * One whose variant processor is `:mini_magick` has to use mini_magick 5.1
50
+ or later, which applying the policy requires. With an older mini_magick,
51
+ it raises at boot.
52
+ * One that renders PostScript or PDF through ImageMagick has to re-allow
53
+ the `gs` delegate and the coders it uses in the host's `policy.xml`. A
54
+ host policy that allows a coder overrides the shipped deny.
55
+ * One that replaces `MiniMagick.cli_env` after it boots, clears the
56
+ environment with `MiniMagick.cli_prefix`, or installs its own
57
+ `MiniMagick::Shell` never receives the shipped policy and has to carry
58
+ its rules in ImageMagick's system policy. So does one that uses
59
+ GraphicsMagick, which does not read ImageMagick policies.
60
+
61
+ Every other application needs no change. See "ImageMagick Security Policy"
62
+ in the Active Storage guide.
63
+
64
+ [GHSA-f3w6-w445-fwh9]
65
+
66
+ *Mike Dalessio*
67
+
68
+ * Enforce the declared content length on Google Cloud Storage direct uploads.
69
+
70
+ The signed URL now requires an `x-goog-content-length-range` header matching the blob's
71
+ `byte_size`, so Cloud Storage rejects a body of any other size.
72
+
73
+ Browsers send the new header, so a bucket's CORS configuration must list
74
+ `x-goog-content-length-range` in `responseHeader`, or direct uploads from the browser fail.
75
+
76
+ [GHSA-hfr4-8rjc-97m2]
77
+
78
+ *Mike Dalessio*
79
+
80
+ * Sign GCS direct uploads with V2 when `cache_control` is set.
81
+
82
+ V4 signatures did not bind the Content-Type or Content-MD5 headers, so the uploader could
83
+ choose the stored Content-Type. Direct-upload URLs with `cache_control` are no longer limited
84
+ to 7 days, and the client sends `Cache-Control` unsigned.
85
+
86
+ [GHSA-3w93-6hmw-whh4]
87
+
88
+ *Mike Dalessio*
89
+
90
+ * Stop an uploaded filename choosing the extension of the tempfile Active
91
+ Storage hands to an external tool.
92
+
93
+ `ActiveStorage::Blob#open` named its tempfile after the uploaded filename,
94
+ extension and all. Tools that select a decoder from the extension alone,
95
+ such as FFmpeg's AviSynth demuxer on `.avs`, therefore took their
96
+ instructions from the uploader. The extension is now kept only when it
97
+ denotes the blob's recorded media type, and dropped otherwise.
98
+
99
+ [GHSA-v924-7rp8-4mxh]
100
+
101
+ *Mike Dalessio*
102
+
103
+ * Restrict the formats ffmpeg and ffprobe will open when previewing and
104
+ analyzing video and audio.
105
+
106
+ Active Storage now passes `-format_whitelist` before the input at every
107
+ ffmpeg and ffprobe call site, limiting demuxer selection to common audio and
108
+ video container formats. Configure the list with
109
+ `config.active_storage.ffmpeg_allowed_formats`, or set it to `nil` to let
110
+ ffmpeg choose any demuxer it was built with.
111
+
112
+ Files in other formats, such as MXF, DV, AC3, WavPack, AU, W64, and raw
113
+ H.264 or MPEG-1/2 video streams, now fail to preview with
114
+ `ActiveStorage::PreviewError`, and their analysis stores no media metadata
115
+ and logs a warning. Add their demuxers to the list to keep them.
116
+
117
+ ```ruby
118
+ config.active_storage.ffmpeg_allowed_formats += %w[ mxf ]
119
+ ```
120
+
121
+ [GHSA-3f4q-8x5m-76v8]
122
+
123
+ *Mike Dalessio*
124
+
1
125
  ## Rails 8.1.4 (September 24, 2026) ##
2
126
 
3
127
  * Warn instead of aborting boot when `image_processing` 2.x is installed without `ruby-vips`
@@ -69,13 +69,18 @@ module ActiveStorage::Streaming
69
69
  stream.write chunk
70
70
  end
71
71
  rescue ActiveStorage::FileNotFoundError
72
+ raise if response.committed?
73
+
72
74
  expires_now
73
75
  head :not_found
74
76
  rescue
75
- # Status and caching headers are already set, but not committed.
76
- # Change the status to 500 manually.
77
- expires_now
78
- head :internal_server_error
77
+ # Until the first chunk is written, status and caching headers are set
78
+ # but not committed. Change the status to 500 manually.
79
+ unless response.committed?
80
+ expires_now
81
+ head :internal_server_error
82
+ end
83
+
79
84
  raise
80
85
  end
81
86
  end
@@ -305,7 +305,8 @@ class ActiveStorage::Blob < ActiveStorage::Record
305
305
 
306
306
  # Downloads the blob to a tempfile on disk. Yields the tempfile.
307
307
  #
308
- # The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
308
+ # The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. It keeps the blob filename's
309
+ # extension when that extension denotes the blob's content type, and has no extension otherwise.
309
310
  #
310
311
  # By default, the tempfile is created in <tt>Dir.tmpdir</tt>. Pass +tmpdir:+ to create it in a different directory:
311
312
  #
@@ -321,7 +322,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
321
322
  key,
322
323
  checksum: checksum,
323
324
  verify: !composed,
324
- name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ],
325
+ name: [ "ActiveStorage-#{id}-", tempfile_extension ],
325
326
  tmpdir: tmpdir,
326
327
  &block
327
328
  )
@@ -360,6 +361,25 @@ class ActiveStorage::Blob < ActiveStorage::Record
360
361
  end
361
362
 
362
363
  private
364
+ # The extension is there for tools that cannot tell the type from the bytes (316d87e4: ImageMagick
365
+ # needs it for an SVG with no XML declaration), but some tools choose a decoder on it alone: FFmpeg
366
+ # runs a `.avs` file as an AviSynth script. So the uploaded extension is kept only when it denotes
367
+ # the blob's media type.
368
+ def tempfile_extension
369
+ extension = filename.extension_with_delimiter
370
+ return "" if extension.blank?
371
+
372
+ type_for_extension = Marcel::MimeType.for(name: filename.to_s)
373
+ return "" if type_for_extension == Marcel::MimeType::BINARY || type_for_extension != media_type
374
+
375
+ extension
376
+ end
377
+
378
+ # Marcel answers in canonical lowercase, so drop parameters and case before comparing.
379
+ def media_type
380
+ content_type.to_s.split(";").first.to_s.strip.downcase
381
+ end
382
+
363
383
  def compute_checksum_in_chunks(io)
364
384
  raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
365
385
 
@@ -25,7 +25,7 @@ class ActiveStorage::Filename
25
25
  # ActiveStorage::Filename.new("racecar").base # => "racecar"
26
26
  # ActiveStorage::Filename.new(".gitignore").base # => ".gitignore"
27
27
  def base
28
- File.basename @filename, extension_with_delimiter
28
+ File.basename sanitized, extension_with_delimiter
29
29
  end
30
30
 
31
31
  # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at the
@@ -35,7 +35,7 @@ class ActiveStorage::Filename
35
35
  # ActiveStorage::Filename.new("racecar").extension_with_delimiter # => ""
36
36
  # ActiveStorage::Filename.new(".gitignore").extension_with_delimiter # => ""
37
37
  def extension_with_delimiter
38
- File.extname @filename
38
+ File.extname sanitized
39
39
  end
40
40
 
41
41
  # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at
@@ -55,9 +55,10 @@ class ActiveStorage::Filename
55
55
  # ActiveStorage::Filename.new("foo:bar.jpg").sanitized # => "foo-bar.jpg"
56
56
  # ActiveStorage::Filename.new("foo/bar.jpg").sanitized # => "foo-bar.jpg"
57
57
  #
58
- # Characters considered unsafe for storage (e.g. \, $, and the RTL override character) are replaced with a dash.
58
+ # Characters considered unsafe for storage (e.g. \, $, the null character, and the RTL override character) are
59
+ # replaced with a dash.
59
60
  def sanitized
60
- @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/<>?*\"\t\r\n\\", "-")
61
+ @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}\u{0000}%$|:;/<>?*\"\t\r\n\\", "-")
61
62
  end
62
63
 
63
64
  # Returns the sanitized version of the filename.
@@ -60,9 +60,12 @@ module ActiveStorage
60
60
  "-show_streams",
61
61
  "-show_format",
62
62
  "-v", "error",
63
+ *ActiveStorage.ffmpeg_format_whitelist_arguments,
63
64
  file.path
64
65
  ]) do |output|
65
66
  JSON.parse(output.read)
67
+ end.tap do
68
+ logger.warn "Audio analysis failed because ffprobe exited with status #{$?.exitstatus}. The file may be corrupt, or in a format outside config.active_storage.ffmpeg_allowed_formats." unless $?.success?
66
69
  end
67
70
  end
68
71
  rescue Errno::ENOENT
@@ -1,13 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- begin
4
- gem "mini_magick"
5
- require "mini_magick"
6
- ActiveStorage::MINIMAGICK_AVAILABLE = true # :nodoc:
7
- rescue LoadError => error
8
- ActiveStorage::MINIMAGICK_AVAILABLE = false # :nodoc:
9
- raise error unless error.message.include?("mini_magick")
10
- end
3
+ require "active_storage/image_magick"
11
4
 
12
5
  module ActiveStorage
13
6
  # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
@@ -24,6 +17,8 @@ module ActiveStorage
24
17
  return {}
25
18
  end
26
19
 
20
+ ActiveStorage.require_securable_mini_magick!
21
+
27
22
  download_blob_to_tempfile do |file|
28
23
  image = instrument("mini_magick") do
29
24
  MiniMagick::Image.new(file.path)
@@ -140,9 +140,12 @@ module ActiveStorage
140
140
  "-show_streams",
141
141
  "-show_format",
142
142
  "-v", "error",
143
+ *ActiveStorage.ffmpeg_format_whitelist_arguments,
143
144
  file.path
144
145
  ]) do |output|
145
146
  JSON.parse(output.read)
147
+ end.tap do
148
+ logger.warn "Video analysis failed because ffprobe exited with status #{$?.exitstatus}. The file may be corrupt, or in a format outside config.active_storage.ffmpeg_allowed_formats." unless $?.success?
146
149
  end
147
150
  end
148
151
  rescue Errno::ENOENT
@@ -29,6 +29,7 @@ module ActiveStorage
29
29
  config.active_storage.paths = ActiveSupport::OrderedOptions.new
30
30
  config.active_storage.queues = ActiveSupport::InheritableOptions.new
31
31
  config.active_storage.precompile_assets = true
32
+ config.active_storage.ffmpeg_allowed_formats = ActiveStorage.ffmpeg_allowed_formats
32
33
 
33
34
  config.active_storage.variable_content_types = %w(
34
35
  image/png
@@ -92,6 +93,10 @@ module ActiveStorage
92
93
  ActiveStorage.previewers = app.config.active_storage.previewers || []
93
94
  ActiveStorage.analyzers = app.config.active_storage.analyzers || []
94
95
 
96
+ if ActiveStorage::MINIMAGICK_AVAILABLE && !ActiveStorage::MINIMAGICK_UNSECURABLE
97
+ ActiveStorage::ImageMagick.apply_policy
98
+ end
99
+
95
100
  begin
96
101
  ActiveStorage.variant_transformer =
97
102
  case ActiveStorage.variant_processor
@@ -123,7 +128,7 @@ module ActiveStorage
123
128
  when /mini_magick/
124
129
  ActiveStorage.logger.warn <<~WARNING.squish
125
130
  Generating image variants with ImageMagick requires the mini_magick gem.
126
- Please add `gem "mini_magick", "~> 5.0"` to your Gemfile.
131
+ Please add `gem "mini_magick", "~> 5.1"` to your Gemfile.
127
132
  WARNING
128
133
  else
129
134
  raise
@@ -158,6 +163,7 @@ module ActiveStorage
158
163
  ActiveStorage.content_types_allowed_inline = app.config.active_storage.content_types_allowed_inline || []
159
164
  ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
160
165
  ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
166
+ ActiveStorage.ffmpeg_allowed_formats = app.config.active_storage.ffmpeg_allowed_formats
161
167
  ActiveStorage.track_variants = app.config.active_storage.track_variants || false
162
168
  ActiveStorage.streaming_chunk_max_size = app.config.active_storage.streaming_chunk_max_size || 100.megabytes
163
169
  end
@@ -10,7 +10,7 @@ module ActiveStorage
10
10
  MAJOR = 8
11
11
  MINOR = 1
12
12
  TINY = 4
13
- PRE = nil
13
+ PRE = "1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -0,0 +1,68 @@
1
+ <!--
2
+ Active Storage's ImageMagick security policy. Applied in addition to the host's policy.xml,
3
+ through MAGICK_CONFIGURE_PATH, to every ImageMagick process MiniMagick spawns.
4
+
5
+ This file must work under both ImageMagick 6 and ImageMagick 7. Use the "coder", "delegate"
6
+ and "path" domains, which every release of both honors. A domain one release does not know,
7
+ such as "module" on older ImageMagick 6 releases, is ignored rather than rejected.
8
+
9
+ ImageMagick picks a coder from the file's content, and several coders name the same underlying
10
+ reader through different tokens (PS, EPS, EPI, EPSF, EPSI, EPT; PDF, AI, EPDF, PDFA, POCKETMOD). A
11
+ coder rule matches one token, so every token is denied by coder.
12
+
13
+ Do not put a backtick anywhere in this file. ImageMagick's tokenizer treats a backtick as an
14
+ opening quote and silently drops every rule up to the next apostrophe.
15
+ -->
16
+ <policymap>
17
+ <!-- Scripting coder that reads and writes files named inside the image. -->
18
+ <policy domain="coder" rights="none" pattern="MSL"/>
19
+
20
+ <!-- Coders that read a file, or delete one, other than the image being processed. -->
21
+ <policy domain="coder" rights="none" pattern="EPHEMERAL"/>
22
+ <policy domain="coder" rights="none" pattern="TEXT"/>
23
+
24
+ <!-- Coder that reads back a pixel cache from a file the image header names. -->
25
+ <policy domain="coder" rights="none" pattern="MPC"/>
26
+
27
+ <!-- Coders that display to a screen. -->
28
+ <policy domain="coder" rights="none" pattern="SHOW"/>
29
+ <policy domain="coder" rights="none" pattern="WIN"/>
30
+ <policy domain="coder" rights="none" pattern="PLT"/>
31
+
32
+ <!-- Coders that read a reference from outside the image, remote or local. FILE reads file://. -->
33
+ <policy domain="coder" rights="none" pattern="URL"/>
34
+ <policy domain="coder" rights="none" pattern="HTTP"/>
35
+ <policy domain="coder" rights="none" pattern="HTTPS"/>
36
+ <policy domain="coder" rights="none" pattern="FTP"/>
37
+ <policy domain="coder" rights="none" pattern="FILE"/>
38
+
39
+ <!-- Interpreters that read and write files of their own. Ghostscript backs the PS and PDF
40
+ coders and their token aliases; GhostXPS and GhostPCL back XPS and PCL. The "gs" delegate
41
+ covers ImageMagick builds that run Ghostscript as an external command, and the coder rules
42
+ cover builds that link it in-process, where the delegate rule never applies. An
43
+ application that transforms PostScript or PDF through ImageMagick must allow the "gs"
44
+ delegate, and the coders it uses, again in the host's policy.xml. -->
45
+ <policy domain="delegate" rights="none" pattern="gs"/>
46
+ <policy domain="coder" rights="none" pattern="PS"/>
47
+ <policy domain="coder" rights="none" pattern="PS2"/>
48
+ <policy domain="coder" rights="none" pattern="PS3"/>
49
+ <policy domain="coder" rights="none" pattern="EPS"/>
50
+ <policy domain="coder" rights="none" pattern="EPS2"/>
51
+ <policy domain="coder" rights="none" pattern="EPS3"/>
52
+ <policy domain="coder" rights="none" pattern="EPI"/>
53
+ <policy domain="coder" rights="none" pattern="EPSF"/>
54
+ <policy domain="coder" rights="none" pattern="EPSI"/>
55
+ <policy domain="coder" rights="none" pattern="EPT"/>
56
+ <policy domain="coder" rights="none" pattern="EPT2"/>
57
+ <policy domain="coder" rights="none" pattern="EPT3"/>
58
+ <policy domain="coder" rights="none" pattern="PDF"/>
59
+ <policy domain="coder" rights="none" pattern="AI"/>
60
+ <policy domain="coder" rights="none" pattern="EPDF"/>
61
+ <policy domain="coder" rights="none" pattern="PDFA"/>
62
+ <policy domain="coder" rights="none" pattern="POCKETMOD"/>
63
+ <policy domain="coder" rights="none" pattern="XPS"/>
64
+ <policy domain="coder" rights="none" pattern="PCL"/>
65
+
66
+ <!-- Indirect reads such as text:@/etc/passwd, which substitute a file for an argument. -->
67
+ <policy domain="path" rights="none" pattern="@*"/>
68
+ </policymap>
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "active_storage"
6
+ require "active_support/core_ext/object/blank"
7
+ require "active_support/core_ext/string/filters"
8
+
9
+ begin
10
+ gem "mini_magick"
11
+ require "mini_magick"
12
+ ActiveStorage::MINIMAGICK_AVAILABLE = true # :nodoc:
13
+ rescue LoadError => error
14
+ ActiveStorage::MINIMAGICK_AVAILABLE = false # :nodoc:
15
+ raise error unless error.message.include?("mini_magick")
16
+ end
17
+
18
+ module ActiveStorage
19
+ # Adds Active Storage's ImageMagick security policy to every ImageMagick process MiniMagick spawns.
20
+ #
21
+ # ImageMagick reads every `policy.xml` on its configuration search path and applies all of them,
22
+ # so the shipped policy adds to the operator's policy rather than replacing it. `MAGICK_CONFIGURE_PATH`
23
+ # prepends to that search path; a value the operator has already set, in the environment or in
24
+ # `MiniMagick.cli_env`, is kept after the shipped directory.
25
+ module ImageMagick
26
+ POLICY_DIRECTORY = File.expand_path("image_magick", __dir__)
27
+
28
+ # Called when Active Storage loads, and again after the application initializes, so that a
29
+ # `MAGICK_CONFIGURE_PATH` or `MiniMagick.cli_env` set by an initializer or dotenv keeps the policy.
30
+ def self.apply_policy # :nodoc:
31
+ operator_paths = operator_paths(MiniMagick.cli_env["MAGICK_CONFIGURE_PATH"]).presence ||
32
+ operator_paths(ENV["MAGICK_CONFIGURE_PATH"])
33
+
34
+ MiniMagick.cli_env = MiniMagick.cli_env.merge("MAGICK_CONFIGURE_PATH" => configure_path(operator_paths)).freeze
35
+ end
36
+
37
+ def self.configure_path(operator_path)
38
+ [ POLICY_DIRECTORY, *operator_paths(operator_path) ].join(File::PATH_SEPARATOR)
39
+ end
40
+
41
+ def self.operator_paths(path)
42
+ Array(path).flat_map { |entry| entry.split(File::PATH_SEPARATOR) } - [ POLICY_DIRECTORY, "" ]
43
+ end
44
+ private_class_method :operator_paths
45
+ end
46
+ end
47
+
48
+ ActiveStorage::MINIMAGICK_UNSECURABLE = ActiveStorage::MINIMAGICK_AVAILABLE && !MiniMagick.respond_to?(:cli_env=) # :nodoc:
49
+
50
+ module ActiveStorage
51
+ # Raises when the installed mini_magick cannot apply the shipped ImageMagick security policy.
52
+ # Called before Active Storage hands untrusted content to ImageMagick.
53
+ def self.require_securable_mini_magick! # :nodoc:
54
+ if MINIMAGICK_UNSECURABLE
55
+ raise <<~ERROR.squish
56
+ Active Storage's ImageMagick security policy requires mini_magick 5.1 or later. Please add
57
+ `gem "mini_magick", "~> 5.1"` to your Gemfile, or use the :vips variant processor.
58
+ ERROR
59
+ end
60
+ end
61
+ end
62
+
63
+ ActiveStorage::ImageMagick.apply_policy if ActiveStorage::MINIMAGICK_AVAILABLE && !ActiveStorage::MINIMAGICK_UNSECURABLE
@@ -30,7 +30,7 @@ module ActiveStorage
30
30
 
31
31
  private
32
32
  def draw_relevant_frame_from(file, &block)
33
- draw self.class.ffmpeg_path, "-i", file.path, *Shellwords.split(ActiveStorage.video_preview_arguments), "-", &block
33
+ draw self.class.ffmpeg_path, *ActiveStorage.ffmpeg_format_whitelist_arguments, "-i", file.path, *Shellwords.split(ActiveStorage.video_preview_arguments), "-", &block
34
34
  end
35
35
  end
36
36
  end
@@ -89,27 +89,16 @@ module ActiveStorage
89
89
  end
90
90
  end
91
91
 
92
- def url_for_direct_upload(key, expires_in:, checksum:, custom_metadata: {}, **)
92
+ def url_for_direct_upload(key, expires_in:, checksum:, content_length:, custom_metadata: {}, **)
93
93
  instrument :url, key: key do |payload|
94
- headers = {}
95
- version = :v2
96
-
97
- if @config[:cache_control].present?
98
- headers["Cache-Control"] = @config[:cache_control]
99
- # v2 signing doesn't support non `x-goog-` headers. Only switch to v4 signing
100
- # if necessary for back-compat; v4 limits the expiration of the URL to 7 days
101
- # whereas v2 has no limit
102
- version = :v4
103
- end
104
-
105
- headers.merge!(custom_metadata_headers(custom_metadata))
106
-
94
+ # V2 signatures bind the Content-Type (empty) and Content-MD5 headers. GCS leaves headers
95
+ # other than x-goog-* out of V2 signatures, so the client sends Cache-Control unsigned.
107
96
  args = {
108
97
  content_md5: checksum,
109
98
  expires: expires_in,
110
- headers: headers,
99
+ headers: { "x-goog-content-length-range" => content_length_range(content_length), **custom_metadata_headers(custom_metadata) },
111
100
  method: "PUT",
112
- version: version,
101
+ version: :v2,
113
102
  }
114
103
 
115
104
  if @config[:iam]
@@ -125,10 +114,10 @@ module ActiveStorage
125
114
  end
126
115
  end
127
116
 
128
- def headers_for_direct_upload(key, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
117
+ def headers_for_direct_upload(key, checksum:, content_length:, filename: nil, disposition: nil, custom_metadata: {}, **)
129
118
  content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
130
119
 
131
- headers = { "Content-MD5" => checksum, "Content-Disposition" => content_disposition, **custom_metadata_headers(custom_metadata) }
120
+ headers = { "Content-MD5" => checksum, "Content-Disposition" => content_disposition, "x-goog-content-length-range" => content_length_range(content_length), **custom_metadata_headers(custom_metadata) }
132
121
  if @config[:cache_control].present?
133
122
  headers["Cache-Control"] = @config[:cache_control]
134
123
  end
@@ -238,5 +227,9 @@ module ActiveStorage
238
227
  def custom_metadata_headers(metadata)
239
228
  metadata.transform_keys { |key| "x-goog-meta-#{key}" }
240
229
  end
230
+
231
+ def content_length_range(content_length)
232
+ "#{content_length},#{content_length}"
233
+ end
241
234
  end
242
235
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_storage/image_magick"
4
+ ActiveStorage.require_securable_mini_magick!
3
5
  module ActiveStorage
4
6
  module Transformers
5
7
  class ImageMagick < ImageProcessingTransformer
@@ -382,6 +382,12 @@ module ActiveStorage
382
382
 
383
383
  mattr_accessor :video_preview_arguments, default: "-y -vframes 1 -f image2"
384
384
 
385
+ mattr_accessor :ffmpeg_allowed_formats, default: %w[ mov matroska avi mpeg mpegts flv asf ogg mp3 wav flac aac aiff caf ].freeze
386
+
387
+ def self.ffmpeg_format_whitelist_arguments # :nodoc:
388
+ [ "-format_whitelist", Array(ffmpeg_allowed_formats).join(",") ] unless ffmpeg_allowed_formats.nil?
389
+ end
390
+
385
391
  module Transformers
386
392
  extend ActiveSupport::Autoload
387
393
 
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.4
4
+ version: 8.1.4.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.4
18
+ version: 8.1.4.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.4
25
+ version: 8.1.4.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.4
32
+ version: 8.1.4.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.4
39
+ version: 8.1.4.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.4
46
+ version: 8.1.4.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.4
53
+ version: 8.1.4.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.4
60
+ version: 8.1.4.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.4
67
+ version: 8.1.4.1
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: marcel
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -166,6 +166,8 @@ files:
166
166
  - lib/active_storage/errors.rb
167
167
  - lib/active_storage/fixture_set.rb
168
168
  - lib/active_storage/gem_version.rb
169
+ - lib/active_storage/image_magick.rb
170
+ - lib/active_storage/image_magick/policy.xml
169
171
  - lib/active_storage/log_subscriber.rb
170
172
  - lib/active_storage/previewer.rb
171
173
  - lib/active_storage/previewer/mupdf_previewer.rb
@@ -193,10 +195,10 @@ licenses:
193
195
  - MIT
194
196
  metadata:
195
197
  bug_tracker_uri: https://github.com/rails/rails/issues
196
- changelog_uri: https://github.com/rails/rails/blob/v8.1.4/activestorage/CHANGELOG.md
197
- documentation_uri: https://api.rubyonrails.org/v8.1.4/
198
+ changelog_uri: https://github.com/rails/rails/blob/v8.1.4.1/activestorage/CHANGELOG.md
199
+ documentation_uri: https://api.rubyonrails.org/v8.1.4.1/
198
200
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
199
- source_code_uri: https://github.com/rails/rails/tree/v8.1.4/activestorage
201
+ source_code_uri: https://github.com/rails/rails/tree/v8.1.4.1/activestorage
200
202
  rubygems_mfa_required: 'true'
201
203
  rdoc_options: []
202
204
  require_paths: