0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-04 10:39:43 +08:00
discourse/lib/vips.rb
Alan Guo Xiang Tan a8e4746b51
FEATURE: Use libvips for low-risk image operations
Letter avatars, dominant-color extraction, and generated topic OG images use ImageMagick today. These straightforward operations keep ImageMagick in the request and background-job paths while the wider image-processing migration remains behind a site setting.

This commit moves the three operations to the stock libvips CLI. It permits untrusted libvips access only for explicit SVG loads, converts each OG asset through its expected loader, rejects unsafe SVG features and media-type mismatches, and bounds SVG asset rendering. Dominant-color extraction verifies the stored format before decoding and caches unsupported pixel layouts as an empty result.

Failures do not fall back to ImageMagick. The companion discourse_docker change removes the unsupported TIFF loader from the libvips build.
2026-08-04 06:19:27 +08:00

58 lines
1.6 KiB
Ruby
Vendored

# frozen_string_literal: true
require "tmpdir"
class Vips
DEFAULT_TIMEOUT = 30
private_constant :DEFAULT_TIMEOUT
RLIMITS = {
cpu_seconds: 300,
memory_bytes: 4 * 1024 * 1024 * 1024,
file_size_bytes: 10 * 1024 * 1024 * 1024,
open_files: 1024,
}.freeze
private_constant :RLIMITS
def self.run(
*command,
read: [],
write: [],
timeout: nil,
nice: nil,
allow_untrusted: false,
failure_message: ""
)
if allow_untrusted && command.take(2) != %w[vips svgload]
raise ArgumentError,
"Only an explicit svgload operation may enable untrusted libvips operations"
end
command = ["nice", "-n", nice.to_s, *command] if nice
Dir.mktmpdir("discourse-vips-") do |scratch|
environment = {
**ENV.slice("PATH", "LANG", "LC_ALL"),
"TMPDIR" => scratch,
"HOME" => scratch,
"XDG_CACHE_HOME" => scratch,
"MALLOC_ARENA_MAX" => "2",
}
# libvips permits operations marked untrusted by default. Block them unless explicitly allowed.
# See https://github.com/libvips/libvips/blob/v8.18.2/doc/developer-checklist.md#L101-L104
environment["VIPS_BLOCK_UNTRUSTED"] = "1" if !allow_untrusted
Discourse::SafeExec.capture(
*command,
env: environment,
unsetenv_others: true,
read: [*Discourse::SafeExec.default_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
end