Scott Smith

Blog Tutorials Projects Speaking RSS

Secure Node Apps Against OWASP Top 10 - Cross Site Request Forgery

Welcome to part 4 of the OWASP security series

  1. Injection
  2. Broken Authentication & Session Management
  3. Cross Site Scripting (XSS)
  4. Cross Site Request Forgery (CSRF)
  5. Using Components with Known Vulnerabilities (Coming soon)

In this multipart series, we will explore some of the the OWASP top web application security flaws including how they work and best practices to protect your application from them. The focus will be on Express web applications in Node, but the principles shown can be applied to any framework or environment.

This part will cover cross site request forgery (CSRF).

Cross Site Request Forgery (CSRF)

So what exactly is a cross site request forgery vulnerability?

A CSRF attack occurs when an attacker is able to create forged HTTP requests and trick the victim into making those requests via image tags, XSS, and many other ways. When the user makes these malicious requests and is authenticated with the application, the attack can be even more devastating. The attacker is able to get the user to perform state changing operations that the user is authorized to do in the application such as updating account details, making purchases, transferring money, and even deleting the account. Essentially, the attacker takes advantage of the website’s trust in the user.

We will go over a few different scenarios and show how to protect against them.

Scenario 1: Changes allowed via GET requests

This first scenario occurs when a website allows changes to be done via GET requests.

CSRF Scenario 1

Login

The first two steps show the victim logging into their bank. This is important because the user must be logged in for the CSRF attack to work.

GET /index.html

The attacker has a page on a site they control and shares a link on social media. Our user clicks the link to see what it is and their browser makes a request to /index.html on the attacker’s site.

HTTP/1.1 200 OK

Requests to /index.html returns the following HTML content. If you look at the image tag, you will see it will make a request directly to the bank’s transfer page along with some query string parameters specifying an account to transfer an amount of money to.

1
2
3
4
5
<html>

  <img src="https://bank.com/transfer?to=12345&dollars=1000000" width="0" height="0">

</html>

GET /transfer?to…

This is where things get bad. Because the bank supports changes via GET requests, the user browsing to the attackers site will automatically make a GET request to https://bank.com/transfer?to=12345&dollars=1000000. Because the user has already logged in, their session cookie is passed along in the request and the attack will succeed.

Solution

The solution here is very simple. Never allow changes to occur on GET requests. Only allow changes to be made when the HTTP method is POST, PUT, or DELETE.

Scenario 2: Posting exploited data to sites

We have now locked our applications down to no longer allow changes via GET requests. This next attack occurs by getting the user to automatically submit a malicious POST request to the web application.

CSRF Scenario 2

Login

The first two steps show the victim logging into their bank.

GET /index.html

The attacker has a page on a site they control and shares a link on social media. Our user clicks the link to see what it is and their browser makes a request to /index.html on the attacker’s site.

HTTP/1.1 200 OK

Requests to /index.html returns the following HTML content. What is going on here is the attacker is creating a form that when submitted will POST to our transfer endpoint on our web application. The content then adds a script that will automatically trigger that POST.

1
2
3
4
5
6
7
8
9
10
<html>

  <form name="bad" method="post" action="https://bank.com/transfer">
    <input type="hidden" name="to" value="12345">
    <input type="hidden" name="dollars" value="1000000">
  </form>

  <script>document.bad.submit()</script>

</html>

POST /transfer?to…

This is where things get bad, again. Because the user is logged in and the attacker was able to get the user’s browser to automatically POST a malicious request to our web application, the user’s bank account will have been successfully hacked.

Solution

The solution here is to implement the synchronizer token pattern.

The following sample Express application shows how to implement this using the csurf npm package.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var express = require('express');
var csrf    = require('csurf');

var app = express();

app.use(csrf());

app.use(function(req, res, next) {
  res.locals._csrf = req.csrfToken();
  next();
});

//Add _csrf to rendered HTML forms as hidden field (see HTML below)

app.listen(80);
1
2
3
4
5
6
7
8
9
<html>

  <form method="post" action="transfer">
    <input type="hidden" name="_csrf" value="_csrf">
    <input type="text" name="to">
    <input type="test" name="dollars">
  </form>

</html>

