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 +4 -4
- data/CHANGELOG.md +124 -0
- data/app/controllers/concerns/active_storage/streaming.rb +9 -4
- data/app/models/active_storage/blob.rb +22 -2
- data/app/models/active_storage/filename.rb +5 -4
- data/lib/active_storage/analyzer/audio_analyzer.rb +3 -0
- data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +3 -8
- data/lib/active_storage/analyzer/video_analyzer.rb +3 -0
- data/lib/active_storage/engine.rb +7 -1
- data/lib/active_storage/gem_version.rb +1 -1
- data/lib/active_storage/image_magick/policy.xml +68 -0
- data/lib/active_storage/image_magick.rb +63 -0
- data/lib/active_storage/previewer/video_previewer.rb +1 -1
- data/lib/active_storage/service/gcs_service.rb +11 -18
- data/lib/active_storage/transformers/image_magick.rb +2 -0
- data/lib/active_storage.rb +6 -0
- metadata +14 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fdfb78984fbd5e8de07c60266b6996cf48474e219120bf89a916ea96aeb7a190
|
|
4
|
+
data.tar.gz: 752b81997b178d7cd7473b1c27d5199bb6e03bebc301b4660eee0f380cfa95c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
76
|
-
# Change the status to 500 manually.
|
|
77
|
-
|
|
78
|
-
|
|
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.
|
|
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}-",
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
@@ -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
|
-
|
|
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:
|
|
99
|
+
headers: { "x-goog-content-length-range" => content_length_range(content_length), **custom_metadata_headers(custom_metadata) },
|
|
111
100
|
method: "PUT",
|
|
112
|
-
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
|
data/lib/active_storage.rb
CHANGED
|
@@ -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:
|