Skip to content

Commit

Permalink
Added image transformation validation via configurable allow-list.
Browse files Browse the repository at this point in the history
Variant now offers a configurable allow-list for
transformation methods in addition to a configurable deny-list for arguments.

[CVE-2022-21831]
  • Loading branch information
fresh-eggs authored and tenderlove committed Mar 8, 2022
1 parent 46fe51b commit 94e2f00
Show file tree
Hide file tree
Showing 5 changed files with 473 additions and 0 deletions.
352 changes: 352 additions & 0 deletions activestorage/app/models/active_storage/variation.rb
Expand Up @@ -20,6 +20,301 @@
class ActiveStorage::Variation
attr_reader :transformations

class UnsupportedImageProcessingMethod < StandardError; end
class UnsupportedImageProcessingArgument < StandardError; end

SUPPORTED_IMAGE_PROCESSING_METHODS = [
"adaptive_blur",
"adaptive_resize",
"adaptive_sharpen",
"adjoin",
"affine",
"alpha",
"annotate",
"antialias",
"append",
"apply",
"attenuate",
"authenticate",
"auto_gamma",
"auto_level",
"auto_orient",
"auto_threshold",
"backdrop",
"background",
"bench",
"bias",
"bilateral_blur",
"black_point_compensation",
"black_threshold",
"blend",
"blue_primary",
"blue_shift",
"blur",
"border",
"bordercolor",
"borderwidth",
"brightness_contrast",
"cache",
"canny",
"caption",
"channel",
"channel_fx",
"charcoal",
"chop",
"clahe",
"clamp",
"clip",
"clip_path",
"clone",
"clut",
"coalesce",
"colorize",
"colormap",
"color_matrix",
"colors",
"colorspace",
"colourspace",
"color_threshold",
"combine",
"combine_options",
"comment",
"compare",
"complex",
"compose",
"composite",
"compress",
"connected_components",
"contrast",
"contrast_stretch",
"convert",
"convolve",
"copy",
"crop",
"cycle",
"deconstruct",
"define",
"delay",
"delete",
"density",
"depth",
"descend",
"deskew",
"despeckle",
"direction",
"displace",
"dispose",
"dissimilarity_threshold",
"dissolve",
"distort",
"dither",
"draw",
"duplicate",
"edge",
"emboss",
"encoding",
"endian",
"enhance",
"equalize",
"evaluate",
"evaluate_sequence",
"extent",
"extract",
"family",
"features",
"fft",
"fill",
"filter",
"flatten",
"flip",
"floodfill",
"flop",
"font",
"foreground",
"format",
"frame",
"function",
"fuzz",
"fx",
"gamma",
"gaussian_blur",
"geometry",
"gravity",
"grayscale",
"green_primary",
"hald_clut",
"highlight_color",
"hough_lines",
"iconGeometry",
"iconic",
"identify",
"ift",
"illuminant",
"immutable",
"implode",
"insert",
"intensity",
"intent",
"interlace",
"interline_spacing",
"interpolate",
"interpolative_resize",
"interword_spacing",
"kerning",
"kmeans",
"kuwahara",
"label",
"lat",
"layers",
"level",
"level_colors",
"limit",
"limits",
"linear_stretch",
"linewidth",
"liquid_rescale",
"list",
"loader",
"log",
"loop",
"lowlight_color",
"magnify",
"map",
"mattecolor",
"median",
"mean_shift",
"metric",
"mode",
"modulate",
"moments",
"monitor",
"monochrome",
"morph",
"morphology",
"mosaic",
"motion_blur",
"name",
"negate",
"noise",
"normalize",
"opaque",
"ordered_dither",
"orient",
"page",
"paint",
"pause",
"perceptible",
"ping",
"pointsize",
"polaroid",
"poly",
"posterize",
"precision",
"preview",
"process",
"quality",
"quantize",
"quiet",
"radial_blur",
"raise",
"random_threshold",
"range_threshold",
"red_primary",
"regard_warnings",
"region",
"remote",
"render",
"repage",
"resample",
"resize",
"resize_to_fill",
"resize_to_fit",
"resize_to_limit",
"resize_and_pad",
"respect_parentheses",
"reverse",
"roll",
"rotate",
"sample",
"sampling_factor",
"saver",
"scale",
"scene",
"screen",
"seed",
"segment",
"selective_blur",
"separate",
"sepia_tone",
"shade",
"shadow",
"shared_memory",
"sharpen",
"shave",
"shear",
"sigmoidal_contrast",
"silent",
"similarity_threshold",
"size",
"sketch",
"smush",
"snaps",
"solarize",
"sort_pixels",
"sparse_color",
"splice",
"spread",
"statistic",
"stegano",
"stereo",
"storage_type",
"stretch",
"strip",
"stroke",
"strokewidth",
"style",
"subimage_search",
"swap",
"swirl",
"synchronize",
"taint",
"text_font",
"threshold",
"thumbnail",
"tile_offset",
"tint",
"title",
"transform",
"transparent",
"transparent_color",
"transpose",
"transverse",
"treedepth",
"trim",
"type",
"undercolor",
"unique_colors",
"units",
"unsharp",
"update",
"valid_image",
"view",
"vignette",
"virtual_pixel",
"visual",
"watermark",
"wave",
"wavelet_denoise",
"weight",
"white_balance",
"white_point",
"white_threshold",
"window",
"window_group",
].concat(ActiveStorage.supported_image_processing_methods)

