There's been some buzz recently about protect_from_forgery, Rails' built-in anti-CSRF mechanism, and how it's not secure by default. Having found, evaluated, disclosed, and tried to fix issues with it in the past, we decided to perform a thorough evaluation of how severe the problem was.
A slice of RubyGems
The first step was to identify the relevant segment of RubyGems. We've discussed the risks associated with depending on random Rails engines in your applications before; in short, controllers provided by engines which subclass
ActionController::Basemust remember to call
protect_from_forgery in order to not expose dependent applications to CSRF. This is an example of an entirely-avoidable problem that exists due to the lack of sane defaults.
Out of the 125K gems on RubyGems, 25K had a release in 2016, and approximately 2142 were engines.
We got hold of the archives for the last category (560K LoC) and set about analyzing everything.
Determining if a class may be vulnerable seems relatively simple:
- The class should extend
ApplicationController, which is protected)
- There should be no call to
protect_from_forgerywhich originates from it. This part can be flow-insensitive.
Ruby's dynamic nature complicates things. For example, determining the subclass hierarchy is not always possible, given that classes are first-class objects:
defget_classif some_diverging_computation then'ActionController::Base'else'ApplicationController'endendclassSuspiciousController < get_class.constantize# No protect_from_forgery. Is this vulnerable?end
To solve this in the simplest way possible, we flagged classes without a statically-known superclass for manual triaging (pending a more sophisticated abstract interpretation which understands the semantics of things like
constantize). Building call graphs for each library thereafter allowed us to find method calls.
The first round of analysis yielded 517 libraries as possibly vulnerable.
The next step was to sift through the results. Triaging each library showed the false positive rate at a little over 85%. Approximately 77 engine gems were vulnerable to this sort of CSRF attack.
On the vulnerable list were gems with thousands of downloads per version. These aren't obscure gems that no one uses. The combined number of downloads is 56K, with 34K in the top 10.
We're in the process of disclosure with the authors now. More information regarding the issue can be found at this link.
Vulnerabilities like these get introduced all the time with seemingly-innocuous changes. We should not fault individual developers; in general, it's just not easy to reason locally with stateful metaprogramming and cross-cutting constructs like filters. Rails uses these to create clean and polished interfaces, which is fine... as long as the abstractions are watertight. Reality is less ideal.
Really, there's no reason that CSRF vulnerability in this manner should even be possible, and there would not be if
ActionController::Base were protected by default. Leaving it up to individual developers to know, let alone remember the difference between the controller superclasses and implement it time after time is unnecessarily error-prone.
The point is that this class of vulnerabilities is entirely preventable. CSRF protection should be opt-out rather than opt-in. Let's try to encourage better defaults, so people can use Rails at a high level of abstraction and not have to dig into internals for basic things like this.