CSRF and Same-Origin XSS
During penetration tests CSRF (Cross-Site Request Forgery) vulnerabilities are typical findings, although proper protection concepts with tokens are well known. But even when protected with tokens these concepts often fail as soon as XSS (Cross-Site Scripting) vulnerabilities exist in the same domain/port combination, since the script executing via XSS in the victim's browser is capable of reading the CSRF protection token and thus can execute CSRF attacks.
In this short blog post I will present some tips on protecting against CSRF attacks even when XSS vulnerabilities exist in other applications running same-origin with the targeted application.
Some of the applications I have pentested even had CSRF protection tokens in place (see OWASP's CSRF prevention cheat sheet for token concepts). But the applications I have seen (with CSRF tokens in use) unfortunately implemented it not secure enough, so that the pure existence of a same-origin (i.e. same scheme, domain, and port) XSS vulnerability leads to a CSRF attack. On the other hand, a single XSS vulnerability in the same application that has CSRF tokens in place definitively allows the attacker to read the CSRF protection tokens. This is best exploited via persistent XSS vulnerabilities.
So what about a XSS flaw in another application same-origin?
A common observation I have made is that most CSRF token protected web applications use a cryptographically strong random token value and store it inside the web session. This token is then sent along with every POST request (often as hidden form fields) in order to protect state-modifying POST requests. It is well known that GET requests, as long as they can not be used to modify state, are often not susceptible to CSRF attacks and therefore not protected with tokens. Even further, using CSRF tokens on GET requests exposes them to other risks of leakage (think of logs, browser histories, bookmarks, etc.). And this is exactly where the problem lies: Web applications that only protect POST requests using CSRF tokens can be attacked from a XSS vulnerability of another (maybe not so relevant) application running same-origin. Just think of using an XMLHttpRequest injected into a XSS vulnerability of another application like in the following scenario:
An online-shop is running at
https://www.example.com/shop which has proper CSRF tokens in place for all of its POST requests.
Users can login using their credentials and enter a secured realm of the shop. The only user-visible entry-point into this
secured realm is a simple form based username/password dialog. This hypothetical online-shop has no XSS vulnerabilities.
But on the same domain a public blog of the shop owning company is running at
https://www.example.com/blog which unfortunately
of the HTML code (in case the victim is logged in at the shop) of the secured realm, which includes the CSRF token that
protects POST requests:
// retrieve page contentvar xhr = new XMLHttpRequest(); xhr.open("GET", "https://www.example.com/shop/viewAccount", false); xhr.withCredentials=true; xhr.send(null);// extract CSRF token from page contentvar token = xhr.responseText; var pos = token.indexOf("csrftoken"); token = token.substring(pos,token.length).substr(12,50);// now execute the CSRF attack using XHR along with the extracted tokenxhr.open("POST", "https://www.example.com/shop/voteForProduct", false); xhr.withCredentials=true; var params = "productId=4711&vote=AAA&csrftoken="+token; xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhr.setRequestHeader("Content-length", params.length); xhr.send(params);
The root problem here is that the CSRF protection style of only securing POST requests with tokens is susceptible to break when combined with XSS attacks even in other applications running same-origin with the protected application. Now, how can this be solved? Ideally also GET requests (even though they are usually not state-modifying) should be protected using CSRF tokens. But as GET request parameters are leaked in server logs and otherwise, the token used to protect GET requests should be a different one from the token protecting POST requests. To be even more secure, use a different token for every page or still better: renew the tokens after each use. But the latter approach has certain problems with browser back buttons and asynchronous requests (where the order is not deterministic).