Cross-Site Request Forgery (CSRF)  Prevention In A Node.js Backend Application

Photo by Arget on Unsplash

Cross-Site Request Forgery (CSRF) Prevention In A Node.js Backend Application

Learn how to prevent CSRF attacks in Node.js with the use of CSRF tokens

What is Cross-Site Request Forgery (CSRF)?

Cross-Site Request Forgery (CSRF) is an attack where the attacker tricks the users of a trusted site into executing dangerous and unwanted actions on the web application in which they’re currently authenticated/logged in, usually with a little help of social engineering (such as sending a link via email or chat). A successful CSRF attack can end up performing undesired functions by sending state changing requests on the victim’s behalf like fund transfers, changing email addresses of victims, and so on. At worst case scenario, an entire web application can be compromised if this attack is successfully done on an admin account.

In effect, CSRF attacks are used by an attacker to make a target system perform a function via the victim's browser, without the victim's knowledge, at least until the unauthorized transaction has been committed.

CSRF uses an attack vector that specifically attacks requests where the browser automatically provides authentication (typically cookies), and that means that it is usually done on users with active logged in sessions on the application. A CSRF attack works because browser requests automatically include all cookies including session cookies.

How Does It work?

There many ways to orchestrate this form of attack, one of the more popular example is to trick users into submitting a form or loading a maliciously crafted URL link with social engineering. This URL may be used to send a request to send an amount of money from a victim's account to the attackers account. or to change a user's email address to the attacker's email address. Yes, you may lose your money or login access to a trusted site through a CSRF attack.

The social engineering aspect may be to send an email with html contents to the victim or to plant deadly scripts on webpages that a victim is likely to visit.

Prevention

The possibility of performing CSRF attacks makes it important to add an extra layer of authentication in an application. All that had to be done before is to authenticate users by asking them to login before performing tasks, but we now have to authenticate each request.

One of the methods of thwarting this kind of attack is using CSRF tokens. So, how do CSRF tokens work?

  • Server sends the client a token.
  • Client submits a request with the token.
  • The server rejects the request if the token is invalid or unavailable.

    CSRF tokens can prevent CSRF attacks by making it impossible for an attacker to construct a fully valid HTTP request suitable for feeding to a victim user. Since the attacker cannot determine or predict the value of a user’s CSRF token, they cannot construct a request with all the parameters that are necessary for the application to honor the request. Fortunately, there is a Node.js CSRF protection middleware, the Express team's csrf and csurf modules.

Using CSRF Tokens WIth Express

This is a summarized line-by-line walkthrough of the whole process of using the CSURF module,

  • Initialize a new node.js project with npm init -y

  • Install the following npm packages to start the project. You can search for what each package does for a clearer picture. npm install express esm csurf cookie-parser body-parser nodemon morgan cors

  • In the package.json file, add the start script for the project as follows, (the use of esm (es modules) is optional).

"scripts": {
    "start": "nodemon -r esm index.js"
  },
  • Create an index.js file in the root folder and start to import the needed modules in there.
import express from "express"; //express app
import cors from "cors"; // for handling cross-site request headers
import csrf from "csurf"; // for CSRF token creation and validation
import cookieParser from "cookie-parser"; // csurf requires a cookie or session middleware
import bodyParser from "body-parser" // request body handler
const morgan = require("morgan"); // (OPTIONAL) HTTP request logger middleware for node.js
  • Create an express app and setup route middlewares

    var csrfProtection = csrf({ cookie: true })
    var parseForm = bodyParser.urlencoded({ extended: false })
    // create express app
    const app = express()
    
  • Apply express middlewares

    app.use(
    cors({
      origin: "http://localhost:3000", // request origin, set this to the url is expected to originate from
      methods: "GET,HEAD,PUT,PATCH,POST,DELETE", // allowed request methods
      allowedHeaders:
        "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-CSRF-Token",
    })
    );
    app.use(cookieParser());
    app.use(morgan("dev"));
    app.use(csrfProtection);
    
  • Setup test routes and controllers

// route to serve CSRF Tokens
app.get("/api/csrf-token", (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// sample PUT request route for testing purposes
app.put("/api/data", (req, res) => {
  res.json({ ok: true });
});
  • Listen for requests at your chosen port (8000 in this case) and start server with npm start
// port
const port = 8000;
app.listen(port, () => console.log(`Server is running on port ${port}`));

Whenever a PUT request is made to the /api/data route, the HTTP headers is automatically checked for any value that includes X-XSRF-Token, X-CSRF-Token, XSRF-Token or CSRF-Token. This is then validated on the backend, the request will be allowed or rejected based on the comparison result.

Implementing A Client Using React and axios to send requests.

A typical react app or any frontend application should send all state changing requests with the generated CSRF tokens and all session cookies received from the backend application.

import { useState, useEffect } from "react";
import axios from "axios";
import "./App.css";

function App() {
  const [email, setEmail] = useState("you@gmail.com");

 // function to get CSRF Token from backend
  const getCsrfToken = async () => {
    const { data } = await axios.get("http://localhost:8000/api/csrf-token");
    // log data received
    console.log("CSRF", data);
    // set default header with axios
    axios.defaults.headers["X-CSRF-TOKEN"] = data.csrfToken;
  };

  // send request on page load
  useEffect(() => {
    getCsrfToken();
  }, []);

  // send post request
  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const { data } = await axios.put(
        "http://localhost:8000/api/data",
        {
          email,
        },
        { withCredentials: true }
      );
      console.log(data);
    } catch (err) {
      console.log(err.response.data);
    }
  };

  // jsx
  return (
    <div className="App">
      <header className="App-header">
        <form onSubmit={handleSubmit}>
          <input
            type="email"
            className="input"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="Enter email"
            required
          />
          <button type="submit" className="button">
            Submit
          </button>
        </form>
      </header>
    </div>
  );
}

export default App;

Conclusion

It is worth looking into for anyone working with cookie-based authentication to implement a CSRF defence technique to prevent malicious individuals from sending dangerous state changing requests. Modern browsers are increasingly beneficial at thwarting a lot of malicious attempts from attackers but developers should not be too lax at implementing custom security protocols because you can not be too secure. For more information on CSRF and other prevention methods, the referenced links below will be of immense help.

References