Smelly Code

CSRF in Action 🎭

June 10, 20197 min 👓

Cross-Site Request Forgery(CSRF/XSRF) 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.

csrf in action cover img

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:

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 inaccessible to the JavaScript running on the client. Picture below depicts the auth flow of the app.

App auth flow without CSRF token

Let’s take a gander at the code organization of the app. The codebase 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 cross-origin requests from a client running at localhost:3001(middleware cors 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 axios 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

The evil client runs at http://locahost:3002 with the help of the package http-server. It has a plain HTML page and a form. The form opens its action in a hidden iframe for silent submission. 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.

<!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

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. ~ Mozila

When the user logs in, the JWT is stored in an httpOnly cookie(see auth flow). 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 CORS 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 Same Origin Policy. 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 Cross-Origin Resource Sharing(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 cors 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 here.

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

Network logs of the todo app with preflight requests.

Network logs of preflight request

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

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

Storing JWT in the Web Storage 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 CORS Origin Header.

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:

{
  "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

Code version of the same with express-jwt:

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 axios:

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: Real-world applications require a more elegant mechanism for handling CSRF tokens. You may want to use the middleware csurf.

The evil client after CSRF token:

Evil client after CSRF token

The final code of the sample app is uploaded here. Thanks for reading 🙏🏻.


Hitesh

Hi, I am Hitesh.

|