Update: see also the article Securing the Rails session secret.

Update 2: a statement from Michael Koziarski of the Rails security team regarding the severity of this bug has been added. He urges people to upgrade immediately. Please scroll to the “Conclusion” section for details.

Update 3: new advisories (CVE-2013-0155 and CVE-2013-0156) have been published. These vulnerabilities are unrelated to the one reported in this blog post, but are extremely critical. Upgrade immediately.

Yesterday a Ruby on Rails SQL injection vulnerability was announced which affects all versions. This immediately received widespread attention on Hacker News. Unfortunately the announcement doesn’t clearly explain how the vulnerability exactly works, which caused a lot of confusion and unnecessary panic, especially among people who are less familiar with Ruby or Rails.

Here are the facts, along with a clear explanation for non-Rails people.

Summary: what is this vulnerability?

The bug allows SQL injection through dynamic finder methods (e.g. find_by_foo(params[:foo]) ). I will explain dynamic finders in a bit.

). I will explain dynamic finders in a bit. The bug affects all Ruby on Rails versions.

A known exploitable scenario is when all of the following applies: You’re using Authlogic (a third party but popular authentication library). You must know the session secret token. There are other exploitable scenarios, but it really depends on what your app is doing. Since it is impossible to prove that something isn’t insecure, you should take the vulnerability seriously and upgrade anyway even if you think you aren’t affected.



What is this vulnerability NOT?

For those who know Rails:

The bug does not affect normal finder methods (e.g. find(params[:id]) ).

). The bug is not exploitable through request parameters.

The bug is not in Authlogic. It’s in Rails. It just so happens that Authlogic triggers it.

Devise (another third-party authentication library) does not trigger the bug.

Point 6, as described at Rails Security Digest. ‘params’ Case, is a totally different and unrelated issue. The issue described there is quite severe and deserves serious attention, so please keep your eye open on any new advisories.

For those who do not know Rails:

It does not mean all unupgraded Rails apps are suddenly widely vulnerable.

It does not mean Rails doesn’t escape SQL inputs.

It does not mean Rails doesn’t provide parameterized SQL APIs.

It does not mean Rails encourages code that are inherently prone to SQL injection. The code should be safe but due to a subtlety was not. This has been fixed.

The main exploitable scenario

Rails provides finder methods for all ActiveRecord (database) models. For example, to lookup a user using a primary key that was provided through the “id” request parameter, one would usually write:

User.find(params[:id])

Rails also provides so-called “dynamic finder methods”. It generates a “find_by_*” method for all database columns in model. If your “users” table have the “id”, “name” and “phone” columns, then it will generate methods so you can write things like this:

User.find_by_id(params[:id]) User.find_by_name(params[:name]) User.find_by_phone(params[:name])

The vulnerability is in these dynamic finder methods, not in the normal and often-used find method.

ActiveRecord protects you against SQL injection by escaping input for you. For example the following works as expected, with no vulnerability:

User.find_by_name("kotori'; DROP TABLE USERS; --") # => SELECT * FROM users WHERE name = 'kotori\'; DROP TABLE USERS; --' LIMIT 1

But ActiveRecord also defines ways for the programmer to inject SQL fragments into the query so that the programmer can customize the query when necessary. The injection interfaces are documented and the programmer is not supposed to pass user input to those interfaces. Normally, the strings passed to the injection interfaces are constant strings that never change. One of those injection interfaces is the options parameter (normally second parameter) for the “find_by_*” methods:

# Fetches a user record by name, but only fetch the 'id' and 'name' fields. User.find_by_name("kotori", :select => "id, name") # => SELECT id, name FROM users WHERE name = 'kotori' LIMIT 1 # You can inject arbitrary SQL if you wish: User.find_by_name("kotori", :select => "id, name FROM users; DROP TABLE users; --") # => SELECT id, name FROM users; DROP TABLE users; -- FROM users WHERE name = 'kotori' LIMIT 1

The vulnerability lies in the fact that “find_by_*” also accepted calls in which only the options parameter is given. In that case, it thinks that the value parameter is nil.

