summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-08-03 03:13:48 +0200
committererdgeist <erdgeist@erdgeist.org>2026-08-03 03:13:48 +0200
commit45bb56ad1d809bead00379e68144e91bb471da06 (patch)
tree83beffaf637ba30f84d6c8d117771d85c0457f6e
parent1c5911cf4765d7180d9f05a7405316426b3f3681 (diff)
Run ImageMagick under a policy that travels with the project
-rw-r--r--app/models/concerns/file_attachment.rb16
-rw-r--r--config/imagemagick/policy.xml48
2 files changed, 60 insertions, 4 deletions
diff --git a/app/models/concerns/file_attachment.rb b/app/models/concerns/file_attachment.rb
index 5e0803fd..fd0cabed 100644
--- a/app/models/concerns/file_attachment.rb
+++ b/app/models/concerns/file_attachment.rb
@@ -29,6 +29,14 @@ module FileAttachment
29 large: { args: ["-resize", "1600x1600>"] } 29 large: { args: ["-resize", "1600x1600>"] }
30 }.freeze 30 }.freeze
31 31
32 # Prepended to ImageMagick's configuration search path on every
33 # invocation, so config/imagemagick/policy.xml is read before the
34 # installed one. Per-invocation rather than in the environment, so it
35 # cannot leak into anything else on the host.
36 MAGICK_ENV = {
37 "MAGICK_CONFIGURE_PATH" => Rails.root.join("config", "imagemagick").to_s
38 }.freeze
39
32 IMAGE_CONTENT_TYPES = %w[image/jpeg image/gif image/png image/webp].freeze 40 IMAGE_CONTENT_TYPES = %w[image/jpeg image/gif image/png image/webp].freeze
33 VECTOR_CONTENT_TYPES = %w[image/svg+xml].freeze 41 VECTOR_CONTENT_TYPES = %w[image/svg+xml].freeze
34 RASTERIZED_CONTENT_TYPES = %w[application/pdf].freeze 42 RASTERIZED_CONTENT_TYPES = %w[application/pdf].freeze
@@ -152,7 +160,7 @@ module FileAttachment
152 STYLES.each do |style, options| 160 STYLES.each do |style, options|
153 dest_path = file_path(style) 161 dest_path = file_path(style)
154 FileUtils.mkdir_p(File.dirname(dest_path)) 162 FileUtils.mkdir_p(File.dirname(dest_path))
155 system("magick", original_path, *extra_args, *options[:args], dest_path) 163 system(MAGICK_ENV, "magick", original_path, *extra_args, *options[:args], dest_path)
156 end 164 end
157 end 165 end
158 166
@@ -173,13 +181,13 @@ module FileAttachment
173 FileUtils.mkdir_p(File.dirname(dest_path)) 181 FileUtils.mkdir_p(File.dirname(dest_path))
174 182
175 if og_full_bleed?(original_path) 183 if og_full_bleed?(original_path)
176 system("magick", "#{original_path}[0]", 184 system(MAGICK_ENV, "magick", "#{original_path}[0]",
177 "-resize", "#{OG_WIDTH}x#{OG_HEIGHT}^", 185 "-resize", "#{OG_WIDTH}x#{OG_HEIGHT}^",
178 "-gravity", "center", "-extent", "#{OG_WIDTH}x#{OG_HEIGHT}", 186 "-gravity", "center", "-extent", "#{OG_WIDTH}x#{OG_HEIGHT}",
179 "-background", OG_GROUND, "-alpha", "remove", "-alpha", "off", 187 "-background", OG_GROUND, "-alpha", "remove", "-alpha", "off",
180 *og_output_args, dest_path) 188 *og_output_args, dest_path)
181 else 189 else
182 system(*og_template_command(dest_path)) 190 system(MAGICK_ENV, *og_template_command(dest_path))
183 end 191 end
184 end 192 end
185 193
@@ -206,7 +214,7 @@ module FileAttachment
206 end 214 end
207 215
208 def source_dimensions(path) 216 def source_dimensions(path)
209 out, status = Open3.capture2("magick", "identify", "-format", "%w %h", "#{path}[0]") 217 out, status = Open3.capture2(MAGICK_ENV, "magick", "identify", "-format", "%w %h", "#{path}[0]")
210 return nil unless status.success? 218 return nil unless status.success?
211 219
212 width, height = out.split.map(&:to_i) 220 width, height = out.split.map(&:to_i)
diff --git a/config/imagemagick/policy.xml b/config/imagemagick/policy.xml
new file mode 100644
index 00000000..6106aa0b
--- /dev/null
+++ b/config/imagemagick/policy.xml
@@ -0,0 +1,48 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<!--
3 Read because FileAttachment sets MAGICK_CONFIGURE_PATH to this directory
4 on every magick invocation. ImageMagick prepends that path to its search
5 list rather than replacing it, so /usr/local/etc/ImageMagick-7/policy.xml
6 is still read afterwards and any hardening the port gains still applies.
7 Rules are processed in order, so these win.
8
9 Coders kept readable and why:
10 PDF, PS the rasterised branch and the 150dpi social card
11 SVG, MSVG the vector social card
12 JPEG PNG GIF WEBP the four accepted image types
13 LABEL used by internal operations
14-->
15<policymap>
16 <!-- Variants are generated synchronously on upload, so an oversized or
17 slow source blocks a request thread. A decompression bomb needs no
18 vulnerability at all. -->
19 <policy domain="resource" name="memory" value="256MiB"/>
20 <policy domain="resource" name="map" value="512MiB"/>
21 <policy domain="resource" name="area" value="128MP"/>
22 <policy domain="resource" name="disk" value="1GiB"/>
23 <policy domain="resource" name="width" value="16KP"/>
24 <policy domain="resource" name="height" value="16KP"/>
25 <policy domain="resource" name="list-length" value="64"/>
26 <policy domain="resource" name="time" value="120"/>
27
28 <!-- @file means "read this path as the argument", and - is stdin. -->
29 <policy domain="path" rights="none" pattern="@*"/>
30 <policy domain="path" rights="none" pattern="-"/>
31
32 <!-- ImageMagick picks its decoder from the file's bytes, not from the
33 declared content type, so an upload announced as image/jpeg is
34 decoded as MVG if that is what it contains. These are the coders
35 that turn that into a file read or a fetch. -->
36 <policy domain="coder" rights="none" pattern="MVG"/>
37 <policy domain="coder" rights="none" pattern="MSL"/>
38 <policy domain="coder" rights="none" pattern="URL"/>
39 <policy domain="coder" rights="none" pattern="HTTP"/>
40 <policy domain="coder" rights="none" pattern="HTTPS"/>
41 <policy domain="coder" rights="none" pattern="FTP"/>
42 <policy domain="coder" rights="none" pattern="EPHEMERAL"/>
43 <policy domain="coder" rights="none" pattern="SHOW"/>
44 <policy domain="coder" rights="none" pattern="WIN"/>
45 <policy domain="coder" rights="none" pattern="PLT"/>
46 <policy domain="coder" rights="none" pattern="XPS"/>
47 <policy domain="coder" rights="none" pattern="TEXT"/>
48</policymap>