Cross-Site Request Forgery attacks can exploit your identity to perform unauthorized operations on a web application. This article shows you how they work in practice and how you can prevent them by applying a few strategies. Throughout the article, you will play with a sample vulnerable web application and fix its vulnerability by using different defensive approaches.
What is CSRF?
A typical Cross-Site Request Forgery (CSRF or XSRF) attack aims to perform an operation in a web application on behalf of a user without their explicit consent. In general, it doesn't directly steal the user's identity, but it exploits the user to carry out an action without their will. For example, it can lead the user to change their email address or password in their profile or even perform a money transfer.
In a nutshell, a typical CSRF attack happens as follows:
- The attacker leads the user to perform an action, like visiting a web page, clicking a link, or similar.
- This action sends an HTTP request to a website on behalf of the user.
- If the user has an active authenticated session on the trusted website, the request is processed as a legitimate request sent by the user.
As you can see, having the website affected by a CSRF vulnerability is not enough to make the attack successful. The user must also have an active session on the website. In fact, the CSRF vulnerability relies on the authenticated session management.
Typically, session management in a web application is based on cookies. With each request to the server, the browser sends the related cookie that identifies the current user's session. This usually happens even if the request is originated from a different website. This is the issue exploited by the attacker.
Even though CSRF attacks are commonly associated with session cookies, be aware that Basic Authentication sessions are also vulnerable to CSRF attacks.
CSRF in Action
So far, you have a high-level idea of what a CSRF attack is. However, to better understand how it works in practice, let's see a concrete case of a vulnerable application.
To run the sample application that comes with this article, you just need Node.js installed on your machine. However, keep in mind that the principles behind the CSRF vulnerability and the fixing strategies are independent of the specific programming language or framework.
Set up the environment
Let's set up a playground environment where you can experience a CSRF attack firsthand. Download the sample project from GitHub by running this command in a terminal window:
git clone https://github.com/auth0-blog/csrf-sample-app.git
Now, move into the project's root folder and install the project's dependencies by running the following command:
npm install
Finally, launch the vulnerable website by running this command:
npm start
Point your browser to the http://localhost:3000 address. You should see the following page:
The sample project implements a page of a fictitious movie streaming website. Only registered users can add their review to the movie by filling in the text box and clicking the submit button. In fact, if you try to add a comment right now, nothing happens.
For simplicity, the project doesn't implement an actual authentication process, since our focus is more on the issues that happen after a user is authenticated and gets a valid session. By clicking the valid session link within the message right above the submit button, you get a simulated user session. With that simulated session, the page should look like the following:
As you can see, the warning message disappeared, and a new link Your profile appeared near the top right corner of the page. By clicking that new link, you can manage your profile, which for simplicity, consists of a name and an email address:
Launch the CSRF attack
Now, let's start the attacker's website by typing this command in a terminal window:
node attacker-server.js
Open a new tab of your browser and point it to http://localhost:4000. You should see a page like the following:
This is a simple web page with a link that invites you to visit a website.
The attack shown here is based on the user visiting the attacker's website. However, the attack may happen in different ways: via email, instant messaging, social networks, etc.
If you click the link, you are redirected to the user's profile page on the movie streaming website. But this is not the only navigation effect. The user's data has been replaced with the attacker's data:
You triggered this change by simply clicking the link on the attacker's website. How could this happen?
The CSRF mechanics
Before analyzing the attacker's website to understand what happened, let's take a look at the user's profile page. It is a standard HTML form that allows you to change the user's name and email address. You can look at its code by opening the EJS template implemented in the template/user.ejs
file. Its relevant content is as follows:
<!-- template/user.ejs -->
<!-- existing markup --->
<form method="post" action="user">
<fieldset>
<label for="username">Your name:</label>
<input name="username" type="text" value="<%= username %>" class="thin">
<label for="email">Your email:</label>
<input name="email" type="email" value="<%= email %>" class="thin">
<button type="submit" class="thin">Save</button></form>
</fieldset>
</form>
<!-- existing markup --->
The /user
endpoint processing the form submission is implemented in the server.js
file. This snippet highlights the relevant code:
// server.js
// ...existing code...
app.post('/user', function (req, res) {
if (req.session.isValid) {
req.session.username = req.body.username;
req.session.email = req.body.email;
res.redirect('/user');
} else {
res.redirect('/');
}
});
// ...existing code...
The submitted data are accepted by the server only if a valid active session is present.
Now, let's see how this seemingly innocent link on the attacker's page is implemented. Its markup looks like the following:
<!-- views/index.ejs -->
<!-- existing markup --->
<form method="post" action="http://localhost:3000/user">
<input type="hidden" name="username" value="The Attacker">
<input type="hidden" name="email" value="theattacker@attacker.com">
</form>
<a href="#" onclick="document.forms[0].submit()">Visit this website!</a>
<!-- existing markup --->
You notice a form with hidden fields. That form's action points to the user's profile page and the link triggers a simple JavaScript statement that submits the form.
This form is harmless when the user of the movie streaming website has no active session. The vulnerable website will refuse to change the user's profile because of this missing session. However, if the user has an active session, the change will be applied as any regular legitimate request.
This behavior is due to a cookie on the user's browser that tracks the current session on the movie streaming website. When the vulnerable website receives the change request, it appears legitimate since it has the correct session cookie.
So, even if the attacker has no direct access to the vulnerable website, they exploit the user and the CSRF vulnerability to perform unauthorized actions. In fact, unlike what may happen in XSS attacks, here, the attacker doesn't directly read the cookie and steal it.
"CSRF attackers exploit app vulnerabilities to perform actions without the user's explicit consent."
Tweet This
Hiding the CSRF attacks
In the example shown so far, the user becomes aware of the attack just after clicking the malicious link. Of course, those examples have an educational purpose and are kept as simple as possible to focus on the attack's logic.
However, keep in mind that most attacks are hidden to the users, and also their interaction is not strictly necessary. For example, the attacker can trigger a CSRF attack by simply putting the following script right after the malicious form:
<script>
document.forms[0].submit();
</script>
It will submit the form right at the page loading.
Also, to prevent users from seeing what is happening, the attacker can simply include the form in a hidden iframe. This way, the user will not be aware that an attack is occurring.
Want to learn more about Credential Stuffing Attacks?
Download the whitepaperCSRF Defenses Strategies
Now you should have a better understanding of how a CSRF attack happens. Let's take a look at how you can prevent them in your applications. Basically, you have two strategies:
- Making sure that the request you're receiving is valid, i.e., it comes from a form generated by the server.
- Making sure that the request comes from a legitimate client.
Validating Requests
Attackers can perform a CSRF attack if they know the parameters and values to send in a form or in a query string. To prevent those attacks, you need a way to distinguish data sent by the legitimate user from the one sent by the attacker. In other words, you need a way to validate requests and only accept the legitimate ones.
Using a CSRF token
The typical approach to validate requests is using a CSRF token, sometimes also called anti-CSRF token. A CSRF token is a value proving that you're sending a request from a form or a link generated by the server. In other words, when the server sends a form to the client, it attaches a unique random value (the CSRF token) to it that the client needs to send back. When the server receives the request from that form, it compares the received token value with the previously generated value. If they match, it assumes that the request is valid.
Let's apply this technique to protect the user's profile page.
As a first step, install the csurf
library by running the following command in a terminal window:
npm install csurf
This library will help you to generate and manage the CSRF token.
You might think that creating and checking a CSRF token is so simple that you can write the code yourself. My suggestion is to use a proven library to do this job at best. Look for the best library for your programming language or framework.
After installing csurf
, change the content of the server.js
file as follows:
// server.js
const express = require("express");
const session = require('express-session');
const bodyParser = require('body-parser');
const csrf = require('csurf'); //👈 new code
// ...existing code...
app.use(bodyParser.urlencoded({ extended: true }));
app.use(csrf()); //👈 new code
// ...existing code...
app.get('/user', function (req, res) {
if (req.session.isValid) {
res.render('user', {
username: req.session.username,
email: req.session.email,
csrfToken: req.csrfToken() //👈 new code
});
} else {
res.redirect('/');
}
});
// ...existing code...
You imported the csurf
module and configured it as an Express middleware. A new CSRF token will now be generated for each request and attached to the current session object. You can access the current CSRF token through the req.csrfToken()
method. With the default csurf
configuration, the token's validity will be checked whenever a POST request is sent to the server.
Now, edit the templates/user.ejs
file and add the markup highlighted in the following:
<!-- template/user.ejs -->
<!-- existing markup --->
<form method="post" action="user">
<fieldset>
<label for="username">Your name:</label>
<input name="username" type="text" value="<%= username %>" class="thin">
<label for="email">Your email:</label>
<input name="email" type="email" value="<%= email %>" class="thin">
<!-- 👇 new code -->
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<button type="submit" class="thin">Save</button></form>
</fieldset>
</form>
<!-- existing markup --->
This markup includes the hidden field _csrf
with the current value of the CSRF token.
With these changes, the movie streaming website will continue to work as before. But if you try to apply the attack from the http://localhost:4000 URL, you will no longer be able to change the user's data. You will get an error message complaining about the invalid CSRF token, as shown in the following picture:
So, the CSRF token protects the user's profile page.
You can download from GitHub the fixed version of the original project with the following command:
git clone -b csrf-token https://github.com/auth0-blog/csrf-sample-app.git
Using the double submit cookie strategy
The previous solution is based on keeping the value of the matching CSRF token on the server side. If you don't want to maintain a copy of the token on the server for any reason, you can apply the double submit cookie strategy. With this variant, the server stores the matching token's value in a cookie instead of keeping it in the server session. It sends the CSRF token's value to the browser in the hidden field and in the cookie. When the server receives a request, it just needs to check if the cookie's value and the hidden field value match.
Let's see how you can implement this alternative strategy with the csurf
library. Start with the original vulnerable project. Refer to the Set up the environment section for directions.
Install the csurf
and cookie-parser
libraries with the following command:
npm install csurf cookie-parser
You already know the csurf
library. The cookie-parser
library allows your application to parse cookies sent by the browser.
Then, change the content of the server.js
file as follows:
// server.js
const express = require("express");
const session = require('express-session');
const bodyParser = require('body-parser');
const csrf = require('csurf'); //👈 new code
const cookieParser = require('cookie-parser'); //👈 new code
// ...existing code...
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser()); //👈 new code
app.use(csrf({ cookie: true })); //👈 new code
// ...existing code...
app.get('/user', function (req, res) {
if (req.session.isValid) {
res.render('user', {
username: req.session.username,
email: req.session.email,
csrfToken: req.csrfToken() //👈 new code
});
} else {
res.redirect('/');
}
});
// ...existing code...
Here, you include the modules just installed and configure them as middleware in the Express HTTP pipeline. In particular, you are configuring the csurf
middleware to use cookies instead of the server session object. As you did for the session-based approach, you will access the CSRF token through the req.csrfToken()
method and will put it in a hidden field of the user's page template:
<!-- template/user.ejs -->
<!-- existing markup --->
<form method="post" action="user">
<fieldset>
<label for="username">Your name:</label>
<input name="username" type="text" value="<%= username %>" class="thin">
<label for="email">Your email:</label>
<input name="email" type="email" value="<%= email %>" class="thin">
<!-- 👇 new code -->
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<button type="submit" class="thin">Save</button></form>
</fieldset>
</form>
<!-- existing markup --->
This way, you fix the CSRF vulnerability with an approach quite similar to the previous case. However, if you take a look at the cookies in your browser, you will notice a new _csrf
cookie containing the CSRF token's value.
You can download this version of the project from GitHub as well. Here is the command to use:
git clone -b double-submit-cookie https://github.com/auth0-blog/csrf-sample-app.git
"A CSRF token allows you to validate a request from the client."
Tweet This
Validating Requests Origin
The strategies discussed in the previous section were based on checking the validity of a request. You can use a strategy based on accepting requests from specific origins, typically the same domain that hosts the web application. Let's take a look at this approach.
Check the request origin
To make sure that an HTTP request is coming from a legitimate client, you should validate its origin. It means that the server should determine the source origin of the request and compare it with the target origin. You can do this by analyzing a few HTTP headers like Origin
or Referer
. You can rely on these headers because they cannot be altered programmatically, that is, only the browser can set them.
Let's take a look at how you can implement this technique. Again, start with the original vulnerable project by setting up the working environment. Then, change the content of the server.js
file by adding the following code:
// server.js
// ...existing code...
app.set('views', './templates');
app.set('view engine', 'ejs');
//👇 new code
app.use(function(req, res, next) {
const referer = (req.headers.referer? new URL(req.headers.referer).host : req.headers.host);
const origin = (req.headers.origin? new URL(req.headers.origin).host : null);
if (req.headers.host == (origin || referer)) {
next();
} else {
return next(new Error('Unallowed origin'));
}
});
//👆 new code
// ...existing code...
You added a middleware that grabs the Origin
and Referer
headers and compares their values with the Host
header's value. It takes into account that the Referer
header may be missing at the first request to the server. Also, it takes into account that old browsers don't support the Origin
header. If one of those headers matches the Host
header, you can process the request. Otherwise, an error is raised.
Do not consider this a production-ready code. It is just for demonstration purposes. Many issues may affect the correct behavior of origin validation. Check out this OWASP document to learn more.
With this change, the attacker's website will not be able to trigger its CSRF attack.
Download the project fixed with this approach by using the following command:
git clone -b request-origin https://github.com/auth0-blog/csrf-sample-app.git
Using SameSite cookies
An alternative way to invalidate requests coming from unauthorized origins is using the sameSite
cookie property. This property has been recently introduced, so old browsers may not support it.
To learn how you can adopt this approach, restore the original project as described in the Set up the environment section. Then, open the server.js
file and apply the following little change:
// server.js
// ...existing code...
app.use(express.static('public'));
app.use(session({
secret: 'my-secret',
resave: true,
saveUninitialized: true,
cookie: {
httpOnly: true,
sameSite: 'strict' //👈 new code
}
}));
// ...existing code...
You assigned the 'strict'
value to the sameSite
property of the session cookie. This value instructs the browser not to send the session cookie when the request comes from a different domain. In other words, that cookie must be sent to the server only by pages loaded from the same website.
Now, you may want to verify that the attacker's website is no longer able to perform any unintentional change on the movie streaming website. Unfortunately, if you try to perform the usual attacker steps as before, you will be able to carry out the attack. This is because you are using the same domain name (localhost) for both the vulnerable and the attacker websites, and cookies are shared independently of the port. So, to correctly test the behavior of the sameSite
property, you need to differentiate the domain names. For example, you can use the http://127.0.0.1:4000 address for the attacker's website.
This time everything should go as expected. However, unlike the other scenarios, your redirection from the attacker's website to the user's profile page doesn't get an error. You simply get the vulnerable website's home page as an unauthenticated user. In fact, this time, the browser is not sending the session cookie to the streaming movie website since the request comes from another site.
As usual, you can download this version of the project with the following command:
git clone -b cookie-samesite https://github.com/auth0-blog/csrf-sample-app.git
"The 'sameSite' cookie's property contributes to preventing CSRF attacks."
Tweet This
About Auth0
Auth0 by Okta takes a modern approach to customer identity and enables organizations to provide secure access to any application, for any user. Auth0 is a highly customizable platform that is as simple as development teams want, and as flexible as they need. Safeguarding billions of login transactions each month, Auth0 delivers convenience, privacy, and security so customers can focus on innovation. For more information, visit https://auth0.com.
Conclusion
At the end of this article, you know better how CSRF attacks work and which strategies you can apply to prevent them. You learned how to use CSRF tokens and validate the origin of HTTP requests. At this point, maybe you are wondering which of these strategies to apply to your web applications. As you've seen, those approaches range from a very simple one, like leveraging the sameSite
property of cookies, to a more complex one, like generating CSRF tokens. On the other side, you learned that some of them have limitations. So, what is the best strategy to adopt in order to defend your application? Of course, there is no definitive answer to this question. In general, the best approach is a combination of multiple strategies so that the limitations of one can be covered by the other ones.
The goal of this article was to explain how CSRF attacks work and provide you with the basic principles to protect your web application. To have a deeper insight into CSRF defenses, please check out the OWASP CSRF prevention cheat sheet.