Skip to content

Commit 94e2f00

Browse files
fresh-eggstenderlove
authored andcommitted
Added image transformation validation via configurable allow-list.
Variant now offers a configurable allow-list for transformation methods in addition to a configurable deny-list for arguments. [CVE-2022-21831]
1 parent 46fe51b commit 94e2f00

File tree

5 files changed

+473
-0
lines changed

5 files changed

+473
-0
lines changed

activestorage/app/models/active_storage/variation.rb

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,301 @@
2020
class ActiveStorage::Variation
2121
attr_reader :transformations
2222

23+
class UnsupportedImageProcessingMethod < StandardError; end
24+
class UnsupportedImageProcessingArgument < StandardError; end
25+
26+
SUPPORTED_IMAGE_PROCESSING_METHODS = [
27+
"adaptive_blur",
28+
"adaptive_resize",
29+
"adaptive_sharpen",
30+
"adjoin",
31+
"affine",
32+
"alpha",
33+
"annotate",
34+
"antialias",
35+
"append",
36+
"apply",
37+
"attenuate",
38+
"authenticate",
39+
"auto_gamma",
40+
"auto_level",
41+
"auto_orient",
42+
"auto_threshold",
43+
"backdrop",
44+
"background",
45+
"bench",
46+
"bias",
47+
"bilateral_blur",
48+
"black_point_compensation",
49+
"black_threshold",
50+
"blend",
51+
"blue_primary",
52+
"blue_shift",
53+
"blur",
54+
"border",
55+
"bordercolor",
56+
"borderwidth",
57+
"brightness_contrast",
58+
"cache",
59+
"canny",
60+
"caption",
61+
"channel",
62+
"channel_fx",
63+
"charcoal",
64+
"chop",
65+
"clahe",
66+
"clamp",
67+
"clip",
68+
"clip_path",
69+
"clone",
70+
"clut",
71+
"coalesce",
72+
"colorize",
73+
"colormap",
74+
"color_matrix",
75+
"colors",
76+
"colorspace",
77+
"colourspace",
78+
"color_threshold",
79+
"combine",
80+
"combine_options",
81+
"comment",
82+
"compare",
83+
"complex",
84+
"compose",
85+
"composite",
86+
"compress",
87+
"connected_components",
88+
"contrast",
89+
"contrast_stretch",
90+
"convert",
91+
"convolve",
92+
"copy",
93+
"crop",
94+
"cycle",
95+
"deconstruct",
96+
"define",
97+
"delay",
98+
"delete",
99+
"density",
100+
"depth",
101+
"descend",
102+
"deskew",
103+
"despeckle",
104+
"direction",
105+
"displace",
106+
"dispose",
107+
"dissimilarity_threshold",
108+
"dissolve",
109+
"distort",
110+
"dither",
111+
"draw",
112+
"duplicate",
113+
"edge",
114+
"emboss",
115+
"encoding",
116+
"endian",
117+
"enhance",
118+
"equalize",
119+
"evaluate",
120+
"evaluate_sequence",
121+
"extent",
122+
"extract",
123+
"family",
124+
"features",
125+
"fft",
126+
"fill",
127+
"filter",
128+
"flatten",
129+
"flip",
130+
"floodfill",
131+
"flop",
132+
"font",
133+
"foreground",
134+
"format",
135+
"frame",
136+
"function",
137+
"fuzz",
138+
"fx",
139+
"gamma",
140+
"gaussian_blur",
141+
"geometry",
142+
"gravity",
143+
"grayscale",
144+
"green_primary",
145+
"hald_clut",
146+
"highlight_color",
147+
"hough_lines",
148+
"iconGeometry",
149+
"iconic",
150+
"identify",
151+
"ift",
152+
"illuminant",
153+
"immutable",
154+
"implode",
155+
"insert",
156+
"intensity",
157+
"intent",
158+
"interlace",
159+
"interline_spacing",
160+
"interpolate",
161+
"interpolative_resize",
162+
"interword_spacing",
163+
"kerning",
164+
"kmeans",
165+
"kuwahara",
166+
"label",
167+
"lat",
168+
"layers",
169+
"level",
170+
"level_colors",
171+
"limit",
172+
"limits",
173+
"linear_stretch",
174+
"linewidth",
175+
"liquid_rescale",
176+
"list",
177+
"loader",
178+
"log",
179+
"loop",
180+
"lowlight_color",
181+
"magnify",
182+
"map",
183+
"mattecolor",
184+
"median",
185+
"mean_shift",
186+
"metric",
187+
"mode",
188+
"modulate",
189+
"moments",
190+
"monitor",
191+
"monochrome",
192+
"morph",
193+
"morphology",
194+
"mosaic",
195+
"motion_blur",
196+
"name",
197+
"negate",
198+
"noise",
199+
"normalize",
200+
"opaque",
201+
"ordered_dither",
202+
"orient",
203+
"page",
204+
"paint",
205+
"pause",
206+
"perceptible",
207+
"ping",
208+
"pointsize",
209+
"polaroid",
210+
"poly",
211+
"posterize",
212+
"precision",
213+
"preview",
214+
"process",
215+
"quality",
216+
"quantize",
217+
"quiet",
218+
"radial_blur",
219+
"raise",
220+
"random_threshold",
221+
"range_threshold",
222+
"red_primary",
223+
"regard_warnings",
224+
"region",
225+
"remote",
226+
"render",
227+
"repage",
228+
"resample",
229+
"resize",
230+
"resize_to_fill",
231+
"resize_to_fit",
232+
"resize_to_limit",
233+
"resize_and_pad",
234+
"respect_parentheses",
235+
"reverse",
236+
"roll",
237+
"rotate",
238+
"sample",
239+
"sampling_factor",
240+
"saver",
241+
"scale",
242+
"scene",
243+
"screen",
244+
"seed",
245+
"segment",
246+
"selective_blur",
247+
"separate",
248+
"sepia_tone",
249+
"shade",
250+
"shadow",
251+
"shared_memory",
252+
"sharpen",
253+
"shave",
254+
"shear",
255+
"sigmoidal_contrast",
256+
"silent",
257+
"similarity_threshold",
258+
"size",
259+
"sketch",
260+
"smush",
261+
"snaps",
262+
"solarize",
263+
"sort_pixels",
264+
"sparse_color",
265+
"splice",
266+
"spread",
267+
"statistic",
268+
"stegano",
269+
"stereo",
270+
"storage_type",
271+
"stretch",
272+
"strip",
273+
"stroke",
274+
"strokewidth",
275+
"style",
276+
"subimage_search",
277+
"swap",
278+
"swirl",
279+
"synchronize",
280+
"taint",
281+
"text_font",
282+
"threshold",
283+
"thumbnail",
284+
"tile_offset",
285+
"tint",
286+
"title",
287+
"transform",
288+
"transparent",
289+
"transparent_color",
290+
"transpose",
291+
"transverse",
292+
"treedepth",
293+
"trim",
294+
"type",
295+
"undercolor",
296+
"unique_colors",
297+
"units",
298+
"unsharp",
299+
"update",
300+
"valid_image",
301+
"view",
302+
"vignette",
303+
"virtual_pixel",
304+
"visual",
305+
"watermark",
306+
"wave",
307+
"wavelet_denoise",
308+
"weight",
309+
"white_balance",
310+
"white_point",
311+
"white_threshold",
312+
"window",
313+
"window_group",
314+
].concat(ActiveStorage.supported_image_processing_methods)
315+
316+
UNSUPPORTED_IMAGE_PROCESSING_ARGUMENTS = ActiveStorage.unsupported_image_processing_arguments
317+
23318
class << self
24319
# Returns a Variation instance based on the given variator. If the variator is a Variation, it is
25320
# returned unmodified. If it is a String, it is passed to ActiveStorage::Variation.decode. Otherwise,
@@ -56,12 +351,15 @@ def initialize(transformations)
56351
def transform(image)
57352
ActiveSupport::Notifications.instrument("transform.active_storage") do
58353
transformations.each do |name, argument_or_subtransformations|
354+
validate_transformation(name, argument_or_subtransformations)
59355
image.mogrify do |command|
60356
if name.to_s == "combine_options"
61357
argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument|
358+
validate_transformation(subtransformation_name, subtransformation_argument)
62359
pass_transform_argument(command, subtransformation_name, subtransformation_argument)
63360
end
64361
else
362+
validate_transformation(name, argument_or_subtransformations)
65363
pass_transform_argument(command, name, argument_or_subtransformations)
66364
end
67365
end
@@ -86,4 +384,58 @@ def pass_transform_argument(command, method, argument)
86384
def eligible_argument?(argument)
87385
argument.present? && argument != true
88386
end
387+
388+
def validate_transformation(name, argument)
389+
method_name = name.to_s.gsub("-","_")
390+
391+
unless SUPPORTED_IMAGE_PROCESSING_METHODS.any? { |method| method_name == method }
392+
raise UnsupportedImageProcessingMethod, <<~ERROR.squish
393+
One or more of the provided transformation methods is not supported.
394+
ERROR
395+
end
396+
397+
if argument.present?
398+
if argument.is_a?(String) || argument.is_a?(Symbol)
399+
validate_arg_string(argument)
400+
elsif argument.is_a?(Array)
401+
validate_arg_array(argument)
402+
elsif argument.is_a?(Hash)
403+
validate_arg_hash(argument)
404+
end
405+
end
406+
end
407+
408+
def validate_arg_string(argument)
409+
if UNSUPPORTED_IMAGE_PROCESSING_ARGUMENTS.any? { |bad_arg| argument.to_s.downcase.include?(bad_arg) }; raise UnsupportedImageProcessingArgument end
410+
end
411+
412+
def validate_arg_array(argument)
413+
argument.each do |arg|
414+
if arg.is_a?(Integer) || arg.is_a?(Float)
415+
next
416+
elsif arg.is_a?(String) || arg.is_a?(Symbol)
417+
validate_arg_string(arg)
418+
elsif arg.is_a?(Array)
419+
validate_arg_array(arg)
420+
elsif arg.is_a?(Hash)
421+
validate_arg_hash(arg)
422+
end
423+
end
424+
end
425+
426+
def validate_arg_hash(argument)
427+
argument.each do |key, value|
428+
validate_arg_string(key)
429+
430+
if value.is_a?(Integer) || value.is_a?(Float)
431+
next
432+
elsif value.is_a?(String) || value.is_a?(Symbol)
433+
validate_arg_string(value)
434+
elsif value.is_a?(Array)
435+
validate_arg_array(value)
436+
elsif value.is_a?(Hash)
437+
validate_arg_hash(value)
438+
end
439+
end
440+
end
89441
end

activestorage/lib/active_storage.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,6 @@ module ActiveStorage
5050
mattr_accessor :content_types_to_serve_as_binary, default: []
5151
mattr_accessor :content_types_allowed_inline, default: []
5252
mattr_accessor :binary_content_type, default: "application/octet-stream"
53+
mattr_accessor :supported_image_processing_methods, default: []
54+
mattr_accessor :unsupported_image_processing_arguments
5355
end

0 commit comments

Comments
 (0)