Skip to content

Commit 2b6efd8

Browse files
committed
Disable libvips unfuzzed operations
and add test coverage for (un)supported file types.
1 parent 9ae6b3b commit 2b6efd8

2 files changed

Lines changed: 127 additions & 3 deletions

File tree

config/initializers/vips.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
raise LoadError, "Please install libvips" unless defined?(Vips::LIBRARY_VERSION)
22

3-
# Disable Openslide to prevent sqlite segfault in forked parallel workers
4-
# Requires libvips 8.13+
5-
Vips.block "VipsForeignLoadOpenslide", true if Vips.respond_to?(:block)
3+
# Disable unfuzzed libvips operations.
4+
#
5+
# To block loaders we need to call `Vips.block` after Rails and image_processing set their
6+
# defaults. Force the order of operations by autoloading the file now.
7+
ActiveStorage::Transformers::Vips
8+
Vips.block_untrusted(true)
9+
Vips.block("VipsForeignLoadOpenslide", true) # prevent sqlite segfault in forked parallel workers
10+
Rails.application.config.active_storage.variable_content_types -=
11+
%w[ image/bmp image/vnd.microsoft.icon image/vnd.adobe.photoshop ]
612

713
# Limit libvips to 4 threads for each thread pool. Default is #CPUs.
814
Vips.concurrency_set 4
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
require "test_helper"
2+
require "vips"
3+
require "tempfile"
4+
5+
# libvips selects a loader from a file's actual bytes, not from its declared content type. These
6+
# tests pin which loader is selected for each file type under the app's configured loader policy
7+
# (config/initializers/vips.rb).
8+
class VipsLoaderPolicyTest < ActiveSupport::TestCase
9+
# Header bytes are enough for libvips to identify a format; native types are encoded live, exotic
10+
# ones are represented by their magic bytes.
11+
FTYP_AVIF = "\x00\x00\x00\x1cftypavif\x00\x00\x00\x00avifmif1miaf".b
12+
FTYP_HEIC = "\x00\x00\x00\x1cftypheic\x00\x00\x00\x00heicmif1miaf".b
13+
BMP = "BM" + [ 0, 0, 54 ].pack("V3") + "\x00" * 40
14+
PSD = "8BPS" + [ 1 ].pack("n") + "\x00" * 26
15+
ICO = "\x00\x00\x01\x00\x01\x00" + "\x00" * 16
16+
SVG = %q(<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"/>)
17+
18+
test "loads PNG" do
19+
assert_equal "VipsForeignLoadPngFile", loader_for(encode("png"))
20+
end
21+
22+
test "loads GIF" do
23+
assert_equal "VipsForeignLoadNsgifFile", loader_for(encode("gif"))
24+
end
25+
26+
test "loads JPEG" do
27+
assert_equal "VipsForeignLoadJpegFile", loader_for(encode("jpg"))
28+
end
29+
30+
test "loads TIFF" do
31+
assert_equal "VipsForeignLoadTiffFile", loader_for(encode("tif"))
32+
end
33+
34+
test "loads WebP" do
35+
assert_equal "VipsForeignLoadWebpFile", loader_for(encode("webp"))
36+
end
37+
38+
test "loads AVIF" do
39+
assert_equal "VipsForeignLoadHeifFile", loader_for(FTYP_AVIF)
40+
end
41+
42+
test "loads HEIC" do
43+
assert_equal "VipsForeignLoadHeifFile", loader_for(FTYP_HEIC)
44+
end
45+
46+
test "loads BMP through magickload" do
47+
assert_nil loader_for(BMP)
48+
end
49+
50+
test "denies PSD through magickload" do
51+
assert_nil loader_for(PSD)
52+
end
53+
54+
test "denies ICO through magickload" do
55+
assert_nil loader_for(ICO)
56+
end
57+
58+
test "denies SVG through svgload" do
59+
assert_nil loader_for(SVG)
60+
end
61+
62+
test "denies OpenSlide files through openslideload" do
63+
# OpenSlide files can segfault the embedded sqlite in forked parallel workers
64+
assert_loader_blocked :openslideload, ".svs"
65+
end
66+
67+
test "denies FITS files through fitsload" do
68+
assert_loader_blocked :fitsload, ".fits"
69+
end
70+
71+
test "denies MATLAB files through matload" do
72+
assert_loader_blocked :matload, ".mat"
73+
end
74+
75+
test "denies NIFTI files through matload" do
76+
assert_loader_blocked :niftiload, ".nii"
77+
end
78+
79+
test "denies RAW files through rawload" do
80+
assert_loader_blocked :dcrawload, ".raw"
81+
end
82+
83+
test "denies VIPS files through vipsload" do
84+
assert_loader_blocked :vipsload, ".vips"
85+
end
86+
87+
private
88+
# Invoke a specific libvips loader directly and assert it is refused because the
89+
# operation is blocked (rather than because the bytes are not a valid image).
90+
def assert_loader_blocked(operation, extension)
91+
Tempfile.create([ "blocked_loader", extension ], binmode: true) do |file|
92+
file.write "not an image"
93+
file.flush
94+
95+
error = assert_raises(Vips::Error) { Vips::Image.public_send(operation, file.path) }
96+
actual = error.message.chomp
97+
98+
# note that exception message may include multiple errors on separate lines,
99+
# so `^` and `$` anchors are used instead of `\A` and `\z`.
100+
if actual =~ /^VipsOperation: class \"#{operation}\" not found$/
101+
skip "libvips does not support #{operation} on this system"
102+
end
103+
assert_match(/^#{operation}: operation is blocked$/, actual)
104+
end
105+
end
106+
107+
def encode(ext)
108+
Vips::Image.black(8, 8).add(128).cast("uchar").write_to_buffer(".#{ext}")
109+
end
110+
111+
def loader_for(bytes)
112+
Tempfile.create(%w[loader_probe .img], binmode: true) do |file|
113+
file.write bytes
114+
file.flush
115+
Vips.vips_foreign_find_load(file.path)
116+
end
117+
end
118+
end

0 commit comments

Comments
 (0)