/feb 22, 2017

Over 50,000 Ruby developers impacted by CSRF attacks

By Ming Yi Ang

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::Base must 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.

Gem Stats

We got hold of the archives for the last category (560K LoC) and set about analyzing everything.

Finding protect_from_forgery calls

Determining if a class may be vulnerable seems relatively simple:

  1. The class should extend ActionController::Base (and not ApplicationController, which is protected)
  2. There should be no call to protect_from_forgery which 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:

def get_class
  if some_diverging_computation then
    'ActionController::Base'
  else
    'ApplicationController'
  end
end

class SuspiciousController < 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.

Refinements

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.

Vulnerable gems

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.

Sane defaults

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.

Related Posts

By Ming Yi Ang

Ming is a security researcher who is passionate about building security automation tools to aid the discovery of various security issues. Through the discovery from the tools, he has since made contributions to various open-source projects by responsibly disclosing the vulnerability findings he encounters from his research.