CSRF For Express.js Apps Made Easy with Same-Site Cookie


Express 4.14.0 was just published. With it an update that makes defending against Cross-Site Request Forgery (CSRF) easier. This post will give an overview of CSRF, talk about historical defense mechanisms and finally show you how to use the Same-Site cookie feature new in Express.


CSRF is one of the most commonly and easily exploited web application vulnerabilities. Part of the reason why it’s so common is that, if we’re honest, most mitigation techniques are tedious to implement, and very easy to get wrong. But before getting into that, a refresher on CSRF is in order.
Despite the many different methods we have today for storing and transmitting authentication data, session cookies remain immensely popular, and unfortunately, nuances in the way cookies work are what enables CSRF to work. Cookies, once assigned, will be sent with every request the browser sends to the originating domain. The problem is, there’s not an easy way for the server to verify that a cookie set for example.com, included in a request to example.com, was sent by HTML loaded from and controlled by example.com. In essence, CSRF is a method of hijacking the trust a server has for requests coming from a user’s browser.
One common example of this is with home routers: You visit an untrusted website, and that website causes your browser to send a request on your behalf to add a new user to your home router. The browser complies, and includes the session cookie you received after logging in earlier along with the request, and now your router is compromised.


With that out of the way, let’s talk about CSRF prevention, and specifically prevention in applications written in node.js. Unlike with attacks like Cross-Site Scripting, there are a multitude of different schemes and systems that have been used to prevent CSRF, and not all of them are equal. Here are some of the most common methods of preventing CSRF:

  1. Anti-CSRF Tokens:
    Anti-CSRF Tokens are cryptographically secure random numbers, linked to a session identifier, and required to be submitted with every non-GET request in a location that doesn’t submit automatically - optimally a header or request body parameter, although on occasion query string parameters are used as well. This token is verified by the server upon receiving the request, to ensure that it matches the token stored with the associated session. This is a secure method for preventing CSRF, when implemented perfectly. It’s main downside is that it is tedious and time consuming to implement, as well as being prone to missing routes and leaving them unprotected.
  2. Naïve Double Submit:
    Naïve Double Submit is a method where an unguessable value is sent as both a cookie and a request body parameter or query string parameter. Upon receiving the request, the server verifies that the two values are equal before continuing. This method relies on the attacker only having the ability to send cross-site requests via an HTML page hosted on a separate domain, assuming that the attacker does not have javascript execution on the target domain or any subdomains - which is why it is referred to as “Naïve.” It makes too many assumptions about the attackers restrictions. This method is too easily defeated, as all an attacker needs to do is gain javascript execution on the target domain or a subdomain and write a cookie containing a value known by the attacker, which is then sent along with the CSRF vector. As such, while it is commonly used, it is not secure and is best avoided.
  3. Referrer Checking:
    Referrer Checking is a method of CSRF protection that relies on the referrer header sent by the browser with requests to the server, verifying that the domain in the header matches the domain specified in the requests HOST header. There are two crucial points in getting this method right: First, that the request must be rejected if the referrer is empty or not present. There are a variety of ways to strip a referrer header from a request entirely, such as making the request from a page visited over HTTPS. Secondly, the method used to compare the referrer and HOST headers, commonly a regex, does a strict match. With a regex this takes the form of an anchored regex, so that a request originating from attacker-example.com will not match on a request sent to example.com. Ultimately, while this method is secure if implemented correctly, it is not the most robust solution.
  4. JSON-Web-Tokens ( JWT )/Bearer Tokens:
    This is without a doubt one of the best methods of preventing CSRF. The underlying mechanism is simple: Don’t use cookies to store sessions. Instead, a value saved in local or session storage is used, such as a JWT, and then sent as a header with every request. The downside to this method is that every request must be sent via XHR, and while that is not a problem with modern single page web applications, older applications written in pure HTML will not find implementing this method to be an easy task.
    These methods, with the exception of Naïve Double Submit, do work well. And out of the multitudes of web applications and frameworks the Lift team has audited, the number of developers that are not aware that CSRF is a problem is a very small one. But the percentage of applications without CSRF protection is unacceptably high.
    And being developers ourselves, we understand why: just thinking about having to implement CSRF protection is enough to cause a headache. Like the excellent but rarely used GPG encryption method, the barrier to entry is just too high, while the payoff seems intangible.