UNSUPPORTED_IMAGE_PROCESSING_ARGUMENTS = ActiveStorage.unsupported_image_processing_arguments

class << self
# Returns a Variation instance based on the given variator. If the variator is a Variation, it is
# returned unmodified. If it is a String, it is passed to ActiveStorage::Variation.decode. Otherwise,
Expand Down Expand Up @@ -56,12 +351,15 @@ def initialize(transformations)
def transform(image)
ActiveSupport::Notifications.instrument("transform.active_storage") do
transformations.each do |name, argument_or_subtransformations|
validate_transformation(name, argument_or_subtransformations)
image.mogrify do |command|
if name.to_s == "combine_options"
argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument|
validate_transformation(subtransformation_name, subtransformation_argument)
pass_transform_argument(command, subtransformation_name, subtransformation_argument)
end
else
validate_transformation(name, argument_or_subtransformations)
pass_transform_argument(command, name, argument_or_subtransformations)
end
end
Expand All @@ -86,4 +384,58 @@ def pass_transform_argument(command, method, argument)
def eligible_argument?(argument)
argument.present? && argument != true
end

def validate_transformation(name, argument)
method_name = name.to_s.gsub("-","_")

unless SUPPORTED_IMAGE_PROCESSING_METHODS.any? { |method| method_name == method }
raise UnsupportedImageProcessingMethod, <<~ERROR.squish
One or more of the provided transformation methods is not supported.
ERROR
end

if argument.present?
if argument.is_a?(String) || argument.is_a?(Symbol)
validate_arg_string(argument)
elsif argument.is_a?(Array)
validate_arg_array(argument)
elsif argument.is_a?(Hash)
validate_arg_hash(argument)
end
end
end

def validate_arg_string(argument)
if UNSUPPORTED_IMAGE_PROCESSING_ARGUMENTS.any? { |bad_arg| argument.to_s.downcase.include?(bad_arg) }; raise UnsupportedImageProcessingArgument end
end

def validate_arg_array(argument)
argument.each do |arg|
if arg.is_a?(Integer) || arg.is_a?(Float)
next
elsif arg.is_a?(String) || arg.is_a?(Symbol)
validate_arg_string(arg)
elsif arg.is_a?(Array)
validate_arg_array(arg)
elsif arg.is_a?(Hash)
validate_arg_hash(arg)
end
end
end

def validate_arg_hash(argument)
argument.each do |key, value|
validate_arg_string(key)

if value.is_a?(Integer) || value.is_a?(Float)
next
elsif value.is_a?(String) || value.is_a?(Symbol)
validate_arg_string(value)
elsif value.is_a?(Array)
validate_arg_array(value)
elsif value.is_a?(Hash)
validate_arg_hash(value)
end
end
end
end
2 changes: 2 additions & 0 deletions activestorage/lib/active_storage.rb
Expand Up @@ -50,4 +50,6 @@ module ActiveStorage
mattr_accessor :content_types_to_serve_as_binary, default: []
mattr_accessor :content_types_allowed_inline, default: []
mattr_accessor :binary_content_type, default: "application/octet-stream"
mattr_accessor :supported_image_processing_methods, default: []
mattr_accessor :unsupported_image_processing_arguments
end

0 comments on commit 94e2f00

Please sign in to comment.