CVE-2021-33564
Description
An argument injection vulnerability in the Dragonfly gem before 1.4.0 for Ruby allows remote attackers to read and write to arbitrary files via a crafted URL when the verify_url option is disabled. This may lead to code execution. The problem occurs because the generate and process features mishandle use of the ImageMagick convert utility.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Argument injection in Dragonfly gem before 1.4.0 allows remote attackers to read/write arbitrary files via crafted URL when verify_url disabled, potentially leading to code execution.
Vulnerability
The Dragonfly gem for Ruby versions before 1.4.0 contains an argument injection vulnerability in the generate and process features, which mishandle the ImageMagick convert utility [1]. When the verify_url option is disabled, a remote attacker can inject arbitrary arguments into the convert command by crafting a malicious URL [4]. This allows unintended file read/write operations [1].
Exploitation
An attacker can send a crafted URL to an exposed Dragonfly server with verify_url disabled. The injected arguments (e.g., -write or -read) allow reading arbitrary files (e.g., /etc/passwd) or writing arbitrary content to files [3]. No authentication or user interaction is required [1]. Proof-of-concept code is publicly available [3].
Impact
Successful exploitation enables arbitrary file read and write on the server, potentially leading to remote code execution (RCE) by writing a malicious file (e.g., a Ruby script or web shell) [1][3]. The attacker gains the privileges of the Dragonfly process, typically the web server user, which can lead to full server compromise [1].
Mitigation
Upgrade to Dragonfly gem version 1.4.0 or later, which fixes the issue [2]. As a workaround, ensure verify_url is enabled (the default is true) to prevent injection [4]. If upgrading is not possible, restrict allowed URLs and disable the generate/process features if not required [1]. The CVE is not listed in the Known Exploited Vulnerabilities (KEV) catalog.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
dragonflyRubyGems | < 1.4.0 | 1.4.0 |
Affected products
2- Dragonfly gem/Dragonfly gemdescription
Patches
125399297bb45Merge branch 'better-security'
23 files changed · +683 −448
lib/dragonfly/content.rb+17 −18 modified@@ -1,8 +1,8 @@ -require 'base64' -require 'forwardable' -require 'dragonfly/has_filename' -require 'dragonfly/temp_object' -require 'dragonfly/utils' +require "base64" +require "forwardable" +require "dragonfly/has_filename" +require "dragonfly/temp_object" +require "dragonfly/utils" module Dragonfly @@ -16,11 +16,10 @@ module Dragonfly # It is acted upon in generator, processor, analyser and datastore methods and provides a standard interface for updating content, # no matter how that content first got there (whether in the form of a String/Pathname/File/etc.) class Content - include HasFilename extend Forwardable - def initialize(app, obj="", meta=nil) + def initialize(app, obj = "", meta = nil) @app = app @meta = {} @previous_temp_objects = [] @@ -79,7 +78,7 @@ def name=(name) # @example "image/jpeg" # @return [String] def mime_type - meta['mime_type'] || app.mime_type_for(ext) + meta["mime_type"] || app.mime_type_for(ext) end # Set the content using a pre-registered generator @@ -93,7 +92,7 @@ def generate!(name, *args) # Update the content using a pre-registered processor # @example - # content.process!(:convert, "-resize 300x300") + # content.process!(:thumb, "300x300") # @return [Content] self def process!(name, *args) app.get_processor(name).call(self, *args) @@ -111,10 +110,10 @@ def analyse(name) # @param obj [String, Pathname, Tempfile, File, Content, TempObject] can be any of these types # @param meta [Hash] - should be json-like, i.e. contain no types other than String, Number, Boolean # @return [Content] self - def update(obj, meta=nil) + def update(obj, meta = nil) meta ||= {} - self.temp_object = TempObject.new(obj, meta['name']) - self.meta['name'] ||= temp_object.name if temp_object.name + self.temp_object = TempObject.new(obj, meta["name"]) + self.meta["name"] ||= temp_object.name if temp_object.name clear_analyser_cache add_meta(obj.meta) if obj.respond_to?(:meta) add_meta(meta) @@ -135,7 +134,7 @@ def add_meta(meta) # "file --mime-type #{path}" # end # # ===> "beach.jpg: image/jpeg" - def shell_eval(opts={}) + def shell_eval(opts = {}) should_escape = opts[:escape] != false command = yield(should_escape ? shell.escape(path) : path) run command, :escape => should_escape @@ -148,7 +147,7 @@ def shell_eval(opts={}) # "/usr/local/bin/generate_text gumfry -o #{path}" # end # @return [Content] self - def shell_generate(opts={}) + def shell_generate(opts = {}) ext = opts[:ext] || self.ext should_escape = opts[:escape] != false tempfile = Utils.new_tempfile(ext) @@ -165,7 +164,7 @@ def shell_generate(opts={}) # "convert -resize 20x10 #{old_path} #{new_path}" # end # @return [Content] self - def shell_update(opts={}) + def shell_update(opts = {}) ext = opts[:ext] || self.ext should_escape = opts[:escape] != false tempfile = Utils.new_tempfile(ext) @@ -176,7 +175,7 @@ def shell_update(opts={}) update(tempfile) end - def store(opts={}) + def store(opts = {}) datastore.write(self, opts) end @@ -188,7 +187,7 @@ def b64_data end def close - previous_temp_objects.each{|temp_object| temp_object.close } + previous_temp_objects.each { |temp_object| temp_object.close } temp_object.close end @@ -199,6 +198,7 @@ def inspect private attr_reader :previous_temp_objects + def temp_object=(temp_object) previous_temp_objects.push(@temp_object) if @temp_object @temp_object = temp_object @@ -215,6 +215,5 @@ def clear_analyser_cache def run(command, opts) shell.run(command, opts) end - end end
lib/dragonfly/image_magick/commands.rb+35 −0 added@@ -0,0 +1,35 @@ +module Dragonfly + module ImageMagick + module Commands + module_function + + def convert(content, args = "", opts = {}) + convert_command = content.env[:convert_command] || "convert" + format = opts["format"] + + input_args = opts["input_args"] if opts["input_args"] + delegate_string = "#{opts["delegate"]}:" if opts["delegate"] + frame_string = "[#{opts["frame"]}]" if opts["frame"] + + content.shell_update :ext => format do |old_path, new_path| + "#{convert_command} #{input_args} #{delegate_string}#{old_path}#{frame_string} #{args} #{new_path}" + end + + if format + content.meta["format"] = format.to_s + content.ext = format + content.meta["mime_type"] = nil # don't need it as we have ext now + end + end + + def generate(content, args, format) + format = format.to_s + convert_command = content.env[:convert_command] || "convert" + content.shell_generate :ext => format do |path| + "#{convert_command} #{args} #{path}" + end + content.add_meta("format" => format) + end + end + end +end
lib/dragonfly/image_magick/generators/convert.rb+0 −19 removed@@ -1,19 +0,0 @@ -module Dragonfly - module ImageMagick - module Generators - class Convert - - def call(content, args, format) - format = format.to_s - convert_command = content.env[:convert_command] || 'convert' - content.shell_generate :ext => format do |path| - "#{convert_command} #{args} #{path}" - end - content.add_meta('format' => format) - end - - end - end - end -end -
lib/dragonfly/image_magick/generators/plain.rb+13 −7 modified@@ -1,25 +1,31 @@ +require "dragonfly/image_magick/commands" +require "dragonfly/param_validators" + module Dragonfly module ImageMagick module Generators class Plain + include ParamValidators - def call(content, width, height, opts={}) + def call(content, width, height, opts = {}) + validate_all!([width, height], &is_number) + validate_all_keys!(opts, %w(colour color format), &is_word) format = extract_format(opts) - colour = opts['colour'] || opts['color'] || 'white' - content.generate!(:convert, "-size #{width}x#{height} xc:#{colour}", format) - content.add_meta('format' => format, 'name' => "plain.#{format}") + + colour = opts["colour"] || opts["color"] || "white" + Commands.generate(content, "-size #{width}x#{height} xc:#{colour}", format) + content.add_meta("format" => format, "name" => "plain.#{format}") end - def update_url(url_attributes, width, height, opts={}) + def update_url(url_attributes, width, height, opts = {}) url_attributes.name = "plain.#{extract_format(opts)}" end private def extract_format(opts) - opts['format'] || 'png' + opts["format"] || "png" end - end end end
lib/dragonfly/image_magick/generators/plasma.rb+10 −6 modified@@ -1,24 +1,28 @@ +require "dragonfly/image_magick/commands" + module Dragonfly module ImageMagick module Generators class Plasma + include ParamValidators - def call(content, width, height, opts={}) + def call(content, width, height, opts = {}) + validate_all!([width, height], &is_number) + validate!(opts["format"], &is_word) format = extract_format(opts) - content.generate!(:convert, "-size #{width}x#{height} plasma:fractal", format) - content.add_meta('format' => format, 'name' => "plasma.#{format}") + Commands.generate(content, "-size #{width}x#{height} plasma:fractal", format) + content.add_meta("format" => format, "name" => "plasma.#{format}") end - def update_url(url_attributes, width, height, opts={}) + def update_url(url_attributes, width, height, opts = {}) url_attributes.name = "plasma.#{extract_format(opts)}" end private def extract_format(opts) - opts['format'] || 'png' + opts["format"] || "png" end - end end end
lib/dragonfly/image_magick/generators/text.rb+67 −58 modified@@ -1,100 +1,111 @@ -require 'dragonfly/hash_with_css_style_keys' +require "dragonfly/hash_with_css_style_keys" +require "dragonfly/image_magick/commands" +require "dragonfly/param_validators" module Dragonfly module ImageMagick module Generators class Text + include ParamValidators FONT_STYLES = { - 'normal' => 'normal', - 'italic' => 'italic', - 'oblique' => 'oblique' + "normal" => "normal", + "italic" => "italic", + "oblique" => "oblique", } FONT_STRETCHES = { - 'normal' => 'normal', - 'semi-condensed' => 'semi-condensed', - 'condensed' => 'condensed', - 'extra-condensed' => 'extra-condensed', - 'ultra-condensed' => 'ultra-condensed', - 'semi-expanded' => 'semi-expanded', - 'expanded' => 'expanded', - 'extra-expanded' => 'extra-expanded', - 'ultra-expanded' => 'ultra-expanded' + "normal" => "normal", + "semi-condensed" => "semi-condensed", + "condensed" => "condensed", + "extra-condensed" => "extra-condensed", + "ultra-condensed" => "ultra-condensed", + "semi-expanded" => "semi-expanded", + "expanded" => "expanded", + "extra-expanded" => "extra-expanded", + "ultra-expanded" => "ultra-expanded", } FONT_WEIGHTS = { - 'normal' => 'normal', - 'bold' => 'bold', - 'bolder' => 'bolder', - 'lighter' => 'lighter', - '100' => 100, - '200' => 200, - '300' => 300, - '400' => 400, - '500' => 500, - '600' => 600, - '700' => 700, - '800' => 800, - '900' => 900 + "normal" => "normal", + "bold" => "bold", + "bolder" => "bolder", + "lighter" => "lighter", + "100" => 100, + "200" => 200, + "300" => 300, + "400" => 400, + "500" => 500, + "600" => 600, + "700" => 700, + "800" => 800, + "900" => 900, } - def update_url(url_attributes, string, opts={}) + IS_COLOUR = ->(param) { + /\A(#\w+|rgba?\([\d\.,]+\)|\w+)\z/ === param + } + + def update_url(url_attributes, string, opts = {}) url_attributes.name = "text.#{extract_format(opts)}" end - def call(content, string, opts={}) + def call(content, string, opts = {}) + validate_all_keys!(opts, %w(font font_family), &is_words) + validate_all_keys!(opts, %w(color background_color stroke_color), &IS_COLOUR) + validate!(opts["format"], &is_word) + opts = HashWithCssStyleKeys[opts] args = [] format = extract_format(opts) - background = opts['background_color'] || 'none' - font_size = (opts['font_size'] || 12).to_i + background = opts["background_color"] || "none" + font_size = (opts["font_size"] || 12).to_i + font_family = opts["font_family"] || opts["font"] escaped_string = "\"#{string.gsub(/"/, '\"')}\"" # Settings args.push("-gravity NorthWest") args.push("-antialias") args.push("-pointsize #{font_size}") - args.push("-font \"#{opts['font']}\"") if opts['font'] - args.push("-family '#{opts['font_family']}'") if opts['font_family'] - args.push("-fill #{opts['color']}") if opts['color'] - args.push("-stroke #{opts['stroke_color']}") if opts['stroke_color'] - args.push("-style #{FONT_STYLES[opts['font_style']]}") if opts['font_style'] - args.push("-stretch #{FONT_STRETCHES[opts['font_stretch']]}") if opts['font_stretch'] - args.push("-weight #{FONT_WEIGHTS[opts['font_weight']]}") if opts['font_weight'] + args.push("-family '#{font_family}'") if font_family + args.push("-fill #{opts["color"]}") if opts["color"] + args.push("-stroke #{opts["stroke_color"]}") if opts["stroke_color"] + args.push("-style #{FONT_STYLES[opts["font_style"]]}") if opts["font_style"] + args.push("-stretch #{FONT_STRETCHES[opts["font_stretch"]]}") if opts["font_stretch"] + args.push("-weight #{FONT_WEIGHTS[opts["font_weight"]]}") if opts["font_weight"] args.push("-background #{background}") args.push("label:#{escaped_string}") # Padding - pt, pr, pb, pl = parse_padding_string(opts['padding']) if opts['padding'] - padding_top = (opts['padding_top'] || pt || 0) - padding_right = (opts['padding_right'] || pr || 0) - padding_bottom = (opts['padding_bottom'] || pb || 0) - padding_left = (opts['padding_left'] || pl || 0) + pt, pr, pb, pl = parse_padding_string(opts["padding"]) if opts["padding"] + padding_top = (opts["padding_top"] || pt).to_i + padding_right = (opts["padding_right"] || pr).to_i + padding_bottom = (opts["padding_bottom"] || pb).to_i + padding_left = (opts["padding_left"] || pl).to_i - content.generate!(:convert, args.join(' '), format) + Commands.generate(content, args.join(" "), format) if (padding_top || padding_right || padding_bottom || padding_left) dimensions = content.analyse(:image_properties) - text_width = dimensions['width'] - text_height = dimensions['height'] - width = padding_left + text_width + padding_right - height = padding_top + text_height + padding_bottom + text_width = dimensions["width"] + text_height = dimensions["height"] + width = padding_left + text_width + padding_right + height = padding_top + text_height + padding_bottom args = args.slice(0, args.length - 2) args.push("-size #{width}x#{height}") args.push("xc:#{background}") args.push("-annotate 0x0+#{padding_left}+#{padding_top} #{escaped_string}") - content.generate!(:convert, args.join(' '), format) + Commands.generate(content, args.join(" "), format) end - content.add_meta('format' => format, 'name' => "text.#{format}") + content.add_meta("format" => format, "name" => "text.#{format}") end private def extract_format(opts) - opts['format'] || 'png' + opts["format"] || "png" end # Use css-style padding declaration, i.e. @@ -103,25 +114,23 @@ def extract_format(opts) # 10 5 10 (top, left/right, bottom) # 10 5 10 5 (top, right, bottom, left) def parse_padding_string(str) - padding_parts = str.gsub('px','').split(/\s+/).map{|px| px.to_i} + padding_parts = str.gsub("px", "").split(/\s+/).map { |px| px.to_i } case padding_parts.size when 1 p = padding_parts.first - [p,p,p,p] + [p, p, p, p] when 2 - p,q = padding_parts - [p,q,p,q] + p, q = padding_parts + [p, q, p, q] when 3 - p,q,r = padding_parts - [p,q,r,q] + p, q, r = padding_parts + [p, q, r, q] when 4 padding_parts else raise ArgumentError, "Couldn't parse padding string '#{str}' - should be a css-style string" end end end - end end end -
lib/dragonfly/image_magick/plugin.rb+20 −25 modified@@ -1,49 +1,48 @@ -require 'dragonfly/image_magick/analysers/image_properties' -require 'dragonfly/image_magick/generators/convert' -require 'dragonfly/image_magick/generators/plain' -require 'dragonfly/image_magick/generators/plasma' -require 'dragonfly/image_magick/generators/text' -require 'dragonfly/image_magick/processors/convert' -require 'dragonfly/image_magick/processors/encode' -require 'dragonfly/image_magick/processors/thumb' +require "dragonfly/image_magick/analysers/image_properties" +require "dragonfly/image_magick/generators/plain" +require "dragonfly/image_magick/generators/plasma" +require "dragonfly/image_magick/generators/text" +require "dragonfly/image_magick/processors/encode" +require "dragonfly/image_magick/processors/thumb" +require "dragonfly/image_magick/commands" +require "dragonfly/param_validators" module Dragonfly module ImageMagick # The ImageMagick Plugin registers an app with generators, analysers and processors. # Look at the source code for #call to see exactly how it configures the app. class Plugin - - def call(app, opts={}) + def call(app, opts = {}) # ENV - app.env[:convert_command] = opts[:convert_command] || 'convert' - app.env[:identify_command] = opts[:identify_command] || 'identify' + app.env[:convert_command] = opts[:convert_command] || "convert" + app.env[:identify_command] = opts[:identify_command] || "identify" # Analysers app.add_analyser :image_properties, ImageMagick::Analysers::ImageProperties.new app.add_analyser :width do |content| - content.analyse(:image_properties)['width'] + content.analyse(:image_properties)["width"] end app.add_analyser :height do |content| - content.analyse(:image_properties)['height'] + content.analyse(:image_properties)["height"] end app.add_analyser :format do |content| - content.analyse(:image_properties)['format'] + content.analyse(:image_properties)["format"] end app.add_analyser :aspect_ratio do |content| attrs = content.analyse(:image_properties) - attrs['width'].to_f / attrs['height'] + attrs["width"].to_f / attrs["height"] end app.add_analyser :portrait do |content| attrs = content.analyse(:image_properties) - attrs['width'] <= attrs['height'] + attrs["width"] <= attrs["height"] end app.add_analyser :landscape do |content| !content.analyse(:portrait) end app.add_analyser :image do |content| begin - content.analyse(:image_properties)['format'] != 'pdf' + content.analyse(:image_properties)["format"] != "pdf" rescue Shell::CommandFailed false end @@ -55,29 +54,25 @@ def call(app, opts={}) app.define(:image?) { image } # Generators - app.add_generator :convert, ImageMagick::Generators::Convert.new app.add_generator :plain, ImageMagick::Generators::Plain.new app.add_generator :plasma, ImageMagick::Generators::Plasma.new app.add_generator :text, ImageMagick::Generators::Text.new # Processors - app.add_processor :convert, Processors::Convert.new app.add_processor :encode, Processors::Encode.new app.add_processor :thumb, Processors::Thumb.new app.add_processor :rotate do |content, amount| - content.process!(:convert, "-rotate #{amount}") + ParamValidators.validate!(amount, &ParamValidators.is_number) + Commands.convert(content, "-rotate #{amount}") end # Extra methods - app.define :identify do |cli_args=nil| + app.define :identify do |cli_args = nil| shell_eval do |path| "#{app.env[:identify_command]} #{cli_args} #{path}" end end - end - end end end -
lib/dragonfly/image_magick/processors/convert.rb+0 −33 removed@@ -1,33 +0,0 @@ -module Dragonfly - module ImageMagick - module Processors - class Convert - - def call(content, args='', opts={}) - convert_command = content.env[:convert_command] || 'convert' - format = opts['format'] - - input_args = opts['input_args'] if opts['input_args'] - delegate_string = "#{opts['delegate']}:" if opts['delegate'] - frame_string = "[#{opts['frame']}]" if opts['frame'] - - content.shell_update :ext => format do |old_path, new_path| - "#{convert_command} #{input_args} #{delegate_string}#{old_path}#{frame_string} #{args} #{new_path}" - end - - if format - content.meta['format'] = format.to_s - content.ext = format - content.meta['mime_type'] = nil # don't need it as we have ext now - end - end - - def update_url(attrs, args='', opts={}) - format = opts['format'] - attrs.ext = format if format - end - - end - end - end -end
lib/dragonfly/image_magick/processors/encode.rb+16 −5 modified@@ -1,18 +1,29 @@ +require "dragonfly/image_magick/commands" + module Dragonfly module ImageMagick module Processors class Encode + include ParamValidators + + WHITELISTED_ARGS = %w(quality) + + IS_IN_WHITELISTED_ARGS = ->(args_string) { + args_string.scan(/-\w+/).all? { |arg| + WHITELISTED_ARGS.include?(arg.sub("-", "")) + } + } - def update_url(attrs, format, args="") + def update_url(attrs, format, args = "") attrs.ext = format.to_s end - def call(content, format, args="") - content.process!(:convert, args, 'format' => format) + def call(content, format, args = "") + validate!(format, &is_word) + validate!(args, &IS_IN_WHITELISTED_ARGS) + Commands.convert(content, args, "format" => format) end - end end end end -
lib/dragonfly/image_magick/processors/thumb.rb+37 −31 modified@@ -1,32 +1,40 @@ +require "dragonfly/image_magick/commands" + module Dragonfly module ImageMagick module Processors class Thumb + include ParamValidators GRAVITIES = { - 'nw' => 'NorthWest', - 'n' => 'North', - 'ne' => 'NorthEast', - 'w' => 'West', - 'c' => 'Center', - 'e' => 'East', - 'sw' => 'SouthWest', - 's' => 'South', - 'se' => 'SouthEast' + "nw" => "NorthWest", + "n" => "North", + "ne" => "NorthEast", + "w" => "West", + "c" => "Center", + "e" => "East", + "sw" => "SouthWest", + "s" => "South", + "se" => "SouthEast", } # Geometry string patterns - RESIZE_GEOMETRY = /\A\d*x\d*[><%^!]?\z|\A\d+@\z/ # e.g. '300x200!' + RESIZE_GEOMETRY = /\A\d*x\d*[><%^!]?\z|\A\d+@\z/ # e.g. '300x200!' CROPPED_RESIZE_GEOMETRY = /\A(\d+)x(\d+)#(\w{1,2})?\z/ # e.g. '20x50#ne' - CROP_GEOMETRY = /\A(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?\z/ # e.g. '30x30+10+10' + CROP_GEOMETRY = /\A(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?\z/ # e.g. '30x30+10+10' - def update_url(url_attributes, geometry, opts={}) - format = opts['format'] + def update_url(url_attributes, geometry, opts = {}) + format = opts["format"] url_attributes.ext = format if format end - def call(content, geometry, opts={}) - content.process!(:convert, args_for_geometry(geometry), opts) + def call(content, geometry, opts = {}) + validate!(opts["format"], &is_word) + validate!(opts["frame"], &is_number) + Commands.convert(content, args_for_geometry(geometry), { + "format" => opts["format"], + "frame" => opts["frame"], + }) end def args_for_geometry(geometry) @@ -37,11 +45,11 @@ def args_for_geometry(geometry) resize_and_crop_args($1, $2, $3) when CROP_GEOMETRY crop_args( - 'width' => $1, - 'height' => $2, - 'x' => $3, - 'y' => $4, - 'gravity' => $5 + "width" => $1, + "height" => $2, + "x" => $3, + "y" => $4, + "gravity" => $5, ) else raise ArgumentError, "Didn't recognise the geometry string #{geometry}" end @@ -54,26 +62,24 @@ def resize_args(geometry) end def crop_args(opts) - raise ArgumentError, "you can't give a crop offset and gravity at the same time" if opts['x'] && opts['gravity'] + raise ArgumentError, "you can't give a crop offset and gravity at the same time" if opts["x"] && opts["gravity"] - width = opts['width'] - height = opts['height'] - gravity = GRAVITIES[opts['gravity']] - x = "#{opts['x'] || 0}" - x = '+' + x unless x[/\A[+-]/] - y = "#{opts['y'] || 0}" - y = '+' + y unless y[/\A[+-]/] + width = opts["width"] + height = opts["height"] + gravity = GRAVITIES[opts["gravity"]] + x = "#{opts["x"] || 0}" + x = "+" + x unless x[/\A[+-]/] + y = "#{opts["y"] || 0}" + y = "+" + y unless y[/\A[+-]/] "#{"-gravity #{gravity} " if gravity}-crop #{width}x#{height}#{x}#{y} +repage" end def resize_and_crop_args(width, height, gravity) - gravity = GRAVITIES[gravity || 'c'] + gravity = GRAVITIES[gravity || "c"] "-resize #{width}x#{height}^^ -gravity #{gravity} -crop #{width}x#{height}+0+0 +repage" end - end end end end -
lib/dragonfly/param_validators.rb+37 −0 added@@ -0,0 +1,37 @@ +module Dragonfly + module ParamValidators + class InvalidParameter < RuntimeError; end + + module_function + + IS_NUMBER = ->(param) { + param.is_a?(Numeric) || /\A[\d\.]+\z/ === param + } + + IS_WORD = ->(param) { + /\A\w+\z/ === param + } + + IS_WORDS = ->(param) { + /\A[\w ]+\z/ === param + } + + def is_number; IS_NUMBER; end + def is_word; IS_WORD; end + def is_words; IS_WORDS; end + + def validate!(parameter, &validator) + return if parameter.nil? + raise InvalidParameter unless validator.(parameter) + end + + def validate_all!(parameters, &validator) + parameters.each { |p| validate!(p, &validator) } + end + + def validate_all_keys!(obj, keys, &validator) + parameters = keys.map { |key| obj[key] } + validate_all!(parameters, &validator) + end + end +end
spec/dragonfly/image_magick/commands_spec.rb+98 −0 added@@ -0,0 +1,98 @@ +require "spec_helper" +require "dragonfly/image_magick/commands" + +describe Dragonfly::ImageMagick::Commands do + include Dragonfly::ImageMagick::Commands + + let(:app) { test_app } + + def sample_content(name) + Dragonfly::Content.new(app, SAMPLES_DIR.join(name)) + end + + describe "convert" do + let(:image) { sample_content("beach.png") } # 280x355 + + it "should allow for general convert commands" do + convert(image, "-scale 56x71") + image.should have_width(56) + image.should have_height(71) + end + + it "should allow for general convert commands with added format" do + convert(image, "-scale 56x71", "format" => "gif") + image.should have_width(56) + image.should have_height(71) + image.should have_format("gif") + image.meta["format"].should == "gif" + end + + it "should work for commands with parenthesis" do + convert(image, "\\( +clone -sparse-color Barycentric '0,0 black 0,%[fx:h-1] white' -function polynomial 2,-2,0.5 \\) -compose Blur -set option:compose:args 15 -composite") + image.should have_width(280) + end + + it "should work for files with spaces/apostrophes in the name" do + image = Dragonfly::Content.new(app, SAMPLES_DIR.join("mevs' white pixel.png")) + convert(image, "-resize 2x2!") + image.should have_width(2) + end + + it "allows converting specific frames" do + gif = sample_content("gif.gif") + convert(gif, "-resize 50x50") + all_frames_size = gif.size + + gif = sample_content("gif.gif") + convert(gif, "-resize 50x50", "frame" => 0) + one_frame_size = gif.size + + one_frame_size.should < all_frames_size + end + + it "accepts input arguments for convert commands" do + image2 = image.clone + convert(image, "") + convert(image2, "", "input_args" => "-extract 50x50+10+10") + + image.should_not equal_image(image2) + image2.should have_width(50) + end + + it "allows converting using specific delegates" do + expect { + convert(image, "", "format" => "jpg", "delegate" => "png") + }.to call_command(app.shell, %r{convert png:/[^']+?/beach\.png /[^']+?\.jpg}) + end + + it "maintains the mime_type meta if it exists already" do + convert(image, "-resize 10x") + image.meta["mime_type"].should be_nil + + image.add_meta("mime_type" => "image/png") + convert(image, "-resize 5x") + image.meta["mime_type"].should == "image/png" + image.mime_type.should == "image/png" # sanity check + end + + it "doesn't maintain the mime_type meta on format change" do + image.add_meta("mime_type" => "image/png") + convert(image, "", "format" => "gif") + image.meta["mime_type"].should be_nil + image.mime_type.should == "image/gif" # sanity check + end + end + + describe "generate" do + let (:image) { Dragonfly::Content.new(app) } + + before(:each) do + generate(image, "-size 1x1 xc:white", "png") + end + + it { image.should have_width(1) } + it { image.should have_height(1) } + it { image.should have_format("png") } + it { image.meta.should == { "format" => "png" } } + end +end
spec/dragonfly/image_magick/generators/convert_spec.rb+0 −19 removed@@ -1,19 +0,0 @@ -require 'spec_helper' - -describe Dragonfly::ImageMagick::Generators::Convert do - let (:generator) { Dragonfly::ImageMagick::Generators::Convert.new } - let (:app) { test_app } - let (:image) { Dragonfly::Content.new(app) } - - describe "calling convert" do - before(:each) do - generator.call(image, "-size 1x1 xc:white", 'png') - end - it {image.should have_width(1)} - it {image.should have_height(1)} - it {image.should have_format('png')} - it {image.meta.should == {'format' => 'png'}} - end - -end -
spec/dragonfly/image_magick/generators/plain_spec.rb+39 −13 modified@@ -1,4 +1,5 @@ -require 'spec_helper' +require "spec_helper" +require "dragonfly/param_validators" describe Dragonfly::ImageMagick::Generators::Plain do let (:generator) { Dragonfly::ImageMagick::Generators::Plain.new } @@ -9,42 +10,67 @@ before(:each) do generator.call(image, 3, 2) end - it {image.should have_width(3)} - it {image.should have_height(2)} - it {image.should have_format('png')} - it {image.meta.should == {'format' => 'png', 'name' => 'plain.png'}} + it { image.should have_width(3) } + it { image.should have_height(2) } + it { image.should have_format("png") } + it { image.meta.should == { "format" => "png", "name" => "plain.png" } } end describe "specifying the format" do before(:each) do - generator.call(image, 1, 1, 'format'=> 'gif') + generator.call(image, 1, 1, "format" => "gif") end - it {image.should have_format('gif')} - it {image.meta.should == {'format' => 'gif', 'name' => 'plain.gif'}} + it { image.should have_format("gif") } + it { image.meta.should == { "format" => "gif", "name" => "plain.gif" } } end describe "specifying the colour" do it "works with English spelling" do - generator.call(image, 1, 1, 'colour' => 'red') + generator.call(image, 1, 1, "colour" => "red") end it "works with American spelling" do - generator.call(image, 1, 1, 'color' => 'red') + generator.call(image, 1, 1, "color" => "red") end it "blows up with a bad colour" do expect { - generator.call(image, 1, 1, 'colour' => 'lardoin') + generator.call(image, 1, 1, "colour" => "lardoin") }.to raise_error(Dragonfly::Shell::CommandFailed) end end describe "urls" do it "updates the url" do url_attributes = Dragonfly::UrlAttributes.new - generator.update_url(url_attributes, 1, 1, 'format' => 'gif') - url_attributes.name.should == 'plain.gif' + generator.update_url(url_attributes, 1, 1, "format" => "gif") + url_attributes.name.should == "plain.gif" end end + describe "param validations" do + { + "color" => "white -write bad.png", + "colour" => "white -write bad.png", + "format" => "png -write bad.png", + }.each do |opt, value| + it "validates bad opts like #{opt} = '#{value}'" do + expect { + generator.call(image, 1, 1, opt => value) + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + end + + it "validates width" do + expect { + generator.call(image, "1 -write bad.png", 1) + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + + it "validates height" do + expect { + generator.call(image, 1, "1 -write bad.png") + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + end end
spec/dragonfly/image_magick/generators/plasma_spec.rb+28 −9 modified@@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe Dragonfly::ImageMagick::Generators::Plasma do let (:generator) { Dragonfly::ImageMagick::Generators::Plasma.new } @@ -10,23 +10,42 @@ generator.call(image, 5, 3) image.should have_width(5) image.should have_height(3) - image.should have_format('png') - image.meta.should == {'format' => 'png', 'name' => 'plasma.png'} + image.should have_format("png") + image.meta.should == { "format" => "png", "name" => "plasma.png" } end it "allows changing the format" do - generator.call(image, 1, 1, 'format' => 'jpg') - image.should have_format('jpeg') - image.meta.should == {'format' => 'jpg', 'name' => 'plasma.jpg'} + generator.call(image, 1, 1, "format" => "jpg") + image.should have_format("jpeg") + image.meta.should == { "format" => "jpg", "name" => "plasma.jpg" } end end describe "urls" do it "updates the url" do url_attributes = Dragonfly::UrlAttributes.new - generator.update_url(url_attributes, 1, 1, 'format' => 'jpg') - url_attributes.name.should == 'plasma.jpg' + generator.update_url(url_attributes, 1, 1, "format" => "jpg") + url_attributes.name.should == "plasma.jpg" end end -end + describe "param validations" do + it "validates format" do + expect { + generator.call(image, 1, 1, "format" => "png -write bad.png") + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + + it "validates width" do + expect { + generator.call(image, "1 -write bad.png", 1) + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + + it "validates height" do + expect { + generator.call(image, 1, "1 -write bad.png") + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + end +end
spec/dragonfly/image_magick/generators/text_spec.rb+51 −20 modified@@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe Dragonfly::ImageMagick::Generators::Text do let (:generator) { Dragonfly::ImageMagick::Generators::Text.new } @@ -7,71 +7,102 @@ describe "creating a text image" do before(:each) do - generator.call(image, "mmm", 'font_size' => 12) + generator.call(image, "mmm", "font_size" => 12) end - it {image.should have_width(20..40)} # approximate - it {image.should have_height(10..20)} - it {image.should have_format('png')} - it {image.meta.should == {'format' => 'png', 'name' => 'text.png'}} + it { image.should have_width(20..40) } # approximate + it { image.should have_height(10..20) } + it { image.should have_format("png") } + it { image.meta.should == { "format" => "png", "name" => "text.png" } } end describe "specifying the format" do before(:each) do - generator.call(image, "mmm", 'format' => 'gif') + generator.call(image, "mmm", "format" => "gif") end - it {image.should have_format('gif')} - it {image.meta.should == {'format' => 'gif', 'name' => 'text.gif'}} + it { image.should have_format("gif") } + it { image.meta.should == { "format" => "gif", "name" => "text.gif" } } end describe "padding" do before(:each) do image_without_padding = image.clone - generator.call(image_without_padding, "mmm", 'font_size' => 12) + generator.call(image_without_padding, "mmm", "font_size" => 12) @width = image_properties(image_without_padding)[:width].to_i @height = image_properties(image_without_padding)[:height].to_i end it "1 number shortcut" do - generator.call(image, "mmm", 'padding' => '10') + generator.call(image, "mmm", "padding" => "10") image.should have_width(@width + 20) image.should have_height(@height + 20) end it "2 numbers shortcut" do - generator.call(image, "mmm", 'padding' => '10 5') + generator.call(image, "mmm", "padding" => "10 5") image.should have_width(@width + 10) image.should have_height(@height + 20) end it "3 numbers shortcut" do - generator.call(image, "mmm", 'padding' => '10 5 8') + generator.call(image, "mmm", "padding" => "10 5 8") image.should have_width(@width + 10) image.should have_height(@height + 18) end it "4 numbers shortcut" do - generator.call(image, "mmm", 'padding' => '1 2 3 4') + generator.call(image, "mmm", "padding" => "1 2 3 4") image.should have_width(@width + 6) image.should have_height(@height + 4) end it "should override the general padding declaration with the specific one (e.g. 'padding-left')" do - generator.call(image, "mmm", 'padding' => '10', 'padding-left' => 9) + generator.call(image, "mmm", "padding" => "10", "padding-left" => 9) image.should have_width(@width + 19) image.should have_height(@height + 20) end it "should ignore 'px' suffixes" do - generator.call(image, "mmm", 'padding' => '1px 2px 3px 4px') + generator.call(image, "mmm", "padding" => "1px 2px 3px 4px") image.should have_width(@width + 6) image.should have_height(@height + 4) end it "bad padding string" do - lambda{ - generator.call(image, "mmm", 'padding' => '1 2 3 4 5') + lambda { + generator.call(image, "mmm", "padding" => "1 2 3 4 5") }.should raise_error(ArgumentError) end end describe "urls" do it "updates the url" do url_attributes = Dragonfly::UrlAttributes.new - generator.update_url(url_attributes, "mmm", 'format' => 'gif') - url_attributes.name.should == 'text.gif' + generator.update_url(url_attributes, "mmm", "format" => "gif") + url_attributes.name.should == "text.gif" + end + end + + describe "param validations" do + { + "font" => "Times New Roman -write bad.png", + "font_family" => "Times New Roman -write bad.png", + "color" => "rgb(255, 34, 55) -write bad.png", + "background_color" => "rgb(255, 52, 55) -write bad.png", + "stroke_color" => "rgb(255, 52, 55) -write bad.png", + "format" => "png -write bad.png", + }.each do |opt, value| + it "validates bad opts like #{opt} = '#{value}'" do + expect { + generator.call(image, "some text", opt => value) + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + end + + ["rgb(33,33,33)", "rgba(33,33,33,0.5)", "rgb(33.5,33.5,33.5)", "#fff", "#efefef", "blue"].each do |colour| + it "allows #{colour.inspect} as a colour specification" do + generator.call(image, "mmm", "color" => colour) + end + end + + ["rgb(33, 33, 33)", "something else", "blue:", "f#ff"].each do |colour| + it "disallows #{colour.inspect} as a colour specification" do + expect { + generator.call(image, "mmm", "color" => colour) + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end end end end
spec/dragonfly/image_magick/plugin_spec.rb+32 −29 modified@@ -1,25 +1,24 @@ -require 'spec_helper' +require "spec_helper" describe "a configured imagemagick app" do - - let(:app){ test_app.configure_with(:imagemagick) } - let(:image){ app.fetch_file(SAMPLES_DIR.join('beach.png')) } + let(:app) { test_app.configure_with(:imagemagick) } + let(:image) { app.fetch_file(SAMPLES_DIR.join("beach.png")) } describe "env variables" do - let(:app){ test_app } + let(:app) { test_app } it "allows setting the convert command" do app.configure do - plugin :imagemagick, :convert_command => '/bin/convert' + plugin :imagemagick, :convert_command => "/bin/convert" end - app.env[:convert_command].should == '/bin/convert' + app.env[:convert_command].should == "/bin/convert" end it "allows setting the identify command" do app.configure do - plugin :imagemagick, :identify_command => '/bin/identify' + plugin :imagemagick, :identify_command => "/bin/identify" end - app.env[:identify_command].should == '/bin/identify' + app.env[:identify_command].should == "/bin/identify" end end @@ -33,7 +32,7 @@ end it "should return the aspect ratio" do - image.aspect_ratio.should == (280.0/355.0) + image.aspect_ratio.should == (280.0 / 355.0) end it "should say if it's portrait" do @@ -60,53 +59,53 @@ end it "should return false for pdfs" do - image.encode('pdf').image?.should be_falsey - end unless ENV['SKIP_FLAKY_TESTS'] + image.encode("pdf").image?.should be_falsey + end unless ENV["SKIP_FLAKY_TESTS"] end describe "processors that change the url" do before do - app.configure{ url_format '/:name' } + app.configure { url_format "/:name" } end - describe "convert" do + describe "thumb" do it "sanity check with format" do - thumb = image.convert('-resize 1x1!', 'format' => 'jpg') + thumb = image.thumb("1x1!", "format" => "jpg") thumb.url.should =~ /^\/beach\.jpg\?.*job=\w+/ thumb.width.should == 1 - thumb.format.should == 'jpeg' - thumb.meta['format'].should == 'jpg' + thumb.format.should == "jpeg" + thumb.meta["format"].should == "jpg" end it "sanity check without format" do - thumb = image.convert('-resize 1x1!') + thumb = image.thumb("1x1!") thumb.url.should =~ /^\/beach\.png\?.*job=\w+/ thumb.width.should == 1 - thumb.format.should == 'png' - thumb.meta['format'].should be_nil + thumb.format.should == "png" + thumb.meta["format"].should be_nil end end describe "encode" do it "sanity check" do - thumb = image.encode('jpg') + thumb = image.encode("jpg") thumb.url.should =~ /^\/beach\.jpg\?.*job=\w+/ - thumb.format.should == 'jpeg' - thumb.meta['format'].should == 'jpg' + thumb.format.should == "jpeg" + thumb.meta["format"].should == "jpg" end end end describe "other processors" do describe "encode" do it "should encode the image to the correct format" do - image.encode!('gif') - image.format.should == 'gif' + image.encode!("gif") + image.format.should == "gif" end it "should allow for extra args" do - image.encode!('jpg', '-quality 1') - image.format.should == 'jpeg' + image.encode!("jpg", "-quality 1") + image.format.should == "jpeg" image.size.should < 2000 end end @@ -117,8 +116,13 @@ image.width.should == 355 image.height.should == 280 end - end + it "disallows bad parameters" do + expect { + image.rotate!("90 -write bad.png").apply + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + end end describe "identify" do @@ -127,5 +131,4 @@ image.identify("-format %h").chomp.should == "355" end end - end
spec/dragonfly/image_magick/processors/convert_spec.rb+0 −88 removed@@ -1,88 +0,0 @@ -require 'spec_helper' - -describe Dragonfly::ImageMagick::Processors::Convert do - - def sample_content(name) - Dragonfly::Content.new(app, SAMPLES_DIR.join(name)) - end - - let(:app){ test_app } - let(:image){ sample_content('beach.png') } # 280x355 - let(:processor){ Dragonfly::ImageMagick::Processors::Convert.new } - - it "should allow for general convert commands" do - processor.call(image, '-scale 56x71') - image.should have_width(56) - image.should have_height(71) - end - - it "should allow for general convert commands with added format" do - processor.call(image, '-scale 56x71', 'format' => 'gif') - image.should have_width(56) - image.should have_height(71) - image.should have_format('gif') - image.meta['format'].should == 'gif' - end - - it "should work for commands with parenthesis" do - processor.call(image, "\\( +clone -sparse-color Barycentric '0,0 black 0,%[fx:h-1] white' -function polynomial 2,-2,0.5 \\) -compose Blur -set option:compose:args 15 -composite") - image.should have_width(280) - end - - it "should work for files with spaces/apostrophes in the name" do - image = Dragonfly::Content.new(app, SAMPLES_DIR.join("mevs' white pixel.png")) - processor.call(image, "-resize 2x2!") - image.should have_width(2) - end - - it "updates the url with format if given" do - url_attributes = Dragonfly::UrlAttributes.new - processor.update_url(url_attributes, '-scale 56x71', 'format' => 'gif') - url_attributes.ext.should == 'gif' - end - - it "allows converting specific frames" do - gif = sample_content('gif.gif') - processor.call(gif, '-resize 50x50') - all_frames_size = gif.size - - gif = sample_content('gif.gif') - processor.call(gif, '-resize 50x50', 'frame' => 0) - one_frame_size = gif.size - - one_frame_size.should < all_frames_size - end - - it "accepts input arguments for convert commands" do - image2 = image.clone - processor.call(image, '') - processor.call(image2, '', 'input_args' => '-extract 50x50+10+10') - - image.should_not equal_image(image2) - image2.should have_width(50) - end - - it "allows converting using specific delegates" do - expect { - processor.call(image, '', 'format' => 'jpg', 'delegate' => 'png') - }.to call_command(app.shell, %r{convert png:/[^']+?/beach\.png /[^']+?\.jpg}) - end - - it "maintains the mime_type meta if it exists already" do - processor.call(image, '-resize 10x') - image.meta['mime_type'].should be_nil - - image.add_meta('mime_type' => 'image/png') - processor.call(image, '-resize 5x') - image.meta['mime_type'].should == 'image/png' - image.mime_type.should == 'image/png' # sanity check - end - - it "doesn't maintain the mime_type meta on format change" do - image.add_meta('mime_type' => 'image/png') - processor.call(image, '', 'format' => 'gif') - image.meta['mime_type'].should be_nil - image.mime_type.should == 'image/gif' # sanity check - end - -end
spec/dragonfly/image_magick/processors/encode_spec.rb+30 −0 added@@ -0,0 +1,30 @@ +require "spec_helper" + +describe Dragonfly::ImageMagick::Processors::Encode do + let (:app) { test_imagemagick_app } + let (:image) { Dragonfly::Content.new(app, SAMPLES_DIR.join("beach.png")) } # 280x355 + let (:processor) { Dragonfly::ImageMagick::Processors::Encode.new } + + it "encodes to a different format" do + processor.call(image, "jpeg") + image.should have_format("jpeg") + end + + describe "param validations" do + it "validates the format param" do + expect { + processor.call(image, "jpeg -write bad.png") + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + + it "allows good args" do + processor.call(image, "jpeg", "-quality 10") + end + + it "disallows bad args" do + expect { + processor.call(image, "jpeg", "-write bad.png") + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + end +end
spec/dragonfly/image_magick/processors/thumb_spec.rb+46 −45 modified@@ -1,100 +1,92 @@ -require 'spec_helper' -require 'ostruct' +require "spec_helper" +require "ostruct" describe Dragonfly::ImageMagick::Processors::Thumb do - let (:app) { test_imagemagick_app } - let (:image) { Dragonfly::Content.new(app, SAMPLES_DIR.join('beach.png')) } # 280x355 + let (:image) { Dragonfly::Content.new(app, SAMPLES_DIR.join("beach.png")) } # 280x355 let (:processor) { Dragonfly::ImageMagick::Processors::Thumb.new } it "raises an error if an unrecognized string is given" do - expect{ - processor.call(image, '30x40#ne!') + expect { + processor.call(image, "30x40#ne!") }.to raise_error(ArgumentError) end describe "resizing" do - it "works with xNN" do - processor.call(image, 'x30') + processor.call(image, "x30") image.should have_width(24) image.should have_height(30) end it "works with NNx" do - processor.call(image, '30x') + processor.call(image, "30x") image.should have_width(30) image.should have_height(38) end it "works with NNxNN" do - processor.call(image, '30x30') + processor.call(image, "30x30") image.should have_width(24) image.should have_height(30) end it "works with NNxNN!" do - processor.call(image, '30x30!') + processor.call(image, "30x30!") image.should have_width(30) image.should have_height(30) end it "works with NNxNN%" do - processor.call(image, '25x50%') + processor.call(image, "25x50%") image.should have_width(70) image.should have_height(178) end describe "NNxNN>" do - it "doesn't resize if the image is smaller than specified" do - processor.call(image, '1000x1000>') + processor.call(image, "1000x1000>") image.should have_width(280) image.should have_height(355) end it "resizes if the image is larger than specified" do - processor.call(image, '30x30>') + processor.call(image, "30x30>") image.should have_width(24) image.should have_height(30) end - end describe "NNxNN<" do - it "doesn't resize if the image is larger than specified" do - processor.call(image, '10x10<') + processor.call(image, "10x10<") image.should have_width(280) image.should have_height(355) end it "resizes if the image is smaller than specified" do - processor.call(image, '400x400<') + processor.call(image, "400x400<") image.should have_width(315) image.should have_height(400) end - end - end describe "cropping" do # Difficult to test here other than dimensions - it "crops" do - processor.call(image, '10x20+30+30') + processor.call(image, "10x20+30+30") image.should have_width(10) image.should have_height(20) end it "crops with gravity" do image2 = image.clone - processor.call(image, '10x8nw') + processor.call(image, "10x8nw") image.should have_width(10) image.should have_height(8) - processor.call(image2, '10x8se') + processor.call(image2, "10x8se") image2.should have_width(10) image2.should have_height(8) @@ -103,77 +95,86 @@ it "raises if given both gravity and offset" do expect { - processor.call(image, '100x100+10+10se') + processor.call(image, "100x100+10+10se") }.to raise_error(ArgumentError) end it "works when the crop area is outside the image" do - processor.call(image, '100x100+250+300') + processor.call(image, "100x100+250+300") image.should have_width(30) image.should have_height(55) end it "crops twice in a row correctly" do - processor.call(image, '100x100+10+10') - processor.call(image, '50x50+0+0') + processor.call(image, "100x100+10+10") + processor.call(image, "50x50+0+0") image.should have_width(50) image.should have_height(50) end - end describe "resize_and_crop" do - it "crops to the correct dimensions" do - processor.call(image, '100x100#') + processor.call(image, "100x100#") image.should have_width(100) image.should have_height(100) end it "resizes before cropping" do image2 = image.clone - processor.call(image, '100x100#') - processor.call(image2, '100x100c') + processor.call(image, "100x100#") + processor.call(image2, "100x100c") image2.should_not equal_image(image) end it "works with gravity" do image2 = image.clone - processor.call(image, '10x10#nw') - processor.call(image, '10x10#se') + processor.call(image, "10x10#nw") + processor.call(image, "10x10#se") image2.should_not equal_image(image) end - end describe "format" do let (:url_attributes) { OpenStruct.new } it "changes the format if passed in" do - processor.call(image, '2x2', 'format' => 'jpeg') - image.should have_format('jpeg') + processor.call(image, "2x2", "format" => "jpeg") + image.should have_format("jpeg") end it "doesn't change the format if not passed in" do - processor.call(image, '2x2') - image.should have_format('png') + processor.call(image, "2x2") + image.should have_format("png") end it "updates the url ext if passed in" do - processor.update_url(url_attributes, '2x2', 'format' => 'png') - url_attributes.ext.should == 'png' + processor.update_url(url_attributes, "2x2", "format" => "png") + url_attributes.ext.should == "png" end it "doesn't update the url ext if not passed in" do - processor.update_url(url_attributes, '2x2') + processor.update_url(url_attributes, "2x2") url_attributes.ext.should be_nil end end describe "args_for_geometry" do it "returns the convert arguments used for a given geometry" do - expect(processor.args_for_geometry('30x40')).to eq('-resize 30x40') + expect(processor.args_for_geometry("30x40")).to eq("-resize 30x40") end end + describe "param validations" do + { + "format" => "png -write bad.png", + "frame" => "0] -write bad.png [", + }.each do |opt, value| + it "validates bad opts like #{opt} = '#{value}'" do + expect { + processor.call(image, "30x30", opt => value) + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + end + end end
spec/dragonfly/param_validators_spec.rb+89 −0 added@@ -0,0 +1,89 @@ +require "spec_helper" +require "dragonfly/param_validators" + +describe Dragonfly::ParamValidators do + include Dragonfly::ParamValidators + + describe "validate!" do + it "does nothing if the parameter meets the condition" do + validate!("thing") { |t| t === "thing" } + end + + it "raises if the parameter doesn't meet the condition" do + expect { + validate!("thing") { |t| t === "ting" } + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + + it "does nothing if the parameter is nil" do + validate!(nil) { |t| t === "thing" } + end + end + + describe "validate_all!" do + it "allows passing an array of parameters to validate" do + validate_all!(["a", "b"]) { |p| /\w/ === p } + expect { + validate_all!(["a", " "]) { |p| /\w/ === p } + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + end + + describe "validate_all_keys!" do + it "allows passing an array of parameters to validate" do + obj = { "a" => "A", "b" => "B" } + validate_all_keys!(obj, ["a", "b"]) { |p| /\w/ === p } + expect { + validate_all_keys!(obj, ["a", "b"]) { |p| /[a-z]/ === p } + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + end + + describe "is_number" do + [3, 3.14, "3", "3.2"].each do |val| + it "validates #{val.inspect}" do + validate!(val, &is_number) + end + end + + ["", "3 2", "hello4", {}, []].each do |val| + it "validates #{val.inspect}" do + expect { + validate!(val, &is_number) + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + end + end + + describe "is_word" do + ["hello", "helLo", "HELLO"].each do |val| + it "validates #{val.inspect}" do + validate!(val, &is_word) + end + end + + ["", "hel%$lo", "hel lo", "hel-lo", {}, []].each do |val| + it "validates #{val.inspect}" do + expect { + validate!(val, &is_word) + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + end + end + + describe "is_words" do + ["hello there", "Hi", " What is Up "].each do |val| + it "validates #{val.inspect}" do + validate!(val, &is_words) + end + end + + ["", "hel%$lo", "What's up", "hel-lo", {}, []].each do |val| + it "validates #{val.inspect}" do + expect { + validate!(val, &is_words) + }.to raise_error(Dragonfly::ParamValidators::InvalidParameter) + end + end + end +end
spec/functional/shell_commands_spec.rb+6 −9 modified@@ -1,33 +1,30 @@ -require 'spec_helper' +require "spec_helper" describe "using the shell" do - let (:app) { test_app } describe "shell injection" do it "should not allow it!" do app.configure_with(:imagemagick) begin - app.generate(:plain, 10, 10, 'white').convert("-resize 5x5 ; touch tmp/stuff").apply + app.generate(:plain, 10, 10, "white; touch tmp/stuff").apply rescue Dragonfly::Shell::CommandFailed end - File.exist?('tmp/stuff').should be_falsey + File.exist?("tmp/stuff").should be_falsey end end describe "env variables with imagemagick" do it "allows configuring the convert path" do - app.configure_with(:imagemagick, :convert_command => '/bin/convert') + app.configure_with(:imagemagick, :convert_command => "/bin/convert") app.shell.should_receive(:run).with(%r[/bin/convert], hash_including) - app.create("").thumb('30x30').apply + app.create("").thumb("30x30").apply end it "allows configuring the identify path" do - app.configure_with(:imagemagick, :identify_command => '/bin/identify') + app.configure_with(:imagemagick, :identify_command => "/bin/identify") app.shell.should_receive(:run).with(%r[/bin/identify], hash_including).and_return("JPG 1 1") app.create("").width end end - end -
spec/spec_helper.rb+12 −14 modified@@ -2,19 +2,19 @@ require "bundler" Bundler.setup(:default, :test) -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib")) $LOAD_PATH.unshift(File.dirname(__FILE__)) -require 'rspec' -require 'dragonfly' -require 'fileutils' -require 'tempfile' -require 'webmock/rspec' -require 'pry' +require "rspec" +require "dragonfly" +require "fileutils" +require "tempfile" +require "webmock/rspec" +require "pry" # Requires supporting files with custom matchers and macros, etc, -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } -SAMPLES_DIR = Pathname.new(File.expand_path('../../samples', __FILE__)) +SAMPLES_DIR = Pathname.new(File.expand_path("../../samples", __FILE__)) RSpec.configure do |c| c.include ModelHelpers @@ -25,8 +25,8 @@ def todo raise "TODO" end -require 'logger' -LOG_FILE = 'tmp/test.log' +require "logger" +LOG_FILE = "tmp/test.log" FileUtils.rm_rf(LOG_FILE) Dragonfly.logger = Logger.new(LOG_FILE) @@ -36,7 +36,7 @@ def todo end end -def test_app(name=nil) +def test_app(name = nil) app = Dragonfly::App.instance(name) app.datastore = Dragonfly::MemoryDataStore.new app.secret = "test secret" @@ -45,8 +45,6 @@ def test_app(name=nil) def test_imagemagick_app test_app.configure do - generator :convert, Dragonfly::ImageMagick::Generators::Convert.new - processor :convert, Dragonfly::ImageMagick::Processors::Convert.new analyser :image_properties, Dragonfly::ImageMagick::Analysers::ImageProperties.new end end
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-j858-xp5v-f8xxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-33564ghsaADVISORY
- github.com/markevans/dragonfly/commit/25399297bb457f7fcf8e3f91e85945b255b111b5ghsax_refsource_MISCWEB
- github.com/markevans/dragonfly/compare/v1.3.0...v1.4.0ghsax_refsource_MISCWEB
- github.com/markevans/dragonfly/issues/513ghsax_refsource_MISCWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/dragonfly/CVE-2021-33564.ymlghsaWEB
- raw.githubusercontent.com/projectdiscovery/nuclei-templates/master/cves/2021/CVE-2021-33564.yamlghsax_refsource_MISCWEB
- zxsecurity.co.nz/research/argunment-injection-ruby-dragonflyghsaWEB
- zxsecurity.co.nz/research/argunment-injection-ruby-dragonfly/mitrex_refsource_MISC
News mentions
0No linked articles in our index yet.