The good folks over at the IETF have recently drafted a standard to help developers protect against CSRF with less hassle. It’s called the Same-Site cookie flag, and it’s incredibly simple to both implement and understand.
One of the reasons we have the problems with cookies and CSRF that we do is that the Same-Origin Policy ( SOP ), which we rely on so heavily for web application security, has not retroactively extended to cover cookies. That’s understandable - it would break a massive amount of applications. But it does cause problems.
The Same-Site cookie flag, is in essence, an opt-in way to extend the SOP to cookies. When you set the flag on a cookie, you can choose one of two ( currently ) possible modes - strict or lax.
In lax mode, the browser will apply the SOP to the cookie as it relates to requests other than GET requests - i.e. if the flag is set, and a POST request is made originating from another origin, it will refuse to send the specified cookie with the request.
In strict mode, the same as with lax mode applies, but is extended to GET requests as well. This can cause problems however - For example, if Twitter were to set the flag to strict mode on their authentication cookie, those handy buttons that let folks tweet a snippet from an article would no longer work.
There are a few other things to be aware of when using the same-site cookie flag to prevent CSRF:

  1. The use of lax mode means the application is still vulnerable to various kinds of CSRF attacks if you allow state to be changed via GET. So a brief audit of the applications routes to ensure it is a RESTful API without exception is in order.
  2. Not all browsers support this yet, and users will likely be using browsers that do not support it for quite some time. That said, if you don’t have CSRF protection in your app yet, you can enable it for what will likely be the majority of your users right now, today, in just a few minutes as a stop-gap. But it is a good idea to put implementing a secondary method on the roadmap, especially if you see a large amount of legacy browsers.

Implementing it is incredibly simple. Here’s how you can enable it for a cookie in your Express >= v4.14 App:

1
res.cookie('sessionID', user.sessionID, { secure: true, sameSite: true });

And here’s what a functional Demo App would look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/******************************************************************
Express CSRF Protection via Same-Site Demo app.
Be sure you have [email protected] and cookie-parser installed first!
usage: node ./app.js ${SAME_SITE_FLAG_VAL}
Visit localhost:3000 (not 127.0.0.1 - we need two origins) to see the same-site cookie in action.
******************************************************************/
var express = require('express');
var cookieParser = require('cookie-parser');
var crypto = require('crypto');
var app = express();
var app_CSRF = express();
app.use(cookieParser());
var sameSite = process.argv[2] || true;
app.locals.sessionID = crypto.randomBytes(32).toString('base64');
app.get('/', function (req, res) {
res
.status(200)
.cookie('sessionID', app.locals.sessionID, { sameSite: sameSite })
.send('<html><body>Session Cookie Set!<form method="POST" action="http://localhost:3000/"><input type="submit"\></form></body></html>')
.end();
});
app.post('/', function(req, res) {
// Yes, this should be a secure time comparison! But it's a demo. Don't do this in your app.
if(req.cookies.sessionID === app.locals.sessionID){
res
.status(200)
.send('Success! You have sent your POST request from a valid origin, and it passes CSRF checking.<a href="http://127.0.0.1:3001">Test from Cross-Site.</a>')
.end();
} else {
res
.status(403)
.send('Fail! Either you do not have a session cookie set, or this POST request came from a different origin and the session cookie was not sent with the request. Access is denied.')
.end();
}
});
app_CSRF.get('/', function (req, res) {
res
.status(200)
.send('<html><body>Attempt to send request from a separate origin:<form method="POST" action="http://localhost:3000/"><input type="submit"\></form></body></html>')
.end();
});
app_CSRF.listen(3001, function () {
})
app.listen(3000, function () {
console.log('Same-Site CSRF mitigation demo app running on localhost:3000!');
});