CSRF Mitigation for AJAX Requests
To start with, a quick recap on what Cross-Site Request Forgery is:
- User is logged into their bank’s website:
https://example.com
. - The bank website has a “money transfer” function:
https://example.com/manage_money/transfer.do
. - The “money transfer” function accepts the following POST parameters:
toAccount
andamount
. - While logged into
https://example.com
the user receives an email from a person they think is their friend. - The user clicks the link inside the email to access a cat video:
https://attacker-site.co.uk/cats.htm
. cats.htm
whilst displaying said cat video, also makes a client-side AJAX request tohttps://example.com/manage_money/transfer.do
POSTing the valuestoAccount=1234
andamount=100
transferring £100 to the attacker’s account from the victim.
Quick POC here that only POSTs to example.com
and not your bank. Hopefully your bank already has CSRF mitigation in place. View source, developer console and Burp or Fiddler are your friends.
There’s a common misconception that websites can’t make cross-domain requests to other domains due to the Same Origin Policy. This could be due to the following being displayed within the browser console:
However, this is not the case. The request has been made, the browser message is telling you that the current origin (https://attacker-site.co.uk
) simply cannot read any of the returned data from the cross-domain request. However, unlike an XSS attack, it doesn’t need to. The request has been made, and because the user is logged into the bank so therefore has a session cookie, this session cookie has been passed to the bank site authorising the transaction.
Quick Demo
Simulate the bank issuing a session cookie by creating our own in the browser:
Note we’ll set the Secure flag and HTTPOnly flags to show these have no effect on CSRF.
Visit the website:
The following request is sent, note our session cookie is included:
Therefore as far as the web application is concerned, this is a legitimate request from the user to transfer the money despite the browser returning Cross-Origin Request Blocked
. Only the response is blocked, not the original request.
AJAX Mitigation
If the target application has no CSRF mitigation in place, the above works for both AJAX requests and traditional form POSTs. This can be mitigated using the traditionally recommended Synchronizer Token Pattern. This involves creating a random, unpredictable token (in addition to the session token held in the cookie) and storing this server-side as a session variable. When a POST is made, this anti-CSRF token is also sent, but using any mechanism apart from cookies. This means that the anti-CSRF token will not be automatically included from the browser should the user follow a dodgy link that makes its own cross-domain request. CSRF averted.
But what if there was another way? One little known way is to include a custom header, such as X-Requested-With
, as I answered here.
Basically:
- Set the custom header in every AJAX request that changes server-side state of the application. e.g.
X-Requested-With: XmlHttpRequest
. - In each server-side method handler, ensure a CSRF check function is called.
- The CSRF function examines the HTTP request and checks that
X-Requested-With: XmlHttpRequest
is present as a header. - If it is, it is allowed. If it isn’t, send an HTTP 403 response and log this server-side.
Many JavaScript frameworks such as JQuery will automatically send this header along with any AJAX requests. This header cannot be sent cross-domain:
- Any attempt to do so with a modern browser will trigger a CORS preflight request.
- Older browsers (think IE 8 and 9) can send cross-domain requests, but custom headers are not supported at all.
- Very old browsers cannot send cross-domain AJAX requests at all.
What is a Preflight?
So referring to the above old browsers couldn’t make cross-domain requests at all via AJAX. Therefore, you may get an old website that does check for a custom header server-side so that it knows it is an AJAX request. Now, the web is developed on the basis of “no breaking changes”. Therefore any new technologies introduced into the browser should not force websites to have to update themselves to continue working (why not visit the World Wide Web - apparently the world’s first website). This goes for functionality as well as security.
Therefore, suddenly allowing browsers to send cross-domain headers could break security if a site relies on this for CSRF mitigation. This scenario covers both points 2 and 3.
So that leaves 1, CORS (Cross-Origin Resource Sharing). CORS is a mechanism that weakens security. Its aim is to allow sites that trust one another to break the Same Origin Policy and read each others responses. e.g. api.example.org
might allow example.org
to make a cross-domain request and read the response in the browser, using the user’s session cookie as authorisation.
In a nutshell CORS does not prevent anything that used to be possible from happening. An example is a cross domain post using <form method="post">
has always been allowed, so therefore CORS allows any AJAX request that results in a previously possible HTTP request to be made, without a preflight request. This is because this has always been possible on the web and allowing AJAX to do this as well does not introduce any extra risk. However, a request with custom headers causes the browser to automatically send a request to the endpoint using the OPTIONS
verb. If the server-side application recognises the OPTIONS
request (i.e. it is CORS aware), it will reply with a header showing which headers will be allowed from the calling domain.
Here you can see the attempt to send X-Requested-With
in a cross-domain POST results in an OPTIONS request requesting this header be allowed, rather than the actual request. This is the preflight.
If the server-side is not explicitly configured to allow this (i.e. no Access-Control-Allow-Origin
to allow the domain and no Access-Control-Allow-Headers
to allow the custom header):
The header is not allowed because our example.com
domain is not configured for CORS.
Therefore if CORS is not allowing the attacker’s domain to send extra headers, this mitigates CSRF.
Will This Work?
What To Look For When Pentesting
The above will only work if the server-side application is verifying that the custom header X-Requested-With
is received in the request. As a pentester you should verify that all potentially discovered CSRF vulnerabilities are actually exploitable. Burp Suite allows this via right clicking an item then clicking Engagement tools > Create CSRF PoC
. This may result in two things:
- If you weren’t aware of the above, you may find a POST request that first appeared vulnerable to CSRF (due to no tokens) however isn’t due to header checking.
- If, after having read this post, you find that an AJAX request is sending
X-Requested-With: XmlHttpRequest
you may find that removing this header still causes the “unsafe” action to take place server-side, therefore the request is vulnerable.
What To Do As A Developer
This may be a good short-cut if your server-side language of choice does not support server-side variables or if you do not want the extra overhead of storing an additional token per user session. However, make sure that the presence of the HTTP request header is verified for every handler that makes a change of state to your application. Aka, “unsafe” requests as defined by the RFC.
Remember, this only works for AJAX requests. If your application has to fall-back to full HTML requests if JavaScript is disabled, then this will not work for you. Custom headers cannot be sent via <form>
tags.
Conclusion
This is a useful, easy to implement mitigation for CSRF. Although an attacker can easily add a custom header themselves (e.g. using Burp Suite), they can only do this to their own requests, not those of the victim as required in a client-side attack. There were vulnerabilities in Flash that allowed a custom-header to be added to a cross-domain request to another attacker’s site that set crossdomain.xml
. Unlike HTML, Flash requires a crossdomain.xml
file for any request, even those that are write only, such as CSRF. The trick here was for the attacker to issue a 307 HTTP redirect to redirect from their second attacker domain to the victim website. The bug in Flash carried over the custom header from the original request. However, as Flash is moribund, and this was a bug, I would say it is generally safe for most sites to rely on the presence of the header as a mitigation. However, if the risk appetite is low for the application in question, go with token mitigation instead of or as well: Defence-In-Depth.
Note that the Flash bug was fixed back in 2015.