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
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
And if you get a 200
you know the file exists:
1 2 3 4 5 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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!