What this code does is makes it so that all POSTs made to our application MUST include a CSRF token or nonce. This token is set when the user makes a request to our page that contains the form and expects that same token when a POST is made. If the token is not there, the POST is not allowed. This will defeat the scenario we just went over because the attacker will not be able to generate a token that our application is aware of.

Scenario 3: Bypassing CSRF protections

Now that we do not make changes via GET requests as well as implementing CSRF protection via nonce tokens, we are still vulnerable to some attacks that can get around CSRF protection.

CSRF Scenario 3

Login

The first two steps show the victim logging into their bank.

GET /index.html

The attacker has a page on a site they control and shares a link on social media. Our user clicks the link to see what it is and their browser makes a request to /index.html on the attacker’s site.

HTTP/1.1 200 OK

Requests to /index.html returns the following HTML content. What is going on here is the attacker is creating an iframe that loads up our page along with our form that includes the fields and the CSRF nonce token.

1
2
3
4
5
<html>

  <iframe src="https://bank.com/transfer?to=12345&dollars=1000000">

</html>

For the sake of this scenario, the resulting form that would be loaded will look like the following.

1
2
3
4
5
<form method="post">
 <input type="text" name="to" value="">
 <input type="text" name="dollars" value="">
 <input type="hidden" name="csrf" value="a0d73b12">
</form>

And finally, here is our code on our server handling POSTs to the transfer endpoint.

1
2
3
4
5
6
7
8
app.post('/transfer', function (req, res) {
  if (isValid(req.body.csrfToken)) {
    var to = req.params.to || req.body.to;
    var dollars = req.params.dollars || req.body.dollars;

    //Transfer money
  }
});

POST /transfer?to…

This is where things get bad, yet again. Like before, our user is logged in. The content from our page including the form is now loaded within an attackers site. Through clickjacking techniques the attack could potentially get the user to submit the form.

Solution 1

Before we discuss the solution, we need to understand what is wrong with our current setup. Even if the page loads up our form, the user would have to manually enter a bank account along with money for the form submission to work, right? Well, not exactly. Because we are not explicitly setting the form action, the default URL that will be POSTed to will be the Url the page was loaded from. In our case this will be the Url the attacker entered which contains query string parameters for the account to send money to and the amount.

If you look at how we coded our handling of the POST, we are using parameters from either the query string or the POSTed data. In this example and attack, the query string parameters would get used and the attack would succeed.

The first solution to this problem is to always set the form action. Instead of leaving it blank, you should always set it to the endpoint you want to submit to.

1
2
3
4
5
<form method="post" action="https://bank.com/transfer">
 <input type="text" name="to" value="">
 <input type="text" name="dollars" value="">
 <input type="hidden" name="csrf" value="a0d73b12">
</form>

This will make it so the attacker cannot get the user to submit the form to an endpoint along with query string parameters.

Solution 2

The second solution goes along with the first. This one is to never use query string parameters for user input. This will force our code to only use parameters sent via the POSTed form data.

1
2
3
4
5
6
7
8
app.post('/transfer', function (req, res) {
  if (isValid(req.body.csrfToken)) {
    var to = req.body.to;
    var dollars = req.body.dollars;

    //Transfer money
  }
});

Solution 3

The final solution is one that is good to implement on all your applications in order to control how your application behaves and to help avoid this type of attack. The solution is to use the X-Frame-Options header.

By taking advantage of this header, you can tell web browsers (that support it) to never allow your application within frames. With this in place, the attacker will not be able to load your page within theirs.

Here is an example Express application using the helmet npm package to do this.

1
2
3
4
5
6
7
8
var express = require('express');
var helmet  = require('helmet');

var app = express();

app.use(helmet.xframe('deny'));       //never allow in frames

app.listen(80);

This code will send back a response header that will tell the browser to never allow it to be within a frame. Here is what the header looks like.

1
X-Frame-Options: DENY

Wrap up

I hope that by learning how CSRF attacks are performed you have a better understanding of how to protect your applications. I have shown a few ways in which you can protect yourself but it is important to learn more on this subject using the links I shared above. Also, these solutions are not exclusive. You should implement many layers of protection for your application.

I have a lot more tutorials coming so be sure to subscribe to my RSS feed or follow me on Twitter. Also, if there are certain topics you would like me to write on, feel free to leave comments and let me know.