Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Node.js Cookbook

You're reading from   Node.js Cookbook Practical recipes for building server-side web applications with Node.js 22

Arrow left icon
Product type Paperback
Published in Nov 2024
Publisher Packt
ISBN-13 9781804619810
Length 456 pages
Edition 5th Edition
Languages
Tools
Arrow right icon
Authors (2):
Arrow left icon
Bethany Griggs Bethany Griggs
Author Profile Icon Bethany Griggs
Bethany Griggs
Manuel Spigolon Manuel Spigolon
Author Profile Icon Manuel Spigolon
Manuel Spigolon
Arrow right icon
View More author details
Toc

Table of Contents (15) Chapters Close

Preface 1. Chapter 1: Introducing Node.js 22 2. Chapter 2: Interacting with the File System FREE CHAPTER 3. Chapter 3: Working with Streams 4. Chapter 4: Using Web Protocols 5. Chapter 5: Developing Node.js Modules 6. Chapter 6: Working with Fastify – The Web Framework 7. Chapter 7: Persisting to Databases 8. Chapter 8: Testing with Node.js 9. Chapter 9: Dealing with Security 10. Chapter 10: Optimizing Performance 11. Chapter 11: Deploying Node.js Microservices 12. Chapter 12: Debugging Node.js 13. Index 14. Other Books You May Enjoy

Preventing cross-site request forgery

CSRF is an attack where a malicious web application causes a user’s web browser to execute an action on another trusted web application where the user is logged in.

In this recipe, we’re going to learn how to secure an Express.js server against CSRF attacks.

Important note

