CSRF in Action 🎭
Tags: csrf, node, cors, javascript
Published: 2019-07-01 08:54:08

---
title: CSRF in Action 🎭
published: true
description: Demonstration of Cross Site Request Forgery(CSRF) with a simple todo app.
tags: ['CSRF', 'NodeJs', 'CORS', 'JavaScript']
---

> Originally published on https://smellycode.com/csrf-in-action/

<a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery" target="_blank">Cross-Site Request Forgery(CSRF/XSRF)</a> is one of the most popular ways of exploiting a server. It attacks the server by forcing the client to perform an unwanted action. This attack targets applications where the client/user is already logged in. It mainly changes the state of the server by making inadvertent updates or transfer of data. For example, updating vital information like emails contact numbers, etc. or transferring data from one entity to another.

![](https://smellycode.com/static/0af882791a9890316f4527090a8cd1b6/19607/csrf-in-action-cover-img.jpg)

This post demonstrates CSRF attack and elaborates concepts linger around it. It uses a simple todo app and an evil client--which updates the state of todos--for demonstration. Technologies used:

- <a href="https://reactjs.org/" target="_blank">ReactJs</a> for client.
- <a href="https://expressjs.com/" target="_blank" rel="noreferrer noopener">ExpressJs</a> and a couple of middlewares(<a href="https://github.com/expressjs/cors" target="_blank" rel="noreferrer noopener">CORS</a>, <a href="https://github.com/expressjs/body-parser" target="_blank" rel="noreferrer noopener">body-parser</a>, <a href="https://github.com/expressjs/cookie-parser" target="_blank" rel="noreferrer noopener">cookie-parser</a>, etc) for server.
- <a href="https://www.mongodb.com/" target="_blank" rel="noreferrer noopener">MongoDb</a> as database and <a href="https://mongoosejs.com/" target="_blank" rel="noreferrer noopener">Mongoose</a> for data modeling.
- <a href="https://jwt.io/" target="_blank" rel="noreferrer noopener">JWT</a> for stateless session management.
- and a few other stuff.

The sample todo app uses JSON Web Token for stateless session management and authentication. It stores the token in a cookie with `httpOnly` flag to make the token <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Secure_and_HttpOnly_cookies">inaccessible to the JavaScript running on the client</a>. Picture below depicts the auth flow of the app<a name="basicAuthFlow"></a>.

![App auth flow without CSRF token](https://smellycode.com/static/9d9a303caf109383412d6d9474a645a0/dc6f3/basic-auth-flow.jpg)

Let's take a gander at the code organization of the app. The <a href="https://github.com/hk-skit/csrf-in-action" target="_blank">codebase</a> has three actors -- a server, a client, and an evil client.

The **server** exposes a few endpoints for CRUD operations on both user(`/users`) and todo(`/todos`). It uses **mongoose** to store data in **MongoDB**. It also supports <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">cross-origin requests</a> from a client running at `localhost:3001`(middleware <a href="https://github.com/expressjs/cors" target="_blank">cors</a> is used to enable cross-origin resource sharing). The server runs at http://localhost:3000.

The **client** has a simple _login form_ and a _todo list_. It uses ReactJs to build the UI and <a href="https://github.com/axios/axios" target="_blank">axios</a> for ajax calls. When the client is loaded, it fetches todos(GET, `/todos`) of the logged in user. If there's an authentication error(status code is 401), it directs the user to login. Todos are successfully fetched only when the user is logged in.

![Simple todo app in action](https://smellycode.com/sample-todo-app-832053812a926b2348fe3ca17d5d336e.gif)

The **evil client** runs at http://locahost:3002 with the help of the package <a href="https://www.npmjs.com/package/http-server" target="_blank">http-server</a>. It has a plain HTML page and a _form_. The form opens its action in a hidden _iframe_ for <a href="https://stackoverflow.com/questions/17940811/example-of-silently-submitting-a-post-form-csrf" target="_blank" rel="noreferrer noopener">silent submission</a>. The app lures the user to click on a button which stimulates the form submission. Form submission makes a **post** call to http://localhost:3000/todos/complete which marks todos belonging to the logged in user as complete.

```html
<!DOCTYPE html>
<html>
  <body>
    <h1>Hey There!</h1>
    <p
      >Having a rough day! Don't worry, I have got a picture of a cute cat to
      cheer you up. <button id="btn_cat">Show me 🐱</button>
    </p>
    <iframe style="display:none" name="csrf-frame"></iframe>
    <form
      method="POST"
      action="http://localhost:3000/todos/complete"
      target="csrf-frame"
      id="csrf-form"
    >
    </form>
    <script type="text/javascript">
      document.getElementById('btn_cat').addEventListener('click', () => {
        document.getElementById('csrf-form').submit();
      });
    </script>
  </body>
</html>
```

Evil client in action:

![Evil client in action](https://smellycode.com/evil-client-in-action-1ea7dda2ae58b69982bc7d632d5b990d.gif)

Let's address questions which create confusion.

**Q: Why no authentication error?** 🤔

The server does not throw any authentication error cause the request contains a valid JWT token. The request gets the token from cookies.

> When receiving an HTTP request, a server can send a `Set-Cookie` header with the response. The cookie is usually stored by the browser, and then the cookie is sent with requests made to the same server inside a `Cookie` HTTP header. ~ <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies">Mozila</a>

When the user logs in, the JWT is stored in an `httpOnly` cookie(see [auth flow](#basicAuthFlow)). _Cookies are sent with every request to the same server_. Because of that, the JWT becomes part of every request 🤖.

**Q: Shouldn't the CORS setup help here?**

Let's talk about <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">CORS</a> before jumping to the answer. Browsers limit the interaction of scripts or document loaded on one origin(a tuple of protocol, domain, and port) with another origin to avoid Jungle Raj. The mechanism used for imposing such limitations is known as <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy">Same Origin Policy</a>. It ensures that applications are running in isolated environments. Sometimes, developers need to relax the same-origin policy so that applications can interact with each other. That's what originates the idea of **C**ross-**O**rigin **R**esource **S**haring(CORS). CORS allows `site-a` to interact with `site-b` only if `site-b` agrees--by responding with appropriate HTTP headers. To enable CORS, the server needs a tad of work(the sample todo app uses <a href="https://github.com/expressjs/cors" target="_blank">cors</a> middleware for the same).

In the browser world, ajax requests are classified into three categories:

1. Simple Request
2. Non-simple request
3. Preflight request ✈️.

More details on these can be found <a href="https://frontendian.co/cors" target="_blank">here</a>.

Whenever a cross-origin resource is requested using a non-simple request, the browser makes a pre-flight `OPTIONS` request. The server responds to the pre-flight request with appropriate response headers. If the origin and the request method are present in `Access-Control-Allow-Origin` and `Access-Control-Allow-Methods`, the browser originates the main request. Otherwise, a cors error is thrown with a pertinent message.

![Preflight request](https://smellycode.com/static/380c0d72b7d2e187c8a39468756dc16b/26dcc/preflight_.png)

Network logs of the todo app with preflight requests.

![Network logs of preflight request](https://smellycode.com/static/c96b52fff3097ed31ce6a4e084f97c5e/e199c/prefligh_request_logs.png)

**For simple requests, the browser doesn't intiate any preflgiht request.** The malicious client leverages this fact to bypass the Same Origin Policy with the help of an HTML form. That's why CORS set up doesn't help here 🤯.

![simple request](https://smellycode.com/static/63958e7743ea262c379f4466c52844a6/42fef/simple_req.png)

**Q: What if WebStorage is used to store JWT instead of httpOnly cookie?**

Storing JWT in the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API" target="_blank">Web Storage</a> will make the app less vulnerable for CSRF attacks. But it spikes the chances of the token being compromised. That's because any JavaScript running on the client has access to the web storage. It's DANGEROUS 🛑.

**Q: How to prevent CSRF?**

The challenge for the server is to validate both the token and the source of the request i.e. origin. The token validation is already implemented. The server needs to verify the source of the request for CSRF protection. The source can either be verified with the help of **CORS Origin Header** or an **XSRF Token**. Shielding server with XSRF token(CSRF token) is more reliable and popular than <a href="https://stackoverflow.com/questions/24680302/csrf-protection-with-cors-origin-header-vs-csrf-token" target="_blank">CORS Origin Header</a>.

The implementation of the XSRF token is straight forward. When the client represents valid credentials, the server generates a random unguessable unique string named as `xsrfToken`. It puts the `xsrfToken` in JWT along with other claims. The server also adds an `xsrfToken` in a cookie(why cookie? cause _cookies are limited by same-origin policy_). Here's a sample JWT payload with `xsrfToken`:

```json
{
  "sub": "hk",
  "xsrfToken": "cjwt3tcmt00056tnvcfvnh4n1",
  "iat": 1560336079
}
```

The client reads the token from cookies and adds the token to request headers as `X-XSRF-TOKEN` before making requests. When the server receives a request, it reads `xsrfToken` from JWT payload and compares with the `X-XSRF-TOKEN` header. If both are same then the request is further processed otherwise it is terminated with status code 401. This technique is also known as **Double Submit Cookies** method.

The auth flow with XSRF token:

![JWT auth flow with CSRF](https://smellycode.com/static/f0cf82191e56c34d3b52595702d76626/a1765/auth-flow-csrf.jpg)

Code version of the same with <a href="https://github.com/auth0/express-jwt" target="_blank">express-jwt</a>:

```javascript
const expressJwt = require('express-jwt');

// Paths without token.
const publicRoutes = ['/users/register', '/users/authenticate'];

const isRevoked = async (req, payload, done) => {
  const { xsrfToken } = payload;
  done(null, xsrfToken !== req.get('X-XSRF-TOKEN'));
};

module.exports = () =>
  expressJwt({
    secret: process.env.JWT_SECRET,

    getToken: req =>
      req.get('X-XSRF-TOKEN') && req.cookies.jwtToken
        ? req.cookies.jwtToken
        : null,
    isRevoked
  }).unless({
    path: publicRoutes
  });
```

Client side request interceptor with <a href="https://github.com/axios/axios" target="_blank">axios</a>:

```javascript
import axios from 'axios';

const getCookies = () =>
  document.cookie.split(';').reduce((cookies, item) => {
    const [name, value] = item.split('=');
    cookies[name] = value;
    return cookies;
  }, {});

const baseURL = 'http://localhost:3000';

const ajax = axios.create({
  baseURL,
  timeout: 5000,
  withCredentials: true
});

// Add a request interceptor
ajax.interceptors.request.use(function(config) {
  const xsrfToken = getCookies()['xsrfToken'];
  // CSRF Token.
  if (xsrfToken) config.headers['X-XSRF-TOKEN'] = xsrfToken;
  return config;
});

export default ajax;
```

Note: <em>Real-world applications require a more elegant mechanism for handling CSRF tokens. You may want to use the middleware <a href="https://github.com/expressjs/csurf" target="_blank">csurf</a></em>.

The evil client after CSRF token:

![Evil client after CSRF token](https://smellycode.com/evil-client-after-csrf-fc47e6948f89fa58a2058ca3eac707d3.gif)

The final code of the sample app is uploaded <a href="https://github.com/hk-skit/csrf-in-action" target="_blank">here</a>. Thanks for reading 🙏🏻.