After discussing about Cross Site Scripting Attacks (XSS) in the previous posts, I am going to explain now about another type of attack Cross-site Request Forgery Attack (CSRF or XSRF), sometimes pronounced as "sea-surf". It has number of other names such as Session Riding, Cross-Site Reference Forgery, Hostile Linking and One-click attack.
CSRF attack, how it happens
CSRF is a more common vulnerability in websites, and the attack sometimes goes unnoticed by the victim. The after effects can be damaging as it happens within an authenticated session, and can perform user actions without the user's knowledge. For example, change password of user, one-click purchase on Amazon, post obscene content in the users social platform, transfer funds and so on.
The attacker may send a link via email or chat and trick the users of a web application into executing actions of the attacker’s choosing. These attacks can get serious, if the user's account is an administrative account, as the entire web application and its accounts are compromised.
There are many ways in which this attack can occur
1.Tampering with GET request
Suppose a website uses GET
request to change the state of web server, transfer parameters or execute logical actions. The attacker can exploit this vulnerability, for example by tricking the victim to click a hyperlink into the vulnerable site that performs unexpected actions. GET
is particularly susceptible to CSRF attacks as it exposes the all the request contents in the URL.
Most forms of hyperlink on the web correspond with HTTP GET
requests. A basic example is <a href="https://example-site.com"></a>
embedded in an HTML snippet.
Suppose a bank website uses a GET
request to transfer funds like below
GET http://bank.com/transfer.do?acct=ANNA&amount=100 HTTP/1.1
an attacker , let's call the person Jack
can tamper with the query strings and make a link or an image or a script, send it to the user via an unsolicited email with HTML content or by planting it on pages that are likely to be visited by the victim while they are doing online banking.
<a href="http://bank.com/transfer.do?acct= JACK&amount=10000"></a>
<img src="http://bank.com/transfer.do?acct=JACK&amount=100000" width="0" height="0" border="0">
<!-- Videos typically load into the DOM immediately, depending on the browser's
configuration. Some mobile browsers will not load until the element is interacted
with. -->
<video width="1280" height="720" controls>
<source src="https://www.bank.com/transfer.do?
acct=JACK&amount=100000" type="video/mp4">
</video>
In case of image tag, the user does not see anything or does not require any action from user to make the request, it will simply trigger as the page or email with HTML content is loaded.
A real case example of CSRF attack was when an older iteration of Twitter, had allowed to create tweets via a GET
requests. The hacker used this loophole to create a viral worm on Twitter, wherein a malicious link when clicked, would post a tweet containing an obscene message with the same link in it. When readers clicked the link that the first victim tweeted, they too were tricked into tweeting the same tweet. This was the first Twitter worm. Tens of thousands of Twitter users were tricked and luckily the development team closed the security hole before the situation got worse.
2. Using POST request
Well, if you thought that using POST
requests are safer, it's not the case. Its just that delivering an attack through POST
require some more work than using a link or image tag for GET
request.
The attack happens typically via a <form></form>
object in HTML, as it is possible to trigger a POST request on submit action without a script.
<form action="https://www.xxx-bank.com/transfer" method="POST">
<input type="hidden" name="to_user" value="hacker">
<input type="hidden" name="amount" value="10000">
<input type="text" name="username" value="username">
<input type="password" name="password" value="password">
<input type="submit" value="Submit">
</form>
In the above HTML form object you can see some hidden input fields, these are used to seed data into the form without the user seeing them.
In this case, the user will see a login form to a legitimate website, and after entering the username and password, the user submits the form. Upon submitting, a POST request will be made along with the hidden data to the bank, and the funds gets transferred to the attacker. There was no actual logging in, but a request was made using the user's existing session with the bank from another web page.
The below script will make the form submit on page load without requiring any user action.
<body onload="document.forms[0].submit()">
You can see how the attacker took advantage of the user's current application state with the bank even if they were on a different website. This technique can also be used to make requests on behalf of a user who has access to an internal network.
3. Using other HTTP methods - PUT, DELETE
Suppose a bank uses PUT that takes a JSON block as an argument:
PUT http://xxx-bank.com/transfer.do HTTP/1.1
{ "acct":"BOB", "amount":100 }
These requests are executed by a script in the malicious website.
<script>
function put() {
var x = new XMLHttpRequest();
x.open("PUT","http://bank.com/transfer.do",true);
x.setRequestHeader("Content-Type", "application/json");
x.send(JSON.stringify({"acct":"BOB", "amount":100}));
}
</script>
<body onload="put()">
Most of the mitigation against CSRF happens on the server side, even then it is important for a frontend developer to know about them, so that when you identify a vulnerability, you can notify and take necessary actions with your backend team.
Defense #1 - Follow REST principles
REST states that you should map website operations to the appropriate HTTP method according to their intention. You should fetch data or pages with GET requests, create new objects on the server (such as comments, uploads, or messages) with PUT requests, modify objects on the server with POST requests, and delete objects with DELETE requests. So Do not use GET
requests to change the state of the server.
If you use it for any reason, they will require a CSRF token header (this a bad practice).
Defense #2 - Anti-CSRF tokens
Anti-CSRF tokens prevent CSRF attacks by requiring the existence of a secret, unique, and unpredictable token that is generated by the server-side application and transmitted to the client in such a way that it is included in a subsequent HTTP request made by the client.
When the later request is made, the server-side application validates that the request includes the expected token and rejects the request if the token is missing or invalid.
These tokens can be set for an entire user session, rotated on a regular basis, or be created uniquely for each request.
The CSRF tokens can be implemented multiple ways, the common examples are
#1 Hidden input field
<input type="hidden" name="csrftoken" value="1df93e1eafa42012f9a8aff062eeb1db0380b">
In the above approach, the token is transmitted from the server to the client within a hidden field of an HTML form. The token will then be included as a request parameter when the form is submitted using POST method.
To prevent the attacker manipulate the HTML document to capture its contents, the field containing the CSRF token should be inserted before any visible input fields or any locations where user-controllable data is embedded within the HTML.
#2 Custom Request Header
On an initial visit, the web application sets a cookie which is scoped appropriately so that it should not be provided during cross-origin requests. The cookie typically contains a random token which may remain the same for up to the life of the web session.
# Server-side: set an anti-CSRF cookie that JavaScript must send as an X header, which can't be done cross-origin
Set-Cookie: CSRFTOKEN=1df93e1eafa42012f9a8aff062eeb1db0380b; Path=/; Secure; SameSite=Strict
JavaScript operating on the client side reads its value and copies it into a custom HTTP header sent with each transactional request.
// Client-side, have JavaScript add it as an X header to the XMLHttpRequest
var token = readCookie(CSRFTOKEN); // read the cookie
httpRequest.setRequestHeader('X-CSRF-Token', token); // add it as an X-CSRF-Token header
On request, with this header, the server then validates presence and integrity of the token.
The use of a custom request header is particularly used for AJAX or API endpoints. This defence relies on the same-origin policy (SOP) restriction that only JavaScript can be used to add a custom header, and only within its origin. By default, browsers do not allow JavaScript to make cross origin requests with custom headers, thereby protecting from an attacker performing CSRF attack from another origin.
Note: The protection provided by this technique can be thwarted if the target website disables its same-origin policy.
Storing CSRF token
Make sure that the CSRF token is not exposed any ways, not in server logs, in URL or in GET requests. It can be in hidden input fields, in javascript variable or anywhere in DOM.
A CSRF token can be also included in the <meta>
tag as shown below. All subsequent calls in the page can extract the CSRF token from this tag.
However, it is not recommended to store it in cookies or browser local storage. There is a related post regarding this discussion.
The following code snippet can be used to include a CSRF token as a <meta>
tag:
<meta name="csrf-token" content="{{ csrf_token() }}">
The exact syntax of populating the content attribute would depend on your web application's backend programming language.
Defence #3 - Using SameSite Cookie Attribute
By default, a browser automatically adds the last known cookies to requests regardless of where they originate. If an attacker steals the security token from your HTML forms, and installs it in their own malicious forms, they can launch a CSRF attack, with any security cookies the server previously set.
Set-Cookie: JSESSIONID=xxxxx; SameSite=Strict
Set-Cookie: JSESSIONID=xxxxx; SameSite=Lax
The SameSite attribute can be used to control whether and how cookies are submitted in cross-site requests. Specifying a SameSite attribute strict
, when you set a cookie tells the browser to strip cookies on a request to your site when the request is coming from an external domain.
The Strict
value will prevent the cookie from being sent by the browser to the target site in all cross-site browsing context, even when following a regular link. Just imagine if you have login each time in Facebook when you return from seeing a video someone shared. The default Lax
value provides a reasonable balance between security and usability for websites that want to maintain user's logged-in session after the user arrives from an external link as it allows only GET requests from other sites to send cookies.
As of now Chrome and other modern browsers has Same-site attribute set by default (Cookies without SameSite are treated as Lax by default, SameSite=None cookies without Secure are rejected.) Read more here
Other Defences
There are various other defence techniques and one of them is by User interaction
- Re-Authentication - Authorization mechanism (password or stronger)
- One-time Token
- CAPTCHA (prefer newer CAPTCHA versions without user interaction or visual pattern matching)
To know more in depth about other defence techniques refer the Cross-Site Request Forgery Prevention Cheat Sheet where I have taken a lot of reference from for this blog post.
Hope you have a better understanding about CSRF attacks now. Please share your comments if you have any feedback or suggestions. In the next post I intend to write about vulnerabilities in Authentication process. Stay tuned!