Lucene search

K
hackeroneGquadros_H1:1154034
HistoryApr 07, 2021 - 3:05 a.m.

Ruby on Rails: Argument/Code Injection via ActiveStorage's image transformation functionality

2021-04-0703:05:50
gquadros_
hackerone.com
4

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

6.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:M/Au:N/C:P/I:P/A:P

0.008 Low

EPSS

Percentile

79.3%

Affected components

Tested on:

  1. activestorage 6.1.3.1
  2. image_processing 1.12.1
  3. mini_magick 4.11.0

Found by

Gabriel Quadros and Ricardo Silva from Conviso Application Security

Description

Intro

ActiveStorage has an image transformation functionality [1, 2, 3, 4, 5, 6] which uses the concept of variants. By their own words [5]:

> Image blobs can have variants that are the result of a set of transformations applied to the original. These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the original.

> Variants rely on ImageProcessing gem for the actual transformations of the file, so you must add gem “image_processing” to your Gemfile if you wish to use variants. By default, images will be processed with ImageMagick using the MiniMagick gem, but you can also switch to the libvips processor operated by the ruby-vips gem).

One example of direct usage can be seen in the docs as:

<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>

This will create an image tag with a variant URL, which when visited will return the avatar image transformed to the new size.

Another example uses the preview() method, which can be used to generate images from videos and PDF files. Once the preview image is generated, it also calls variant() under the hood.

<ul>
  &lt;% @message.files.each do |file| %&gt;
    <li>
      &lt;%= image_tag file.preview(resize_to_limit: [100, 100]) %&gt;
    </li>
  &lt;% end %&gt;
</ul>

Vulnerabilities

First, it is worth noting that the docs [3, 4, 7] do not state anything about it being insecure to pass user-supplied values as arguments to the variant()/preview() methods.

Rails uses the gem ImageProcessing [8] with MiniMagick by default, passing the transformations to the apply method.

File: activestorage/lib/active_storage/transformers/image_processing_transformer.rb

 12 module ActiveStorage                                                          
 13   module Transformers                                                         
 14     class ImageProcessingTransformer &lt; Transformer                            
 15       private                                                                 
 16         def process(file, format:)                                            
 17           processor.                                                          
 18             source(file).                                                     
 19             loader(page: 0).                                                  
 20             convert(format).                                                  
 21             apply(operations).                                                
 22             call                                                              
 23         end

This method passes these operations to the builder object by iterating over them and calling methods providing arguments, as can be seen below.

File: lib/image_processing/chainable.rb

 24     # Add multiple operations as a hash or an array.                          
 25     #                                                                         
 26     #   .apply(resize_to_limit: [400, 400], strip: true)                      
 27     #   # or                                                                  
 28     #   .apply([[:resize_to_limit, [400, 400]], [:strip, true])               
 29     def apply(operations)                                                     
 30       operations.inject(self) do |builder, (name, argument)|                  
 31         if argument == true || argument == nil                                
 32           builder.send(name)                                                  
 33         elsif argument.is_a?(Array)                                           
 34           builder.send(name, *argument)                                       
 35         elsif argument.is_a?(Hash)                                            
 36           builder.send(name, **argument)                                      
 37         else                                                                  
 38           builder.send(name, argument)                                        
 39         end                                                                   
 40       end                                                                     
 41     end

At some point, ImageProcessing passes these operations to MiniMagick via method calling as well:

File: lib/image_processing/processor.rb

 51     # Calls the operation to perform the processing. If the operation is      
 52     # defined on the processor (macro), calls the method. Otherwise calls the 
 53     # operation directly on the accumulator object. This provides a common    
 54     # umbrella above defined macros and direct operations.                    
 55     def apply_operation(name, *args, &block)                                  
 56       receiver = respond_to?(name) ? self : @accumulator                      
 57                                                                               
 58       if args.last.is_a?(Hash)                                                
 59         kwargs = args.pop                                                     
 60         receiver.public_send(name, *args, **kwargs, &block)                   
 61       else                                                                    
 62         receiver.public_send(name, *args, &block)                             
 63       end                                                                     
 64     end

