3.22.2012

T5WTPYAFGP - Fix Your URL


At South by Southwest this year, during my talk Defense Against The Dark Arts - ESAPI I covered the "Top 5 Ways To Protect Your Application From Getting Pwnd" [T5WTPYAFGP]. After a couple offline conversations I decided that this would make an excellent series of follow-up blog posts so what follows is the adaptation of that presentation material from that talk. Unlike a lot of other Top-N lists, the goal of this one is not to iterate the flaws, but rather to iterate the solutions.

Additionally each post includes some samples on how you can use ESAPI to implement the solutions discussed.
You can use the navigation below to navigate between each of the posts.

[5] Encrypt Sensitive Information
[4] Become "Big Brother" 
[3] Fix Your URL [Current]

3. Fix Your URL
In the history of dynamic web applications, few things have been more fruitful and more easy than simply changing the value of a parameter on a URL or in a hidden form field. For years we have seen urls that contain something like ?id=1000 which instructs the application to load the data associated with a primary key value of 1000. This isn't rocket science, and it required behavior in most apps, after all it is the very definition of a dynamic web application.

What about a form with a field like this:

<input type="hidden" name="id" value="1000" />

This seems innocent enough, but what if the object on the other side of that id is a sensitive document, or contains sensitive information?

Better yet, what if we change the name of the parameter to accountNumber and the data on the other side of accountNumber contains all of the financial data for the client of a bank? What if that banks name happens to be CitiGroup?

Last year, CitiGroup was the target of an attack that exposed the financial information, including credit card information of 200,000 customers, about 1% of their customer-base - all because of a little problem from the OWASP Top Ten called Insecure Direct Object References. Attackers in this case discovered a parameter that they could simply increment to gain access to user accounts that did not belong to them.

So how exactly do you protect yourself against this problem, I mean we have to have the ability to reference data dynamically and the selection of that data has to come from the client.

Option 1: Data Level Access Control
The best solution to this problem is to implement some kind of data level access control policy. This can be implemented in a number of ways from Row-Level Security in the database layer to implementing an application layer check. Creating and implementing a data level access control policy is a difficult and time consuming task and is extremely hard to get right.

In my SXSW talk I glazed over this approach a little, mainly because it is a field full of many rabbit-holes just waiting for some poor unsuspecting speaker to tumble in. Below are just a few of the potential solutions and some high level analysis of each approach.

Database Row-Level Security
Row-Level and Cell-Level Security are a bit of an ingenious idea that has grown in popularity over the last several year - the fundamental idea is that you limit the data accessible to the session user. While Oracle and SQL Server have implemented this type of functionality natively within their products and documented it somewhat profusely, it isn't quite as straight-forward in MySQL - so that is where we will focus for the purpose of this post.

There are 2 key elements to implementing a Row-Level Security implementation at the database layer:

  1. Federate User Identity to Database Layer
  2. Extensive use of Views
The idea here is that the view acts as the Policy Enforcement Point for the data being requested by the User. There are two approaches to defining the policy for data - add a column to the table, or have a central policy table that is joined in the view definition. I like to opt for the second option simply as a matter of maintainability. 

Envision you have the following table:

create table account (
   id         bigint not null auto_increment,
   type       int not null,
   name       varchar(64) not null,
   primary key (id) 
) engine=InnoDB;

Perhaps you create a policy table (and support tables) with the following schema:

create table rls_policy (
   id         bigint not null auto_increment,
   user_id    bigint not null,
   data_id    bigint not null,
   data_type  int not null,
   read       int(1) not null,
   write      int(1) not null,
   create     int(1) not null,
   delete     int(1) not null,
   primary key (id),
   foreign key (user_id) references user(id),
   foreign key (data_type) references data_type(type),
   index (user_id, data_id, data_type)
) engine=InnoDB;

create table data_type (
   type        varchar(256) not null primary key
) engine=InnoDB;


Now you need to create a view:

