Another Reason to Avoid Constantize in Rails
Tags: security, ruby, rails, webdev
Published: 2020-09-14 18:09:59

## Backstory

Recently, a friend asked if _just_ calling `constantize` on user input was dangerous, even if subsequent code did not use the result.

For example, this Rails code turns a query parameter into a constant:

```ruby
params[:class].classify.constantize
```

[Brakeman](https://brakemanscanner.org/) will report 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? How can we execute code with that? Well...

Coincidentally, around that same time I was looking into Ruby deserialization gadgets - in particular [this one](https://www.elttam.com/blog/ruby-deserialization/) 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'`:

```ruby
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`](https://rdoc.info/stdlib/core/Module: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](https://api.rubyonrails.org/classes/String.html#method-i-constantize). 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.

```ruby
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](https://github.com/rubychan/coderay), [loads files based on constant names](https://github.com/rubychan/coderay/blob/d38502167541a1cd1b505a0e468e0098e3ae7538/lib/coderay/helpers/plugin_host.rb#L59-L67) 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](https://github.com/judofyr/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:

```ruby
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:

```ruby
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?

```ruby
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:

```ruby
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](https://github.com/restforce/restforce) gem.

The [ErrorCode module](https://github.com/restforce/restforce/blob/b14175a9072fb6333c1c1b3dce2bb498c022a1b3/lib/restforce.rb#L65-L67) uses `const_missing` like this:

```ruby
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 from the Rails console again:

```ruby
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](https://github.com/restforce/restforce/blob/master/CHANGELOG.md#500-jul-10-2020).**

## Finding and Exploiting Memory Leaks

_Finding_ vulnerable code like this in a production application would be difficult. You would need to just guess which parameters might be `constantize`d.

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

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` first 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.