*.blog

by Justin Collins

Another Reason to Avoid Constantize in Rails

Backstory

Recently, a friend asked me if just calling constantize on user input was dangerous, even if subsequent code did not use the result:

1
params[:class].classify.constantize

Brakeman generates a “remote code execution” warning for this code:

Confidence: High
Category: Remote Code Execution
Check: UnsafeReflection
Message: Unsafe reflection method `constantize` called with parameter value
Code: params[:class].classify.constantize
File: app/controllers/users_controller.rb
Line: 7

But why? Surely just converting a string to a constant (if the constant even exists!) can’t be dangerous, right?

Coincidentally, around that same time I was looking at Ruby deserialization gadgets - in particular this one which mentions that Ruby’s Digest module will load a file based on the module name. For example, Digest::A will try to require 'digest/a':

2.7.0 :001 > require 'digest'
 => true 
2.7.0 :002 > Digest::Whatever
Traceback (most recent call last):
        5: from /home/justin/.rvm/rubies/ruby-2.7.0/bin/irb:23:in `<main>'
        4: from /home/justin/.rvm/rubies/ruby-2.7.0/bin/irb:23:in `load'
        3: from /home/justin/.rvm/rubies/ruby-2.7.0/lib/ruby/gems/2.7.0/gems/irb-1.2.1/exe/irb:11:in `<top (required)>'
        2: from (irb):2
        1: from /home/justin/.rvm/rubies/ruby-2.7.0/lib/ruby/2.7.0/digest.rb:16:in `const_missing'
LoadError (library not found for class Digest::Whatever -- digest/whatever)

The Digest library uses the const_missing hook to implement this functionality.

This made me wonder if constantize and const_missing could be connected, and what the consequences would be.

Constantizing in Rails

The constantize method in Rails turns a string into a constant. If the constant does not exist then a NameError will be raised.

However, it is possible to hook into the constant lookup process in Ruby by defining a const_missing method. If a constant cannot be found in a given module, and that module has const_missing defined, then const_missing will be invoked.

2.7.0 :001 > module X
2.7.0 :002 >   def self.const_missing(name)
2.7.0 :003 >     puts "You tried to load #{name.inspect}"
2.7.0 :004 >   end
2.7.0 :005 > end
 => :const_missing 
2.7.0 :006 > X::Hello
You tried to load :Hello
 => nil

If const_missing is implemented with behavior based on the constant name, such as loading a file or creating a new object, there is an opportunity for malicious behavior.

Some Vulnerable Gems

Fortunately, const_missing is not used very often. When it is, the implementation is not usually exploitable.

Searching across ~1300 gems, I found only ~40 gems with a const_missing implementation.

Of those, the majority were not exploitable because they checked the constant name against expected values or called const_get which raises an exception if the constant does not exist.

One gem, coderay, loads files based on constant names like the Digest library. Also like the Digest library, this does not appear to be exploitable because the files are limited to a single coderay directory.

The next two gems below have memory leaks, which can enable denial of service attacks through memory exhaustion.

Temple

The Temple gem is a foundational gem used by Haml, Slim, and other templating libraries.

In Temple, there is a module called Temple::Mixins::GrammarDSL that implements const_missing like this:

1
2
3
def const_missing(name)
  const_set(name, Root.new(self, name))
end

The method creates a new constant based on the given name and assigns a new object.

This is a memory leak since constants are never garbage collected. If an attacker can trigger it, they can create an unlimited number of permanent objects, using up as much memory as possible.

Unfortunately, it is easy to exploit this code.

Temple::Grammar extends Template::Mixins::GrammarDSL and is a core class for Temple. Let’s see if it is loaded by Haml, a popular templating library often used with Rails:

2.7.0 :001 > require 'haml'
 => true 
2.7.0 :002 > Temple::Grammar
 => Temple::Grammar 

Great! What happens if we try to reference a module that definitely does not exist?

2.7.0 :003 > Temple::Grammar::DefinitelyDoesNotExist
 => #<Temple::Mixins::GrammarDSL::Root:0x000055a79b011060 @grammar=Temple::Grammar, @children=[], @name=:DefinitelyDoesNotExist> 

As can be seen above, the constant is created along with a new object.

To go one step further… does the use of constantize invoke this code?

We can test by loading a Rails console for an application using Haml:

Loading development environment (Rails 6.0.3.2)
2.7.0 :001 > require 'haml'
 => false 
2.7.0 :002 > 'Temple::Grammar::DefinitelyDoesNotExist'.constantize
 => #<Temple::Mixins::GrammarDSL::Root:0x000055ba28031a50 @grammar=Temple::Grammar, @children=[], @name=:DefinitelyDoesNotExist> 

It does!

Any Ruby on Rails application using Haml or Slim that calls constantize on user input (e.g. params[:class].classify.constantize) is vulnerable to a memory leak via this method.

Restforce

A very similar code pattern is implemented in the restforce gem.

The ErrorCode module uses const_missing like this:

1
2
3
4
5
module ErrorCode
  def self.const_missing(constant_name)
    const_set constant_name, Class.new(ResponseError)
  end
end

Nearly the same, except this actually creates new classes, not just regular objects.

We can verify again:

Loading development environment (Rails 6.0.3.2)
2.7.0 :001 > require 'restforce'
 => false 
2.7.0 :002 > Restforce::ErrorCode::WhateverWeWant
 => Restforce::ErrorCode::WhateverWeWant 

This time we get as many new classes as we want.

This has been fixed in Restforce 5.0.0.

Finding and Exploiting Memory Leaks

Finding vulnerable code like this in a production application would be difficult. You would need to guess which parameters might be constantized.

Verifying that you’ve found a memory leak is a little tricky and the two memory leaks described above create very minimal objects.

From what I could estimate, a new Rule object in Temple uses about 300 bytes of memory, while a new class in Restforce was taking up almost 1,000 bytes.

Based on that and my testing, it would take 1 to 4 million requests to use just 1GB of memory.

Given that web applications are usually restarted on a regular basis and it’s not usually a big deal to kill off a process and start a new one, this does not seem particularly impactful.

However, it would be annoying and possibly harmful for smaller sites. For example, the base Heroku instance only has 512MB of memory.

Another note here: Memory leaks are not the worst outcome of an unprotected call to constantize. More likely it can trigger remote code execution. The real issue I am trying to explore here is the unexpected behavior that may be hidden in dependencies.

Conclusions

In short: Avoid using constantize in Rails applications. If you need to use it, check against an allowed set of class names before calling constantize. (Calling classify before checking is okay, though.)

Likewise for const_missing in Ruby libraries. Doing anything dynamic with the constant name (loading files, creating new objects, evaluating code, etc.) should be avoided. Ideally, check against an expected list of names and reject anything else.

In the end, this comes down to the security basics of not trusting user input and strictly validating inputs.

Edit: It seems some language I used above was a little ambiguous, so I tweaked it. Calling classify does not make the code safe - I meant calling classify is not dangerous by itself. It’s the subsequent call to constantize that is dangerous. So you can safely call classify, check against a list of allowed classes, then take the appropriate action.

Comments