User.find_by_name(:select => "1; DROP TABLE users; --") # => SELECT 1; DROP TABLE users; -- FROM users WHERE name IS NULL LIMIT 1;

Not many people ever use the second parameter, but code of the following form is quite common:

User.find_by_name(params[:name])

params[:name] is normally a string. Can an attacker somehow ensure that params[:name] is an options hash? Yes. Rails converts request parameters of a certain form into hashes. Suppose you call the controller method like this:

/example-url?name[select]=whatever&name[limit]=23

params[:name] is now a hash: { "select" => "whatever", "limit" => 23 }

However, this is not exploitable. Ruby has two datatypes, strings and symbols. Symbols are kind of like string constants. You’ve seen them before in this article: :select is a symbol. The vulnerability can only be triggered when the keys are symbols, but the Rails-generated request parameter hashes all have string keys thanks to the way HashWithIndifferentAccess works.

An attacker can only exploit this if the application somehow passes an arbitrary hash to “find_by_*”, yet with symbol keys. We now bring in the second part of the puzzle: Authlogic. This exploit is described here and works as follows.

Authlogic accepts authentication credentials through multiple ways: cookies, Rails session data, HTTP basic authentication, etc. All user accounts have a so-called persistence token, and the user must provide this persistence token through one of the authentication methods in order to authenticate himself. Authlogic looks up the user associated with the persistence token using roughly the following call:

User.find_by_persistence_token(the_token)

Can an attacker ensure that the_token is an options hash? Yes, but only through the Rails session data authentication method. In all the other methods, the_token is always a string.

The Rails session mechanism allows storing arbitrary Ruby objects, including hashes with symbol keys. Rails provides a variety of session stores, the default being the cookie store which stores session data in a cookie on the client. The cookie data is not encrypted, but is signed with an HMAC to prevent tampering. The cookie store is fast, does not require any server-side maintenance, and is only meant for session data that do not contain sensitive information such as credit card numbers. Apps that store sensitive information in the session should use the database session store instead. Nevertheless, it turned out that 95% of all Rails apps only ever store the user authentication credentials in the session, so the cookie store was made the default.

So to inject arbitrary SQL, you need to tamper with the cookie, which requires the HMAC key. The HMAC key is the so-called session secret. As the name implies, it is supposed to be secret. Rails generates a random 512-bit secret upon project creation. This is why most Rails apps that are running Authlogic are not exploitable: the attacker does not know the secret. Open source Rails apps however can form a problem. Many of them come with a default session secret, but the user never customizes them, so all those instances end up using the same HMAC key, making them very easily exploitable. Of course, in this case the operator have to worry about more than just SQL injection. If the HMAC key is known then anybody can send fake credentials to the app.

Other exploitable scenarios

Your code is vulnerable if you call Foo.find_by_whatever(bar) , where bar can be an arbitrary user-specified hash with symbol keys. HashWithIndifferentAccess stores keys as strings, not symbols, so that does not trigger the vulnerability.

Mitigation

There are several ways to mitigate this.

Upgrade to the latest Rails version. This solves everything, you don’t need to do anything else. “find_by_*” has been patched so that the first parameter may not be an options hash. Ensure that you only pass strings or integers to “find_by_*”, e.g. find_by_name(params[:name].to_s) . This requires changing all code, including third party code. I do not recommend this as you’ll likely overlook things. If you upgrade Rails, you don’t need to do this. Keep your session secret secret! If you write open source Rails apps, make sure the user generates a different session secret upon installation. Don’t let them use the default one.

Demo app

We’ve put together a demo app which shows that the bug is not exploitable through request parameters. Setup this app, run it, and try to attack it as follows:

curl 'http://127.0.0.1:3000/?id\[limit\]=1'

You will see that this attack does not succeed in injecting SQL.

Conclusion

So here you have it. Some folks on Hacker News asked “how can this giant bug be overlooked”? As you can see, it is not a “giant bug”, it is much more subtle than that and requires a specific combination of code and circumstances to work. Most apps are not vulnerable.

Update: Michael Koziarski of the Rails security team said the following:

“When we told people they should upgrade immediately we meant it. It *is* exploitable under some circumstances, so people should be upgrading immediately to avoid the risk.”

References