0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-04 10:39:43 +08:00
discourse/lib/image_magick.rb
David Taylor a071880169
DEV: Sandbox all image processing via Landlock (#42048)
- Update all imagemagick calls to go through a new `::Imagemagick`
wrapper, which wraps the command in `Discourse::SafeExec`
- Patch image_optim to force its calls through `SafeExec`

This provides robust defense-in-depth against vulnerabilities in image
processing binaries. Landlock is supported on Linux Kernel 5.13 and
above.
2026-07-27 18:27:34 +01:00

62 lines
2.3 KiB
Ruby
Vendored

# frozen_string_literal: true
require "tmpdir"
# Runs ImageMagick under the Landlock sandbox (via Discourse::SafeExec) so a
# decoder bug parsing an untrusted upload is confined to an explicit filesystem
# allowlist with no network, not the full rights of the calling process.
module ImageMagick
# memory_bytes sits above the policy.xml ceiling (memory 1GiB + map 2GiB) so a
# legitimate ≤128MP decode is not killed; MALLOC_ARENA_MAX bounds per-thread
# arenas that would otherwise inflate address space against it. cpu_seconds is
# a runaway backstop above the wall-clock timeout.
RLIMITS = {
cpu_seconds: 300,
memory_bytes: 4 * 1024 * 1024 * 1024,
file_size_bytes: 10 * 1024 * 1024 * 1024,
open_files: 1024,
}.freeze
DEFAULT_TIMEOUT = 30
def self.asset_read_paths
@asset_read_paths ||= [Rails.root.join("vendor").to_s, ENV["MAGICK_CONFIGURE_PATH"]].compact
end
def self.magick(*args, read: [], write: [], timeout: nil, nice: nil, failure_message: "")
command = ["magick", *args]
command = ["nice", "-n", nice.to_s, *command] if nice
run(*command, read:, write:, timeout:, failure_message:)
end
def self.identify(*args, read: [], write: [], timeout: nil, failure_message: "")
run("identify", *args, read:, write:, timeout:, failure_message:)
end
def self.run(*command, read:, write:, timeout:, failure_message:)
# A private scratch dir keeps ImageMagick's disk-backed pixel cache inside
# the write allowlist, so large images that spill to disk still succeed.
Dir.mktmpdir("discourse-imagemagick-") do |scratch|
Discourse::SafeExec.capture(
*command,
env: {
**ENV.slice("PATH", "MAGICK_CONFIGURE_PATH", "LANG", "LC_ALL"),
"MAGICK_TEMPORARY_PATH" => scratch,
"TMPDIR" => scratch,
"HOME" => scratch,
"XDG_CACHE_HOME" => scratch,
"MALLOC_ARENA_MAX" => "2",
},
unsetenv_others: true,
read: [*Discourse::SafeExec.default_read_paths, *asset_read_paths, *read],
write: [scratch, *write],
execute: Discourse::SafeExec.default_execute_paths,
timeout: timeout || DEFAULT_TIMEOUT,
rlimits: RLIMITS,
failure_message:,
seccomp_deny_network: true,
)
end
end
private_class_method :run
end