Browser security has improved significantly in recent years. It’s very difficult to replicate a CSRF attack on any modern browser. However, as there are still many users on older browsers, it’s important to understand how these attacks work and how to protect against them. In this recipe, we’ll replicate a CSRF attack on the same domain. Please refer to the Developers: Get Ready for New SameSite=None; Secure Cookie Settings (https://blog.chromium.org/2019/10/developers-get-ready-for-new.html) Chromium blog, which covers some of the updates that have been made to Google Chrome to prevent CSRF attacks.

Getting ready

Follow these steps:

  1. Start by creating a directory named express-csrf for this recipe and initializing the project with npm:
    $ mkdir express-csrf
    $ cd express-csrf
    $ npm init --yes
    $ npm install express express-session body-parser
  2. Create a file named server.js. This will contain our server, which is vulnerable to CSRF attacks:
    $ touch server.js
  3. In server.js, import the required modules and register the express-session middleware:
    const express = require('express');
    const bodyParser = require('body-parser');
    const session = require('express-session');
    const app = express();
    const mockUser = {
      username: 'beth',
      password: 'badpassword',
      email: '[email protected]'
    };
    app.use(
      session({
        secret: 'Node Cookbook',
        name: 'SESSIONID',
        resave: false,
        saveUninitialized: false
      })
    );
    app.use(bodyParser.urlencoded({ extended: false }));
  4. Next, in server.js, we need to define the routes for our server:
    app.get('/', (req, res) => {
      if (req.session.user) return
        res.redirect('/account');
      res.send(`
        <h1>Social Media Account - Login</h1>
        <form method="POST" action="/">
          <label> Username <input name=username> </label>
          <label> Password <input name=password
            type=password> </label>
          <input type=submit>
        </form>
      `);
    });
    app.post('/', (req, res) => {
      if (
        req.body.username === mockUser.username &&
        req.body.password === mockUser.password
      ) {
        req.session.user = req.body.username;
      }
      if (req.session.user) res.redirect('/account');
      else res.redirect('/');
    });
    app.get('/account', (req, res) => {
      if (!req.session.user) return res.redirect('/');
        res.send(`
          <h1>Social Media Account - Settings</h1>
          <p> Email: ${mockUser.email} </p>
          <form method="POST" action=/update>
            <input name=email value="${mockUser.email}">
            <input type=submit value=Update >
          </form>
        `);
    });
    app.post('/update', (req, res) => {
      if (!req.session.user) return res.sendStatus(403);
      mockUser.email = req.body.email;
      res.redirect('/');
    });
  5. Then, add the following to server.js to start the server:
    app.listen(3000, () => {
      console.log('Server listening on port 3000');
    });

Now, we’re ready to start this recipe.

How to do it…

First, we’ll create a malicious web page that can replicate a CSRF attack. After that, we’ll learn how to protect our Express.js server against these attacks.

Your steps should be formatted like so:

  1. Start the server:
    $ node server.js
    Server listening on port 3000
  2. Navigate to http://localhost:3000 in your browser and expect to see the following HTML login form. Enter beth as the username and badpassword as the password. Then, click Submit:
Figure 9.9 – Social Media Account – Login

Figure 9.9 – Social Media Account – Login

  1. Once logged in, you should be taken to the Settings page of the demo social media profile. Notice that there’s a single field to update your email. Try updating the email to something else. You should see that the update is reflected after clicking Update:
Figure 9.10 – Social Media Account – Settings

Figure 9.10 – Social Media Account – Settings

  1. Now, we’re going to create our malicious web page. Create a file named csrf-server.js. This is where we’ll build our malicious web page:
    $ touch csrf-server.js
  2. Add the following code to create the malicious web page:
    const http = require('node:http');
    const attackerEmail = '[email protected]';
    const server = http.createServer((req, res) => {
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.end(`
    <iframe name=hide style="position:absolute;left:-
      1000px"></iframe>
    <form method="post"
      action="http://localhost:3000/update" target=hide>
    <input type=hidden name=email
      value="${attackerEmail}">
    <input type=submit value="Click this to win!">
    </form>`);
    });
    server.listen(3001, () => {
      console.log('Server listening on port 3001');
    });
  3. In a second terminal window, start the csrf-server.js server:
    $ node csrf-server.js
    Server listening on port 3001

Important note

In a real CSRF attack, we’d expect the attack to come from a different domain to the vulnerable server. However, due to advances in web browser security, many CSRF attacks are prevented by the browser. For this recipe, we’ll demonstrate the attack on the same domain. Note that CSRF attacks are still possible today, particularly as many users may be using older browsers that don’t have the latest security features to protect against CSRF attacks.

  1. Navigate to http://localhost:3001 in your browser. Expect to see the following output showing a single button:
Figure 9.11 – Malicious CSRF web page showing a suspicious “Click this to win!” button

Figure 9.11 – Malicious CSRF web page showing a suspicious “Click this to win!” button

  1. Click the Click this to win! button. By clicking the button, an HTTP POST request is sent to http://localhost:3000/update, with a body containing the [email protected] email. By clicking this button, the HTTP POST request has been sent to the real website’s server, leveraging the cookie stored in the browser.
  2. Go back to the social media profile page and refresh it. We’ll see that the attacker has managed to update the email address:
Figure 9.12 – The Social Media Account – Settings page showing that the email has been updated to attacker@example.com

Figure 9.12 – The Social Media Account – Settings page showing that the email has been updated to [email protected]

  1. Now, let’s fix the server so that it isn’t susceptible to CSRF attacks. First, copy the server.js file to a file named fixed-server.js:
    $ cp server.js fixed-server.js
  2. To fix the server, we need to add some additional configuration to the express-session middleware. Change the express-session configuration to the following:
    app.use(
      session({
        secret: 'Node Cookbook',
        name: 'SESSIONID',
        resave: false,
        saveUninitialized: false,
        cookie: { sameSite: true }
      })
    );

    Note the addition of the { cookie : { sameSite : true }} configuration.

  3. Now, having stopped the original server, start fixed-server.js:
    $ node fixed-server.js
    Server listening on port 3000
  4. Return to http://localhost:3000 and log in again with the same credentials as before. Then, in a second browser tab, visit http://127.0.0.1:3001 (csrf-server.js should still be running) and click the button again. Note that you must navigate using http://127.0.0.1:3001 rather than http://localhost:3001; otherwise, the request will be considered as coming from the same domain.

    You’ll find that this time, clicking the button will not update the email on the Social Media Account - Settings page. If we open Chrome DevTools | Console, we’ll even see a 403 (Forbidden) error, confirming that our change has prevented the attack:

Figure 9.13 – The Chrome DevTools window showing 403 (Forbidden) on our CSRF request

Figure 9.13 – The Chrome DevTools window showing 403 (Forbidden) on our CSRF request

This recipe has demonstrated a simple CSRF attack and the associated risks. We mitigated the vulnerability by supplying additional configuration using the express-session middleware.

How it works…

In this recipe, we demonstrated a simple CSRF attack. The attacker crafted a malicious site to leverage a cookie from a social media website to update a user’s email to their own. This is a dangerous vulnerability as once an attacker has updated the email to their own, they can end up with control over the account.

To mitigate this vulnerability, we passed the express-session middleware the { cookie : { sameSite : true }} configuration. The SameSite attribute of the cookie header can be set to the following three values:

  • none: The cookie can be shared and sent in all contexts, including cross-origin requests
  • lax: This allows the cookie to be shared with HTTP GET requests initiated by third-party websites, but only when it results in top-level navigation
  • strict: Cookies can only be sent through a request in a first-party context – if the cookie matches the current site URL

Setting the { sameSite : true } configuration option in the express-session middleware configuration equates to setting the Set-Cookie : SameSite attribute to strict mode.

Inspecting the header of the request in this recipe would show a Set-Cookie header similar to the following:

Set-Cookie:
SESSIONID=s%3AglL_...gIvei%2BEs; Path=/; HttpOnly; SameSite=Strict

There’s more…

Some older browsers don’t support the Set-Cookie SameSite header attribute. A strategy for dealing with these cases is to generate an anti-CSRF token. These anti-CSRF tokens are stored in the user session, which means the attacker would need access to the session itself to carry out the attack.

We can use a module named csurf to help implement anti-CSRF tokens:

  1. Still in the express-csrf directory, copy fixed-server.js to a new file named csurf-server.js:
    $ cp fixed-server.js csurf-server.js
  2. Install the csurf module:
    $ npm install csurf
  3. Next, we need to import and initialize the csurf module in the csruf-server.js file. Add the following lines below the express-session import:
    const csurf = require('csurf');
    const csrf = csurf();
  4. Then, we need to alter the HTTP GET request handler so that it uses the csrf middleware. We can achieve this by supplying it as the second parameter to the get() method of the /account route handler:
    app.get('/account', csrf, (req, res) => {
      if (!req.session.user) return res.redirect('/');
      res.send(`
          <h1>Social Media Account - Settings</h1>
          <p> Email: ${mockUser.email} </p>
          <form method="POST" action=/update>
            <input type=hidden name=_csrf
              value="${req.csrfToken()}">
            <input name=email value="${mockUser.email}">
            <input type=submit value=Update >
          </form>
        `);
    });

    In the HTML template, we generate and inject csrfToken using the req.csrfToken() method of the request object. We inject the token into the HTML template as a hidden field named _csrf. The csrf middleware looks for a token with that name.

  5. We also need to update the post() method of our /update route handler so that it can use the csrf middleware:
    app.post('/update', csrf, (req, res) => {
      if (!req.session.user) return res.sendStatus(403);
      mockUser.email = req.body.email;
      res.redirect('/');
    });

    Upon an HTTP POST request, the csrf middleware will check the body of a request for the token stored in the _csrf field. The middleware then validates the supplied token with the token stored in the user’s session.

  6. Start the server:
    $ node csurf-server.js
    Server listening on port 3000
  7. Navigate to http://localhost:3000 and log in with the same username and password that we used in this recipe. Click on View Page Source on the Social Media Account - Settings page. You should see the following HTML showing the hidden _csrf field:
    <html>
    <head></head>
    <body>
    <h1>Social Media Account - Settings</h1>
          <p> Email: [email protected] </p>
          <form method="POST" action="/update">
            <input type="hidden" name="_csrf"
              value="r3AByUA1-csl3hIjrE3J4fB6nRoBT8GCr9YE">
            <input name="email" value="[email protected]">
            <input type="submit" value="Update">
          </form>
        </body>
    </html>

    You should be able to update the email as before.

The csurf middleware helps mitigate the risk of CSRF attacks in older browsers that don’t support the Set-Cookie:SameSite attribute. However, our servers could still be vulnerable to more complex CSRF attacks, even when using the csurf middleware. The attacker could use XSS to obtain the CSRF token, and then craft a CSRF attack using the _csrf token. However, this is best-effort mitigation in the absence of support for the Set-Cookie:SameSite attribute.

Slowing an attacker down by making the attack they have to create more complex is an effective way of reducing risk. Many attackers will try to exploit many websites at a time – if they experience a website that takes significantly longer to exploit, in the interest of time, they will often just move on to another website.

See also

  • The Authentication with Fastify recipe in this chapter
  • The Hardening headers with Helmet recipe in this chapter
  • The Anticipating malicious input recipe in this chapter
  • The Preventing JSON pollution recipe in this chapter
  • The Guarding against cross-site scripting recipe in this chapter
  • The Diagnosing issues with Chrome DevTools recipe in Chapter 12
lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image