Last week I notified the Ruby on Rails security team about a huge vulnerability that I spotted in the latest stable release of Rails and its related gems. As a result the Rails core team published a security advisory today, urging users to upgrade the json gem to the latest stable release.
Here’s the gist: The default JSON parser can be used to inject malicious objects into the params hash of a Rails application. This allows for tampering withActiveRecord::Base functionality like dynamic finders and attribute assignment, eventually leading to mass assignment of blacklisted attributes or even SQLinjection.
Besides deserializing simple data types, the default JSON parser used by Rails supports deserialization of more complex classes. When parsing a JSON string, the parser honors a special key called json_class. It will try to instantiate this class by calling .json_create on it. Consider the following example:
This mechanism can be used to deserialize JSON into instances of more complex Ruby classes. This isn’t a problem in itself, as nearly no class provided by the standard Rails stack implements a .json_create method, and thus no Rails internal class can be instantiated this way (as opposed to the YAML parser vulnerability, which allowed to instantiate every class). But unfortunately, json >= 1.6.7 ships with a class called JSON::GenericObject which is basically an OpenStruct descendant class:
As you can see above, this class can be used to ‘fake’ method return values: If I’d expect to work on a String somewhere in my code but actually work on anJSON::GenericObject instance, methods like #strip don’t behave as assumed. An attacker can use this behavior to trick Rails into accepting values for mass assignment protected attributes. This works by passing a nested data structure consisting of JSON::GenericObject instances to a model’s initializer. Take a look at an example from my Blog-in-10-minutes app:
As you can see, I’ve created a Post instance and directly assigned a protected attribute — that’s bad. Rails’ mass assignment protection errorneously thinks it always works on simple data types like strings and hashes. It tries to convert the supplied hash keys to strings by calling #stringify_keys on the hash. Afterwards it rejects each attribute that is on the mass assignment blacklist by calling #rejecton the hash. If I pass the above data structure to Post.create, all those method calls are actually handled by my JSON::GenericObject, and Rails thinks it deals with a sanitized hash even though I can freely determine the value of all attributes.
This mechanism works at least for ActiveRecord and Mongoid, other ORMs untested. ActiveRecord is even vulnerable to SQL injection: The abstract database adapter’s ID quoting can be circumvented by providing “pre-quoted” arguments:
I was able to verify this vulnerability remotely against a Rails 3.2.11 app consisting of a simple scaffold. Due to the severity of this issue, all Rails applications should be updated to use the latest stable release of json immediately. Your application is at risk!
Thanks to Michael Koziarski from Rails’ security team and Aaron Patterson for the constructive collaboration on this issue.