Facebook JavaScript SDK Authorization Vulnerabilities
by Karthikeyan Bhargavan & Chetan Bansal
Project-Team PROSECCO
INRIA Paris-Rocquencourt

Summary

As a part of our study of various security critical Javascript SDKs we did an analysis of the Facebook Connect JS SDK. Since they use HTML5 based PostMessage API we were specifically interested in the way the origins were validated. We managed to bypass the origin validation by exploiting 3 different bugs in their SDK.

These bugs in the Facebook's JavaScript SDK (specifically in the code loaded from xd_arbiter.php) allows a malicious website (M) that a user visits to obtain the user's access tokens for any other website (W) that she has previously authorized. All M needs to know is W's (public) client_id. The attacker M may subsequently use the access token to:

(1) log in to W if it enables "login with Facebook"
(2) read the user's Facebook profile offline
(3) write to the user's Facebook profile feed (if W has this permission).

Notably, using (3), M could post a link to itself on the user's profile feed, and thus spread the attack (like a worm) through the user's social network. The attack is widely applicable; it works whether or not W uses the JavaScript SDK. If W is one of Facebook's instant personalization partners like Bing, Yelp, and TripAdvisor, user does not even have to have previously authorize it, and M immediately gets the user's basic profile details.

Background

Websites such as Facebook, Google, and Live, protect API access to user data through the OAuth 2.0 authorization protocol. To access user data from any of these websites, a web application must first obtain an access token from an authorization server. The token is issued only if the user explicitly gives her permission. A typical use for such tokens is to authenticate a user, by retrieving her email address. This is the basis of single sign-on solutions provided by various social networks (e.g. Login with Facebook). More generally, the token may be used to retrieve user data for personalization, or to post a comment on a user's social profile (e.g. Like buttons).

In its strongest server-side configuration (authorization code grant), the OAuth protocol requires each application to authenticate its token request with its identifier and secret key (obtained during registration), and requires that the authorization server only returns the token to a preregistered application URI. However, when returning tokens to JavaScript clients in the Implicit grant flow, authorization servers are more lax. They do not require application authentication, because storing or using long term secret keys in JavaScript is considered dangerous. They require only that the destination web page be on the same origin as the application's registered URI, because browsers do not offer a reliable way to distinguish one page on an origin from another. If a user authorizes a website to access her Facebook profile, she is authorizing every script on every page that will ever be loaded in that website's origin.

FB JS SDK
Fig 1. Facebook JS SDK Authentication flow

Facebook JS SDK uses iFrames for Cross domain communication. It creates a proxy iFrame on initialization which basically serves as a communication channel between the 3rd party Client Website and Facebook.com. When a website uses the Facebook JavaScript SDK ("http://connect.facebook.net/en_US/all.js") to ask for an access token which is required to fetch the user's profile data (e.g. by calling FB.login), the SDK creates two iframes, the first "Proxy" iframe communicates with the current website, and the second "OAuth" frame communicates with Facebook. If the OAuth iframe gets an access token back from Facebook, it sends the token to the "Proxy" iframe, which then releases the token to the page. The goal of the whole exchange is that the access token should be given to the current website only if the user has authorized it. We show how this goal is broken.

The Proxy iframe is sourced at:
    https://static.ak.facebook.com/connect/xd_arbiter.php
         ?version=11
         #channel=...&
          origin=&
          channel_path=..."
Note the parameter "origin", it contains the origin for the current website. The code downloaded from "xd_arbiter.php" exports a function "proxyMessage"; when this function is called with an access token, it sends it to its parent frame as a postMessage with targetOrigin set to the "origin" parameter. Hence, only a website with this "origin" will get access to the token.

The OAuth iframe is sourced at:
    https://www.facebook.com/dialog/oauth
         ?client_id=&
          response_type=token code&
          domain=&
          redirect_uri=https://static.ak.facebook.com/connect/xd_arbiter.php
                            ?version=11
                            &cb=...
                            &origin=
                            &relation=parent
Here, the "domain" parameter is unimportant, it seems to be optional and typically ignored by all parties. However, the "origin" parameter in the "redirect_uri" must match the "client_id" for which the access token is requested. If they match, and the user has previously authorized an app with this "origin", the Facebook OAuth server will issue an access token and redirect this frame to the location:
    https://static.ak.facebook.com/connect/xd_arbiter.php
        ?version=11
        &cb=...
        &origin=
        &relation=parent
        #access_token=AAABBBCCC...&
        code=XXYYZZ