create or replace view user_accounts (
   id,
   type,
   name
) 
AS
   select acct.id
        , acct.type
        , acct.name
     from account acct
        , rls_security rls
    where rls.user_id = @app_user_id
      and rls.data_type = 'account',
      and rls.read = 1
 order by 3 asc;

The final step is to instruct your application to set the app_user variable when a connection is checked out and clear it when the connection is returned (assuming you are using a connection pool)
public class RowLevelSecurityConnectionCustomizer extends AbstractConnectionCustomizer {
    @Override
    public void onCheckOut( Connection c, String pdsIdt )
    { 
        User currentUser = ESAPI.authenticator().getCurrentUser();
        try {
            PreparedStatement ps = c.prepareStatement("SET @app_user=?").setInt(Integer.valueOf(currentUser.getAccountID());
            ps.execute();
        } catch (Exception e) {
            // Don't blindly catch exceptions and take no action in production, please for the love of all that is good and holy.
        }
    }
    
    @Override
    public void onCheckIn( Connection c, String pdsIdt )
    { 
        try {
            c.executeUpdate("SET @app_user=NULL");
        } catch (Exception e) {
            // Handle the error
        }
    }

The code sample is not guaranteed to compile and work (I don't remember the last time I interfaced directly with JDBC), but is simply to illustrate the idea.

Now when you would normally query the accounts table, you should be querying the view instead. The results returned in a select * from user_accounts; will result in a subset of the data scoped to the currently logged in application user. This example can be extended even further using triggers and update-able views to add policy enforcement to create, update and delete actions as well.

Application Layer Data Access Control
Another approach is to perform data access control in your application, which has been the standard approach to this problem for some time. The ESAPI implementation ships with a reference implementation of the AccessController which illustrates an example of how to build a data access controller and access control policy.

Once you have your policy configured, you can simply invoke the ESAPI Access Controller to make your policy decisions.
ESAPI.accessController().assertAuthorized("AccessAccountInfo", account);

Option 2: Indirect Object Reference Map
Creating a data level access control policy and implementation is a time consuming and daunting task, and may not make sense for every situation. When Jeff invented the ESAPI he understood and realized this so he created an object that if used correctly would offer a solution to the problem of direct references and offer some deny by default access control to data as well. Using this object is very simple, the idea is to populate the Map with all the data available to the application user first, then refer to each piece of data be it's indirect key rather than it's primary key.

What you end up with is something similar to this (continuing on our accounts example from above)

List<account> accounts = AccountService.fetchAccounts();
AbstractAccessReferenceMap<account> accountReferenceMap = new AbstractAccessReferenceMap(accounts.size());
for (Account a : accounts) {
   accountReferenceMap.addDirectReference(a);
}

session.setAttribute("accounts", accounts);
session.setAttribute("accountsRef" accountReferenceMap);
And a view layer that looks something like this:
<%
   List<Account> accounts = ESAPI.httpUtilities().getSessionAttribute("accounts");
   AbstractAccessReferenceMap<Account> accountRefs = ESAPI.httpUtilities().getSessionAttribute("accountRefs");

   for (Account a : accounts) {
%>
<a href="http://draft.blogger.com/view/account?accountID=<%= accountRefs.getIndirectReference(a) %>">View Account <%= a.getName() %></a>
<% } %>
Which leaves you with a URL that looks something like this:
http://my.company.com/application/view/account?accountID=5JtjyJA573JsJ48732Kkojnv
As you can imagine, this makes it extremely unlikely that an attacker would be able to guess the reference to your account (especially since it isn't a static value and is changing every time the list of accounts is built up)
To add a inferred level of data access control to this strategy, you should populate the list with only accounts that are owned by the application user.

So there you have it, your level of risk should determine which approach you take to solving this issue, but the goal should always be the same - don't be like CitiGroup (or one of the countless other thousands of applications that do the same thing)

Stay tuned for tomorrows post for #2 - Validate User Data!

2 comments: