Posted on:
June 15, 2011
by Wolfram Arnold

Access Control 101 in Rails and the Citibank Hack

The New York Times reported two days ago that a trivial vulnerability on Citibank’s consumer credit card web site was exploited by hackers to retrieve “names, account numbers, e-mail addresses and transaction histories of more than 200,000 Citi customers”. In this article we show how this vulernability can exists in a Rails application and how to avoid it easily.

On June 13, 2011, the New York Times published an article about a trivial security vulenerability on the Citibank’s consumer credit card web site.

In the Citi breach, the data thieves were able to penetrate the bank’s defenses by first logging on to the site reserved for its credit card customers.

Once inside, they leapfrogged between the accounts of different Citi customers by inserting vari-ous account numbers into a string of text located in the browser’s address bar. The hackers’ code systems automatically repeated this exercise tens of thousands of times — allowing them to capture the confidential private data.

Many of the reader comments on the New York Times article mention that this is a simple exploit and is part of basic web security awareness that any developer should be familiar with. I can’t help but agree. The existence of such a vulnerability on a the site of a major credit card issuer is a sad and frightening commentary on the state of the art of the software development culture that is responsible for this.

A small, if after-the-fact, saving grace is that Citi’s own monitoring systems picked up the breach.

If the New York Times reporting is accurate–which I was not able to independently verify–then the vulnerability was something as trivial as changing the resource ID in the URL.

In a Rails analogy with resourceful routing scheme, you would have an accounts controller that takes an account ID:

    controller AccountsController < ApplicationController

        before_filter :authenticate_user!  # make sure user is logged in

        def show
            @account = Account.find(params[:id])
        end

    end
    

If the show action is implemented as above, then swapping the ID on the url, e.g. from http://accounts/12345 to http://accounts/56789 or to any other ID would let the user see that particular account.

The fact that the user is logged in, as enforced here with the before filter, does nothing to prevent this breach.

Authentication vs. Authorization

This is a classic security hole and arises from the misconception that authentication (ensuring the user is logged in) is the same thing as authorization (ensuring the user has access rights to a given resource). Authentication and authorization require two separate lines of defense.

The Citibank example illustrates that its developers were not aware of this very basic concept or the ensuing security hole. Further conclusions are even less pretty, such as the New York Times quoting a so-called security “expert” familiar with the investigation:

“It would have been hard to prepare for this type of vulnerability,”

Human Factor

So long you have untrained developers and an organization where even the security experts don’t understand basic access privileges, it’s probably impossible to build systems that follow basic security principles. Better frameworks, coding tools or technologies won’t help without the human and cultural factors being addresses first.

Once you have basic awareness in place, technologies and frameworks can make a difference in how they support developers in their mission of securing systems.

Rails can be very helpful in that, when used correctly. Associations are your friend.

Simple case: one account per user

To avoid a vulnerability like the one above, here are some basic ideas. Let’s first look at the case where there is a 1-to-1 mapping between a user and the resouces, i.e. in this example one account per user.

Disable the RESTful actions that require an ID

This sounds simple, and it is. Where there is no ID to use, there is no fake ID to pass in. The RESTful methods with ID are: update, delete, edit, show.

Disabling them is also easy, in config/routes.rb:

              resources :accounts, :only => [:create, :new, :index]
          

Access the user’s account through associations only

You might ask, without an ID, how can I get to the account? The answer lies in the logged-in user. Since we have an authenticated user, often in the form of a current_user method (e.g. from the Devise gem), and we have a foreign key that links an account to a user, we can leverage ActiveRecords’s associations to look up the account through the user:

              class User < ActiveRecord::Base
                has_one :account
              end

              controller AccountsController < ApplicationController
                before_filter :authenticate_user!  # make sure user is logged in

                def index
                  @account = current_user.account
                end
              end
          

This works in situations where there is a 1-to-1 relationship between the user and the resouce. This is true, in particular, for the user resource itself. So for editing, updating, or viewing the user record (for the logged-in user themselves), you don’t need RESTful actions with ID.

For super-user access, the situation may be different, certainly, and then the story gets more involved. But the needs of administrative super-user access are often different enough that that they are implemented in their own controllers, with specific authorization and access controls.

What about a user with multiple accounts?

More commonly, you’ll still have a one-to-many relationship, as is true in the Citibank case as well.

      class User < ActiveRecord::Base
        has_many :accounts
      end
    

We can’t do away with the RESTful actions that have an ID, because we still need to distinguish between the different accounts of a given user. However, the problematic vulnerability from above can still be averted through association proxy methods, as all the methods defined on the association object are called.

What are Association Proxy Methods?

It’s a legitimate question. When you call

        user.accounts    # => [#<Account id: nnn, ...>, ...]
    

you get something that behaves like an array–i.e. you can iterate over the results, etc.–but is not actually an array. It’s a Association Proxy. In addition to behaving like an array, Association Proxies have other methods defined on them, such as:

For the full list see the Rails API documentation on Associations.

These proxies operate through the assocation, that means they constrain the results by the foreign key using a SQL clause like: WHERE(accounts.user_id = 423).

In other words, we can now look up a user’s accounts like this:

        user.accounts.find(12345)    # => #<Account id: 12345, ...>
    

We thereby enjoy implicit access rights control. The controller action would look like this:

      controller AccountsController < ApplicationController
        before_filter :authenticate_user!  # make sure user is logged in

        def show
          @account = current_user.accounts.find(params[:id])
        end
      end
    

There are certainly more complex cases that require specific access rights for groups of users, also known as role-based authorization. A good gem for this is Declarative Authorization.

Conclusion

In summary, there is a difference between authentication (log-in) and authorization (access rights to a specific object). In keeping security practices sound, it’s important to understand this difference and to address each with a specific tool.

For this article I assumed that authentication is handled already (e.g. by the Devise gem). Authorization requires tying the object (here, accounts) to the authorized party (here, the logged-in user). Rails associations and particularly the Association Proxy Methods and Finders are an easy and elegant way to accomplish this in Rails.

I don’t know what framework the Citibank site is built on, and I wonder if it has a similarly elegant mechanism available. That said, even the most elegant mechanism won’t help if developers aren’t aware of the problem in the first place and the culture they work in is not encouraging continuous improvement, training and innovation.

blog comments powered by Disqus