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.
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,”
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.
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.
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]
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.
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.
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.
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