Most Rails applications don’t properly sanitise user input when passing it to queries (UPDATE: Rails has fixed the problems raised in this article so it was mostly a Rails problem rather than an application programmer problem). I’m going to use an example to illustrate this problem.
The Scenario
Johnny has been tasked to add a password reset feature to his Rails application. So he adds a reset_token to his User model and a PasswordsController class to the application. When the user forgets their password they type in their email and a reset_token is generated and saved on the User model and a url containing the reset token is sent to the users email address. The url looks like /users/1/passwords/edit?reset_token=kjksldjflskdjf
. This reset token is then checked when the user resets their password. Johnny writes the following code in the PasswordsController:
1 2 3 4 5 6 7 8 9 10 11 |
|
Johnny deploys this new feature to the staging environment and Mary is given the task to test it. Now Mary is quite clever and checks what happens if she removes the reset_token parameter from the url and changes the user id. She visits the url /users/2/passwords/edit
and finds that she can change the password for any user that has not requested their password to be reset. She raises this as a critical bug.
Johnny reproduces the problem on his machine and notices it is is doing the following query:
1 2 |
|
He realises he needs to stop users from not sending the reset_token parameter because if params[:reset_token]
is nil
then they can update any user who hasn’t requested a password reset. He updates the code in PasswordController to the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Mary tries her trick again but it doesn’t work this time. But Mary has more tricks in her bag and this time she uses the url /users/2/passwords/edit?reset_token[]
. Again she is able to change the password for any user that has not had a reset_token generated.
Johnny reproduces the problem on his machine and sees it doing the same query:
1 2 |
|
Johnny is completely stumped as to how nil.blank? could be false. He adds some logging and finds the params[:reset_token]
is actually an array containing a nil element: [nil]
. He decides to fix the problem by calling to_s
on the query parameters.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Not Just Arrays (SQL Manipulation)
If Johnny had a used the where
function instead of find_by_
then an attacker could have exploited it by passing in a Hash
instead of an Array
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
For example Mary could of sent the url /users/2/passwords/edit?reset_token[users.id]=2
. The query then performed would have been:
1 2 |
|
The user is able to change the token filter to a filter on a column of their choice. On previous versions of Rails this attack can be escalated to arbitrary SQL injection. This attack uses the previously fixed issue of SQL injection in table names and columns. This bug was originally not as serious because you would not normally let a user choose arbitrary columns or table names in a query. However, with the SQL Manipulation bug an attacker is now able to change table and column names to perform SQL injection.
1 2 3 4 5 |
|
This Hash
problem is actually a security bug in rails and the rails team has released a patch for it.
Underlying Problem
The problem is developers expect the user input to be a String
but it can also be an Array
or a Hash
and Rails has quite different behaviour if a Hash
or an Array
is passed in. The Hash
is particularly troubling because if you have a filter on column X then a user can change it to be a filter on column Y. Example:
1 2 3 4 |
|
1 2 3 4 |
|
This Hash
trick only seems to work on where
filterings and not find_by
methods:
1 2 3 |
|
Vulnerable Code
- https://github.com/thoughtbot/clearance - Possible to change any users password.
- Rails ( 2.3.x, < 3.2.6, <3.1.6, < 3.0.14) SQL manipulation/SQL injection anywhere there is use of
where()
orfind()
that takes user input.
Fixes
- Rails has released 3.2.6 that fixes both the nil issue and SQL manipulation/injection problems with
Hash
. - Clearance has released a new version 0.6.13 which fixes the problem with nil parameters
Mitigation
It is recommended that you install the Rails patches to fix the Hash
problem and nil problem. Also, with security sensitive code I strongly recommend all query
parameters be coerced to the type you expect them to be. For example if you expect a parameter to be a String
you should call to_s
on it.
Previous Work
The Devise team seem to have been aware of the general problem of users being able to send non-string parameters. They have a ParamFilter
class that forces all parameters to be String
s. It looks like they did this because they had an injection problem with mongoid.
1 2 3 4 5 6 7 |
|
Stay Tuned
We only covered the issues fixed in 3.2.5 and 3.2.4 in this article. There was another variant of the Hash
attack that was fixed in 3.2.6. I will cover
that in a future article and show how to exploit it.