In this blog post, I will be writing about a method that could be used to prevent a CSRF attack.

What is a CSRF?

CSRF stands for Cross Site Request Forgery. A CSRF vulnerability can be exploited when a web application has an authenticated browser session which is the only way used by the backend server to validate the requests coming from the frontend.

For example, if the backend server of a system does not contain any precautions for CSRF attacks, an attacker could simply trick you to give them your session cookies by making you to submit a forged form and then use them to do alterations within the system using your session.

Synchronizer Token Pattern

This is one of the methods that could be used to prevent CSRF attacks. Here, an extra token known as the CSRF token is generated by the server side application and used to validate the requests coming from the frontend. To understand things a bit more easily lets get on with the implementations.

The following implementations will show you how this pattern could be implemented within a simple NodeJS application, which simply contains a login screen and a form where user could submit a post.

Once the user logs in to the application using the username and password, the frontend sends a POST request to the /login endpoint where the it is processed by validating the user input and then by creating a session identified along with the csrf token. The session identifier is set in the cookies of the response sent, which is a redirection to the form page.

When the form page is loaded, it sends a request to the /tokens endpoint, where the csrf token for the respective session identifier which comes along with the request as a cookie is sent back in response. Upon receiving the token, the frontend set it within a hidden field in the form, so that it is embedded in all the outgoing requests.

The server side, upon receiving a request to the /posts endpoints, validates it comparing the token that comes along with it and that is stored in the memory on the server side. If the tokens match, a success response is sent or else an error is sent back.

const express = require('express'),
  cookieParser = require('cookie-parser'),
  randomBytes = require('random-bytes'),
  bodyParser = require('body-parser'),
  app = express(),
  PORT = 9090;

app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(express.static('public'));

const SESSION_DATA = {};

app.get('/', (req, res) => {
  let session_id = req.cookies['session-id'];
  if (session_id && SESSION_DATA[session_id]) {
    res.sendFile('public/form.html', { root: __dirname });
  } else {
    res.sendFile('public/login.html', { root: __dirname });
  }
});

app.post('/login', (req, res) => {
  let username = req.body.username;
  let password = req.body.password;
  if (username === undefined || username === "") {
    res.status(400).json({ success: false, message: "Username undefined" });
    return;
  }
  if (password === undefined || password === "") {
    res.status(400).json({ success: false, message: "Password undefined" });
    return;
  }
  if (username === "demo" && password === "pass123$") {
    let session_id = Buffer.from(randomBytes.sync(32)).toString('base64');
    let csrf_token = Buffer.from(randomBytes.sync(32)).toString('base64');
    SESSION_DATA[session_id] = csrf_token;
    res.setHeader('Set-Cookie', [`session-id=${session_id}`, `time=${Date.now()}`]);
    res.sendFile('public/form.html', { root: __dirname });
  } else {
    res.status(405).json({ success: false, message: "Unauthorized user" });
    res.redirect('/');
  }
});

app.post('/posts', (req, res) => {
  let session_id = req.cookies['session-id'];
  if (session_id && SESSION_DATA[session_id]) {
    if (SESSION_DATA[session_id] === req.body.csrf_token) {
      res.status(200).json({ success: true });
    } else {
      res.status(400).json({ success: false });
    }
  } else {
    res.sendFile('public/login.html', { root: __dirname });
  }
});

app.get('/tokens', (req, res) => {
  let session_id = req.cookies['session-id'];
  if (session_id && SESSION_DATA[session_id]) {
    res.status(200).json({ success: true, token: SESSION_DATA[session_id] });
  } else {
    res.status(400).json({ success: false, message: 'Token unavailable' });
  }
});

Repository: https://github.com/ntbandara3/synchronizer-token-pattern