Now, the JavaScript code downloaded from "xd_arbiter.php" looks for the Proxy iframe in the parent page and calls its "proxyMessage" function with the access token. (Since both frames are now on the same origin, they can directly call each other's functions.) We found three different ways of confusing the Proxy iframe into releasing its access token to an unauthorized website:

Attack 1

Important Dates

November 1, 2012: date of report
November 2-3, 2012: old bugfix pushed out to CDN

Details

The first attack relies on the fact that although the OAuth iframe only obtains access tokens for an authorized origin W and the Proxy iframe only releases access tokens to the origin in its fragment identifier, there is no check guaranteeing that these origins are the same. The main flaw in the above flow is that the "origin" parameters passed to the two frames are never compared with each other. By using different origin parameters for the two, a malicious website may steal an authorized website's access token.

The attack proceeds as follows:

  • U logs into Facebook and has previously authorized W
  • U visits an attacker-owned website M
  • M starts a Proxy iframe with "origin=M" and an OAuth iframe with "origin=W"
  • The OAuth iframe successfully gets a token for W and passes it to the Proxy iframe
  • The Proxy iframe sends the token to M
  • Attack 2

    Important Dates

    November 5, 2012: date of report
    November 6, 2012: bug fixed

    Details

    This attack was due to a bug in the function called to compare the origins of the two iframes before the Proxy iframe released the access token to the hosting page:

    function q(z, aa) {
            if (!z || z.length === 0) return true;
            if (typeof z === 'string' && z === aa) return true;   /* ADDED by the BUGFIX on November 3, 2012 */
            var ba = '.' + (/https?:\/\/(.*)$/).exec(aa)[1],
                ca = z.length;
            while (ca--) {
                var da = '.' + z[ca];
                if (da == ba.substring(ba.length - da.length)) return true;
            }
            i.error('Failed proxying to %s, expected %s', ba, z.toString());
            return false;
        }
    
    If (typeof z === 'string' && z !== aa), the code above continues, treating z as an array.
    Hence, in this case it will take each character c of z and check whether "."+c is a suffix of aa
    
    The correct fix would have been:
    
    if (typeof z === 'string')
    if (z === aa) return true; else return false;
    else {...}
    
    The suffix-matching code starting from the third line of q assumes that both z and aa are origins, that is, that they do not have any query params or fragment identifiers. This is a mistaken assumption. Since the value aa is set by the home page, it can have the form "http://whatever.com/?q=.yahoo.com" . This URL should not match the origin "yahoo.com" but it will. Function q is happy to return true for q("yahoo.com","http://whatever.com/?q=.m")

    Attack 3

    Important Dates

    November 9, 2012: date of report
    November 10, 2012: bug fixed

    Details

    The third attack was a parameter pollution attack where you pass two values of a parameter and different values are processed on the clientside and the server side. We exploit this bug by passing two values of origin in the redirect uri to the OAuth endpoint as given below:

        https://www.facebook.com/dialog/oauth?client_id[...]origin=1&redirect_uri=http[...]xd_arbiter.php%3Fversion%3D15%[...]
            origin%3D [ORIGIN 1] %26
            origin%3D [ORIGIN 2] %26
            relation%3Dparent&sdk=joey
    
    While the PHP code on the server validates the second value of the Origin and ignores the first one and redirects to the Redirect URI with both the origins in the query string, JS SDK parses the returned query string in such a way so that the first value is validated.

    Impact

    All Facebook users were vulnerable to privacy breaches; a malicious website can get the same information about them as Facebook's instant personalization partners.All Facebook users who have ever authorized "Login with Facebook" on a website were vulnerable to impersonation attacks on that website. All Facebook users who have ever authorized "Facebook Sharing" on a website were vulnerable to privacy breaches and profile spamming; a malicious website can read their profile and write any message on their wall. Notably it could have propagated the attack through Facebook as a worm by posting a link to itself.

    Response from Facebook

    Facebook immediately acknowledged and fixed the bugs. As a part of their Whitehat bounty program, they also rewarded us with a bug bounty.