Monday, January 22, 2007

Preventing CSRF when vulnerable to XSS

*The following code and concepts should be considered highly experimental and should be considered a work in progress. Not to be used for production websites.*

Web Worms (like Samy) targeting social networking websites (like MySpace) typically involve combining two attacks, Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF). An attacker posts JavaScript Malware (Web Worm) to their user profile web page because the website allows user-supplied HTML and the input filters didn’t catch the offending code. When a logged-in user visits the infected profile web page their browser is hi-jacked (user XSS’ed) to “friend the attacker” and post of copy the Web Worm code to their profile (user CSRF’ed) causing self-replication. There is no “Cross-Site” as part of the forged requests as CSRF implied, but that conversation is for another time.

As was case for MySpace and many other website, important features such posts to a user profile and friend’ing users, are protected from CSRF using session tokens embedded in URL’s or HTML Forms. Requests aren’t valid without a token. To defeat the CSRF solution, Web Worms first request a third-party page (on the same domain) to get a valid token and use it as part of a forged request. Since the attack is on the same domain, access to session tokens is can be easily achieved. This is why many people, including myself, have believe that CSRF solutions can be defeated when XSS vulnerability exist on that domain. However, there may be something we can do.


Without using browser exploits, JavaScript only has a few ways to access HTML data from another page on the same domain. Generally speaking, if we can prevent JavaScript on an XSS’ed web page from being able to read in session tokens from other pages, we might have something worth pursuing.

XMLHttpRequest
window.open
IFrame

If we can remove access these API’s, we may be able to prevent or make it harder for JavaScript Malware to bypas CSRF security. Enter prototype hijacking. The following proof-of-concept code effectively does this when called first at the top of the web page.

PoC Code is (Firefox ONLY!) Should this method be found workable, we can port examples to other browsers, namely Internet Explorer.

1) XMLHttpRequest

The Samy Worm used this method. This following function overwrites the XMLHttpRequest constructor so it has no functionality of any kind.

function XMLHttpRequest() { }

By calling XHR after this point:

var req = new XMLHttpRequest();
req.open('GET', 'http://website/', true);

Results in the following error message:

Error: req.open is not a function

I’ve not been able to find a way to restore XHR’s functionality


2) window.open

A Web Worm could feasibly open a small, but hidden new window and read the session token out of that. Impose the following line:

window.__defineGetter__("open", function() { });

By calling window.open after this point:

window.open("http://website/",null,"");

Results in the following error message:

Error: window.open is not a function

Again, have found no way to restore window.open

3) IFrame

There are a couple of ways to create IFrames from JavaScript, createElement and document.write. Overwriting these constructors is the same as the previous examples.

document.__defineGetter__("write", function() { });
document.createElement = function () {}


There are a couple of things to note here:
  • Any mechanism I have missed, the constructor should be able to be overwritten.
  • The model break down if the XSS'ed web page requires any of these constructors.
  • I have not yet explored if Flash, JScript, VBScript can be used as a substitute for JavaScript to retrieve session tokens.

Like I said, a work in progress, but fun none the less :)

12 comments:

Anonymous said...

You have to think about

- VBScript prevention
- Sites implementing this can't use XMLHTTP (Ajax)


- zeno
http://www.cgisecurity.com/

lpilorz said...

It seems to be an interesting idea, but it has some use in very limited conditions only. By the way, you can put an iframe on the page with innerHTML property of many html elements.

Jeremiah Grossman said...

> It seems to be an interesting idea, but it has some use in very limited conditions only.

Yes it does, but not all solutions have to work perfectly for everyone in all circumstances. If this could work for you, cool, if not, oh well.

> By the way, you can put an iframe on the page with innerHTML property of many html elements.

Good point! Thank you.

kL said...

Here's how to block and restore JS functions:

// call anonymous function - you'll have scope that is inaccessible from outside
(function() {
var myProtectedXHR = window.XMLHttpRequest;

delete window.XMLHttpRequest;

window.restoreXHR = function()
{
// to restore - just assign function reference again
window.XMLHttpRequest = myProtectedXHR;
}

})();

Jeremiah Grossman said...

Hi kl,

If I'm reading this correctly your saving off a reference of window.XMLHttpRequest for reuse later. Unless I'm missing something with your example you shouldn't be able to do this if I call this line before yours:

function XMLHttpRequest() { }

Because its gone.

Correct?

kL said...

Yes.

What I've wrote is not a hack (once overwritten reference can't be restored indeed), but improvement of the blocking method, that might retain XHR and make it available to your own code (with added verification against CSRF attack for example).

BTW: protection can be further improved by removing (hiding) token from page's DOM using JavaScript - this will work for pages loaded in popup/iframe.

Wladimir Palant said...

You forgot the prototypes:

Window.prototype.open.call(window, "test");
HTMLDocument.prototype.write.call(document, "test");
HTMLDocument.prototype.createElement.call(document, "test");

Unfortunately it seems that overwritten prototypes are easily restored with "delete HTMLDocument.prototype.open". And a minor note: there is also createElementNS(), DOMParser and innerHTML that can be used as well to create elements.

Christian Matthies said...

Good thoughts Jeremiah but there's one way you didn't mention.

To prevent tokens from being stolen (1) you can keep them away from pages which are vulnerable to XSS since these attacks are limited to the current domain. Embedding forms in iFrames is what Stefan Esser once discribed in his blog. http://blog.php-security.org/archives/48-CSRF-protections-are-not-doomed-by-XSS.html

(1) Anyway, taking the IE mhtml issue into account, this isn't an adequate protection anymore.

Sylvan von Stuppe said...

I just wrote a post on another way to deal with stealing the token. Assuming the XSRF target and the XSS vulnerable page are NOT the same, you can use a key tied to the action to mess up the token. So you store a random number in the session, as usual, but give the user a hash of the random number concatenated with the key for the action you presume they're going to post to. To check the result, take the number from the session, the key for this action, hash, and compare to the hidden value dropped in.

Jeremiah Grossman said...

> Good thoughts Jeremiah but there's one way you didn't mention.

Thank you. Trying to find SOMETHING a website can do. And it seems as though I have some more work to do on the JS side because people quickly found work arounds.

I believe Stefan Esser's concept was to host the forms on a separate domain as well. Using the same origin policy to help, which could work. The problem I saw was this would require rearchitecting websites and that might be an undue burden.

> Anyway, taking the IE mhtml issue into account, this isn't an adequate protection anymore.

Every solution dies in light of a browser vuln. They have to be patched.

Anonymous said...

IFrames should just die. They were a bad idea when introduced in IE 3, and they are still a bad idea today. The only people who use them are the bad guys and adware vendors. Really, the best thing the W3C could do would be to deprecate them.

Anonymous said...

XSS using dynamic SCRIPT tags is far more dangerous than using IFRAME tags, even because there are no domain restrictions nor same origin policy.

So chop also those remote scripts and applications from the WEB and let's remake a new static world with just local scripts and lot of nice fade effects and accordions... :-)

There is still a better world, things that only work on your PC (fantastic) never heard about that ?