MiniMagick receives these operations by defining a method_missing method, which takes the called methods and convert them to CLI options:

File: lib/mini_magick/tool.rb

260     ##                                                                        
261     # Any undefined method will be transformed into a CLI option              
262     #                                                                         
263     # @example                                                                
264     #   mogrify = MiniMagick::Tool.new("mogrify")                             
265     #   mogrify.adaptive_blur("...")                                          
266     #   mogrify.foo_bar                                                       
267     #   mogrify.command.join(" ") # =&gt; "mogrify -adaptive-blur ... -foo-bar"  
268     #                                                                         
269     def method_missing(name, *args)                                           
270       option = "-#{name.to_s.tr('_', '-')}"                                   
271       self &lt;&lt; option                                                          
272       self.merge!(args)                                                       
273       self                                                                    
274     end

Argument Injection

The first problem arrises when a user-supplied value is passed as input to a hard-coded transformation, such as:

&lt;%= image_tag user.avatar.variant(resize: params[:new_size]) %&gt;

Since Rails params[] can be an array, one thing the attacker could do here is to pass an array and inject arbitrary arguments into the command to be executed (ImageMagick’s convert by default).

Example:

https://example.com/controller?new_size[]=123&new_size[]=-set&new_size[]=comment&new_size[]=MYCOMMENT&new_size[]=-write&new_size[]=/tmp/file.erb

This is going to generate the following command:

convert ORIGINAL_IMAGE -auto-orient -resize 123 -set comment MYCOMMENT -write /tmp/file.erb /tmp/image_processing20210328-23426-63rmm2.png

Which has the effect of writing a file containing user-controlled data anywhere in the system. This could be used easily to achieve RCE against Rails applications by overwriting ERB files, for example.

User-controlled transformation

A second problem arrises when the user is also allowed to choose the kind of transformation to be applied, such as:

&lt;%= image_tag user.avatar.variant(params[:t].to_s =&gt; params[:v].to_s) %&gt;

This is still dangerous since ImageMagick’s convert program has a lot of powerful command-line options and they can be used to compromise the application. For example, the user could pass:

https://example.com/controller?t=write&v=/tmp/file2.erb

This is going to generate the following command:

convert ORIGINAL_IMAGE -auto-orient -write /tmp/file2.erb /tmp/image_processing20210328-23426-63rmm2.png

Which has a similar effect as the previous attack, if we consider the original image is usually user-controlled.

Code Injection

The third problem occurs due the way ImageProcessing passes the operations to the builder object (via send()). There is no filtering to check if the called method is a valid operation and this can be explored by an attacker to execute code.

Consider the same pattern as before:

&lt;%= image_tag user.avatar.variant(params[:t].to_s =&gt; params[:v].to_s) %&gt;

The attacker could pass:

https://example.com/controller?t=eval&v=system("touch /tmp/hacked")

And the Ruby code system(“touch /tmp/hacked”) would be executed.

Recomendations

  1. Add some notes in the documentation to warn developers about the dangers of passing user-supplied data to the affected methods (variant/preview) without sanitization;
  2. Fix the argument injection problem;
  3. Implement an operations whitelist in ImageProcessing, so it won’t call unexpected methods.

References

  1. https://guides.rubyonrails.org/active_storage_overview.html#transforming-images
  2. https://guides.rubyonrails.org/active_storage_overview.html#previewing-files
  3. https://api.rubyonrails.org/v6.1.3.1/classes/ActiveStorage/Blob/Representable.html#method-i-variant
  4. https://api.rubyonrails.org/v6.1.3.1/classes/ActiveStorage/Blob/Representable.html#method-i-preview
  5. https://api.rubyonrails.org/v6.1.3.1/classes/ActiveStorage/Variant.html
  6. https://api.rubyonrails.org/v6.1.3.1/classes/ActiveStorage/Preview.html
  7. https://github.com/rails/rails/issues/32989
  8. https://github.com/janko/image_processing

Impact

Vulnerable code patterns could allow the attacker to achieve RCE.

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

6.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:M/Au:N/C:P/I:P/A:P

0.008 Low

EPSS

Percentile

79.3%