Node.js Security Unleashed: Your Ultimate Defense Guide (1/7)

Part 1 - How to prevent Cross-Site Request Forgery (CSRF)

Node.js Security Unleashed: Your Ultimate Defense Guide (1/7)

A Preface

Nowadays, everyone acknowledges that there exists a plethora of possible attacks and exploits, each capable of employing diverse approaches to compromise a targeted system. Thankfully, a significant portion of them derives from the well-established concepts and relies on widely recognized patterns.

Therefore, such threats, though each having its own peculiar characteristics, can still be classified based on their shared traits. By identifying these commonalities, the development community managed to elaborate a comprehensive suite of effective countermeasures against them.

Throughout this article series, we will highlight seven signature attack patterns that we should be concerned about when safeguarding our Node.js applications.

!!! Disclaimer !!!

The information provided in this article is intended for educational purposes only. Readers are encouraged to consult with qualified cybersecurity experts and adhere to their organization’s security procedures when addressing the mentioned attacks. Neither the author nor the hosting platform is responsible for any actions taken by individuals or organizations based on the information contained herein.

Please be prudent, and use the provided information solely with good intentions.


Series Agenda.


How to prevent Cross-Site Request Forgery (CSRF) in Node.js

Introduction

Greetings, dear readers!

Welcome to the first installment of our article series dedicated to the theory and practice of defending against prevalent attack patterns in software development.
In this very segment, we will explore the ins and outs of the Cross-Site Request Forgery (CSRF) threats, and come to know why we, developers, should be concerned about it.

By the end of this paper, you will be well-equipped with both the general knowledge about CSRF attacks and skills needed to diminish their impact on your applications.

So without further ado, let’s begin our journey…


Understanding CSRF

“Cross-Site Request Forgery (CSRF, or Session Riding) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated. With a little help of social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker’s choosing. If the victim is a normal user, a successful CSRF attack can force the user to perform state-changing requests like transferring funds, changing their email address, and so forth. If the victim is an administrative account, CSRF can compromise the entire web application”.

Excepted from: https://owasp.org/www-community/attacks/csrf

In simple terms, during a CSRF attack, a bad actor attempts to perform some activity within a targeted application on behalf of an already logged-in user. Most often this involves creating a malicious request concealed within seemingly benign links or forms, and inducing the user to trigger it. The credentials required for authorizing such action are taken from the victim’s session, typically stored in associated client cookies.

For example, an attacker can leverage an HTML form submission event to redirect the user’s browser to some undesirable URL. In a typical banking application scenario, this URL can represent an endpoint for transferring money between two accounts. Such action is, of course, protected by authentication; however, the issued request already includes the necessary session credentials, enabling a successful transaction.

The impact of a successful CSRF attack may vary but often results in severe consequences. For regular users, it can lead to unauthorized actions, such as changing their email addresses or alterations to personal, account-specific settings. If the victim possesses the administrative or high-level privileges, such an attack could disrupt the application functionality, leading to substantial financial and reputational losses.

Post Scriptum

Throughout recent years, you might have heard that, with the introduction of the newest security mechanisms like Same-Origin policy, CORS and “SameSite” cookies, the threats posed by CSRF attacks have been neutralized, and developers shouldn’t actually care for defending against them.

But even in case you consider implementing proper countermeasures, you’ll soon find that many tutorials give incomplete explanations and use outdated code samples. Hence, people often get confused about the nature of CSRF attacks and situations where they can and cannot cause harm.

As for me, I back the unpopular opinion that the CSRF attack is still very much “alive”, and remains popular among attackers.

And, the aforementioned security features, though indeed effective, actually are quite complicated and require developers to possess deep knowledge about their usage. Otherwise, they can easily be misconfigured and so fail to provide the intended protection level.

Summing up, I think that it is important to remain alert and continue to defend against CSRF attacks to ensure the ongoing security of our web applications.


Real-World Scenario

To illustrate a common CSRF attack, imagine a widely used online banking application that allows clients to view their account details, transfer funds, and pay bills. The application includes a messaging feature that enables users to communicate with the customer support team for inquiries and assistance.

However, the application fails to properly validate or sanitize user inputs before displaying them to the customer support representatives who access these messages. A malicious attacker discovers this vulnerability and decides to exploit it.

He crafts a seemingly innocent-looking message that embeds malicious JavaScript code and, with the help of social engineering, sends it to the support team: “Hey, I’m having trouble logging in to my account after the latest update. I’ve ticketed this issue on your support page, and have extensively described the encountered problem at: “my-domain.com/issues/3456?action=stealCrede..”.
Wouldn’t you please help me resolve this? Thanks a lot!”
.

When a support representative reviews the message and clicks the embedded link, the CSRF payload executes, stealing his session token and sending it to an attacker-controlled server. With these credentials, the attacker can impersonate himself as a legitimate employee, gaining unauthorized access to sensitive customer data and performing unauthorized transactions.


Defense Strategies

To defend against this threat, we need to develop a way to distinguish legitimate client requests from malicious ones within two distinct contexts.
The first involves a harmful action execution within our website (Same Origin), and the second, a redirection to the attacker’s own domain (Cross Origin).

1. Same Origin Context.

So, against this attack vector, we can employ one of two preferred approaches: the “Synchronizer Token” and the “Double Submit Cookie”.

  • a. Synchronizer Token pattern.
    During the first contact with a server (e.g. on the Home page load), each user is assigned a special token that gets included in his session.
    NB: Because every token should be associated with a respective user and persisted within a session, the Synchronizer Token is considered a stateful approach.

    After that, every subsequent client-side action should be supplied with a unique string called Synchronized Token (aka “CSRF Token”), which serves to prove the request's legitimacy. Typically, this string gets delivered to the server via a hidden form data value or a custom HTTP header. Please note that embedding the CSRF Token in a header is more secure, as any browser request with non-standardized headers is automatically subject to the same-origin policy.

    In most cases, the client does not generate a CSRF token itself; instead, he receives it as a part of a response payload from some server-side endpoint.
    NB: Avoid sending the token through cookies or URL parameters, as these methods are easily intercepted and hence defeat the Synchronizer Token pattern purpose.

    Upon a client request, the server will extract the CSRF token and compare it with the associated token from the user’s session. If such a comparison fails, the operation will be promptly denied to prevent a potential exploit.
    To further enhance the security, we would advise not to rely on a single token throughout the entire user session. Instead, we recommend re-generating a new pair of tokens for each form submission or similar sensitive user operation being taken.

    Illustrative flow of a request protected with Synchronizer Token

    Illustrative flow of a request protected with Synchronizer Token

/*
  * The superficial implementation flow
  * of the `Synchronizer Token` pattern
*/

// ------ Server Side ------
const app = Express();

function generateCsrfToken(user: { id: string }): string {
  try {
    // Retrieving a token from a generator function
    const token = generateRandomToken();
    // Persist the token in the current user's session
    sessionStore[user.id].csrfToken = token;
    // return the generated token
    return token;
  } catch {
    return '';
  }
};

// Include CSRF token in the response
app.get('/api/csrf-token', (req, res) => {
  const csrfToken = generateCsrfToken(req.user);
  // sending the token back to the requesting client
  res.json({ csrfToken });
});

app.post('/api/protected-endpoint', ({ user, headers }, res) => {
  // retrieving the token from the client request
  const csrfHeaderValue = req.headers['X-Csrf-Token'] ?? '';
  // comparing existing token from a user's session with the retrieved one
  const isLegitimateReq = verifyToken(req.user.id, csrfHeaderValue);

  if (!isLegitimateReq) {
    return res.sendStatus(403);
  }
  res.status(201).json(/* sample response... */);
});


// ------ Client Side ------

// A function to fetch the token from a remote server
const csrfToken = await getCsrfTokenFromServer();

await fetch('/api/protected-endpoint', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    // embedding the token into a custom `X-Csrf-Token` header
    'X-Csrf-Token': csrfToken,
  },
  // Request payload...
});
  • b. Double Submit Cookie pattern.
    A Double Submit Cookie is a simpler alternative to the “Synchronizer Token” pattern that uses a special HTTP-only cookie (called “checksum”) for storing an “anticipated” token. If a client-provided CSRF token matches the one stored in checksum cookie, a server allows the request to proceed. And, if tokens mismatch, a server rejects it.

  • NB: Because the “anticipated” token is persisted within a checksum cookie and not within a user’s session, such implementation is considered stateless.

  • This cookie can also be “signed” (i.e. encrypted), ensuring that an attacker cannot inject any existing or manually crafted tokens into the active user’s session.

  • Unlike in the first (Synchronizer Token) pattern, the most secure way of delivering a CSRF token to the client is via another HttpOnly cookie.
    It is however possible to send this token through the custom header, and in some specific cases, such an approach may be considered reasonable.

    Illustrative flow of a request protected with Double Submit Cookie

    Illustrative flow of a request protected with Double Submit Cookie

— — — — —— — — — ——

2. Cross Origin Context.

Here, our main concern should be the security of the session cookies. To prevent their exploitation in a CSRF attack, we may employ the following security mechanisms:

  • a. "SameSite" Cookie Flag.

    The most important is the SameSite cookie attribute that controls whether a browser should send the associated cookies to a remote origin. It may take three different values:

    • SameSite: None (default value)  —  Cookies are sent alongside any request contexts, which is the usual behavior of cookies.

    • SameSite: Lax  —  Cookies are sent when a user navigates to a web page’s URL from an external site (e.g. following a link), and are withheld during requests for the embedded resources (like images or scripts).

      NB: This option appears most suitable for the majority of cases.

    • SameSite: Strict  —  Cookies are sent only alongside requests originating from the same domain, and are withheld during requests by third-party services.

  • b. Session Rotation.
    Another mechanism revolves around the configuration of the session cookie, precisely its expiry date (controlled via “Max-Age” and "Expires" attributes).

    It is highly recommended to leverage the non-persistent cookies for managing sessions, but if such an option isn't available, fall back to these two attributes.
    The default session lifetime should not be longer than a few hours or days, depending on the non-functional requirements of a specific application.
    And, for the sake of user convenience, consider implementing a “Remember Me” functionality that extends the session’s validity beyond its typical lifetime.


CSRF Token Implementations: Comparative Analysis

After acquainting both the Synchronizer Token and the Double Submit Cookie patterns, let us compare them in terms of various aspects:

1. Statefulness.

Synchronizer Token:

  • Statefulness: Requires server-side storage of the generated CSRF tokens.

  • Session Dependency: Tied to the user's session as the token is stored on the server.

Double Submit Cookie:

  • Statelessness: Stateless on the server; the CSRF token is stored on the client side as a cookie.

  • No Bound Storage: The server doesn't need to store the CSRF token; it relies on the client to include it in requests.

2. Implementation Complexity.

Synchronizer Token:

  • Complexity: Perceived as more complex due to server-side storage/verification.

  • Server-side Overhead: Often populated using server-side middleware providers.

Double Submit Cookie:

  • Simplicity: Generally considered simpler as it doesn't require server-side storage.

  • Client-side Handling: Involves managing the CSRF token on the client side.

3. Ease of Integration.

Synchronizer Token:

  • Integration in Forms: Requires adding a hidden input field or extra header.

  • Server-side: Relatively straightforward implementation flow.

Double Submit Cookie:

  • Integration in Requests: Involves including the CSRF token in request headers or as a cookie.

  • Server-side: Relatively similar implementation flow.

4. Assumed Security.

Synchronizer Token:

  • Protection Mechanism: Provides robust protection against CSRF attacks.

  • Token Renewal: Tokens can be renewed to mitigate certain attack vectors.

Double Submit Cookie:

  • Effective Protection: Effective when implemented correctly, but potentially vulnerable if the cookie is accessible by malicious scripts (XSS vulnerability).

  • Checksum Validation: The checksum mechanism adds an extra layer of security.

5. Cross-Domain Considerations:

Synchronizer Token:

  • Cross-Domain Requests: Requires careful handling and additional considerations for cross-domain requests (e.g., CORS headers).

Double Submit Cookie:

  • Cross-Domain Requests: Cookies are subject to the same-origin policy; additional configurations are needed for cross-domain requests (e.g. an include option with Fetch API).

6. Usage Scenarios:

Synchronizer Token:

  • Highly Stateful Applications: Well-suited for applications with complex state requirements and session management.

  • Standard Web Applications: Commonly used in traditional server-rendered applications.

Double Submit Cookie:

  • Stateless or Stateless-aware Applications: Best-suited for applications that do not heavily depend on stateful workloads.

  • Single-Page Applications (SPAs): Can be more convenient in SPAs where maintaining any kind of server-side state is less common.

Overall, the choice between these two well-established approaches depends on the specific requirements and characteristics of the application.
The Synchronizer Token offers robust protection, especially in highly stateful applications, while the Double Submit Cookie provides a more lightweight solution suitable for stateless (mainly SPA) environments.
Consideration of factors such as application architecture, security requirements, and ease of integration will help you select the most appropriate CSRF protection mechanism.


Cross-Origin Resource Sharing (CORS)

As we have mentioned earlier, there exists another prominent defensive mechanism in the Cross-Origin context, called Cross-Origin Resource Sharing (CORS for short). Essentially, it is a stateless, header-based policy that allows a server to indicate which third-party origins are authorized to access its underlying resources. This also includes enforcing the specific constraints that a client must adhere to when connecting with the server.

In simple terms, CORS defines which domains are allowed to make requests to your server-side application. It enables you to control and restrict which origins can access your server’s resources, thereby hindering CSRF attacks that might originate from unauthorized domains.

The CORS mechanism works within the boundaries of the Same Origin Policy.
It provides a way to relax the SOP's limitation, and selectively make controlled network requests to other origins.

Preflight Requests

The CORS operates by introducing the concept of “preflight” (or “premature”) requests. Before a browser makes a potentially risky Cross-Origin request (e.g. one with non-standard HTTP headers or insecure methods), it first sends a preflight request to check if the intended request is permitted. This usually includes details describing the actual request, such as the headers and method it will use, and issued via a special “OPTIONS” HTTP method and the Access-Control-Request-* header family.

How the CORS-imbued request is made:

  1. The browser issues a “preflight” request via an OPTIONS method, telling the server what the actual request structure would be (via Access-Control-Request-* headers).

  2. The server, if configured to handle Cross-Origin requests, responds to the browser with the allowed request structure (via Access-Control-Allow-* headers).

  3. The browser receives the server’s response, compares the expected with the actual, and finally decides whether an actual request is permitted.

Flowchart showing a decision process of a Simple / Preflighted request when making a Cross-Origin call.

Flowchart showing a CORS decision when making a Cross-Origin request call.

— — — — —— — — — ——

Family of Access-Control-Request-* headers

  • Origin  —  denotes an external origin that has issued a request to our server.

  • Access-Control-Request-Method  —  describes to the server which HTTP method will be used when the actual request is made.

  • Access-Control-Request-Headers  —  describes to the server which HTTP headers will be used when the actual request is made.

  • Access-Control-Max-Age  —  allows to cache the CORS responses for a specified amount of time.

Family of Access-Control-Allow-* headers

To properly configure CORS, you should set specific HTTP headers in your server-side application’s responses. These will ensure that only trusted origins are allowed to interact with your application’s resources. These headers include:

  • Access-Control-Allow-Origin — specifies the origins (domains) that are allowed to access the server resources.

  • Access-Control-Allow-Methods — defines which HTTP methods are permitted for Cross-Origin client requests.

  • Access-Control-Allow-Headers — denotes a list of allowed headers that may be used in the actual request.

  • Access-Control-Allow-Credentials — specifies whether the associated client credentials (sessions and cookies) should be included in server responses.

To enable CORS in Node.js, you could manually handle OPTIONS HTTP requests and set response headers for every outgoing response application-wide. However, it is advisable to utilize an existing cors package, which handles these concerns automatically. We will discuss a sample CORS policy configuration in the next, practical section of the article.

— — — — —— — —

Explicit Authentication

You can further protect any sensitive action by applying another defensive layer on top of the existing authentication mechanism. This assumes a direct asking of the user to confirm his intention to perform a potentially risky operation through other means. Such include supplying an account password, refreshing the existing session, sending a One-Time Password (OTP) or using a standard CAPTCHA screen. With this feature in place, even if an attacker gets access to a user’s account, he can’t authorize himself to perform any sensitive actions as they will be promptly denied.


Moving towards Practice

Historically, the csurf package (and an underlying csrf module) was traditionally considered a de-facto standard for implementing CRSF protection. However, it did not receive any updates for quite a while, and by the time of writing this article, is regarded deprecated.

Instead, a new family of packages has been developed to address this issue, by the names of csrf-sync and csrf-csrf. The first adopts a "Synchronizer Token" pattern, while the second implements the "Double Submit Cookie" approach.

Due to its simplicity and stateless nature, we decided to focus our attention on the second method, and implement it on both the client and server sides.
Here’s how it works:

“Double Submit Cookie” pattern Implementation overview:

  1. On the initial web page load, a server responds with a CSRF cookie (with a token inside) as well as a checksum cookie. To increase the interception resiliency, both cookies should be signed. You can also place a CSRF token within a request header instead of a cookie, or make a session cookie readable and derive the actual token value from it. However, these approaches are not as secure.

  2. For any state-mutating action (e.g. POST, PUT, PATCH and DELETE) a client should send both of these cookies (or a cookie + header) back to the server.
    If you’re using Angular as your client-side framework, it handles this step automatically through the built-in HttpClientXsrfModule. It extracts the token value from a non-signed, JavaScript-readable cookie and places it into the X-Xsrf-Token request header.
    Unfortunately, its popular competitors (like React, Vue.js etc.) do not provide similar solutions, so we will need to invent our own.

  3. When the server receives a client request, it compares the received token and the session cookie, rejecting if there is a mismatch.

We will be using the aforementioned csrf-csrf package for our code samples.

— — — — —— — —

Let us start with the implementation of the Angular-like approach, which involves a Checksum cookie and CSRF header combination:

------------- Server Side > Attaching middlewares -------------

1. Initialize bodyparser.json() middleware to parse “application/json” request bodies, as well as cookieParser() middleware to parse the request's cookies. The latter MUST be applied before the csrf-csrf middleware, as it depends on cookies for correct functioning:

// index.ts
import Express from 'express';
import { json } from 'body-parser';
import cookieParser from 'cookie-parser';

const app = Express();

// parsing `application/json` data
app.use(json({ limit: '50kb' }));

// the cookie-parser middleware should be initialized first
// this allows `csrf-csrf` to work with server-side cookies
app.use(cookieParser());

...

2. Initialize the cors() middleware, which will attach the necessary Access-Control-Allow-* headers to the response.
Allow credentials (cookies) to be passed through CORS requests.
Enable support of preflight requests by configuring the “OPTIONS” response handler.
Add our custom CSRF header (“X-Csrf-Token”) to the list of allowed request headers:

// cors.ts
import cors from 'cors';

const allowedOrigin = process.env.CLIENT_APP_URI;

export const corsPolicy = cors({
  origin: allowedOrigin,
  methods: 'GET,PUT,PATCH,POST,DELETE',
 // `X-Csrf-Token` must be provided, otherwise the request would be blocked
  allowedHeaders: ['Content-Type', 'Accept', 'Origin', 'X-Csrf-Token'],
  // credentials must be allowed, othewise cookies won't be sent to the client
  credentials: true,
});

/* ----- */

// index.ts
import { corsPolicy } from './cors';

app.options('*', corsPolicy);
app.use(corsPolicy);

...

3. Configure the doubleCsrf() middleware that allows to generate CSRF tokens as well as compare them with the value of associated checksum cookie. Provide a secret key, which will be used for HMAC-based hashing of the CSRF token’s value.
Do not forget to explicitly set the SameSite attribute of a checksum cookie to either “lax” or “strict”.
To further boost security, a cookie’s name should be set to some random noise value, which helps prevent Server Fingerprinting:

// csrf.ts
import { doubleCsrf, type DoubleCsrfCookieOptions } from 'csrf-csrf';
import { randomBytes } from 'node:crypto';

const csrfSecret = randomBytes(20).toString('hex');

const cookieOptions: DoubleCsrfCookieOptions = {
  maxAge: 24 * 60 * 60 * 1000, // 24 hours
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'lax',
  path: '/api',
};

export const { doubleCsrfProtection, generateToken } = doubleCsrf({
  // The entropy (in bits) of the generated tokens
  size: 4 * 8,
  // A function that optionally takes the request and returns a secret
  getSecret: () => csrfSecret,
  cookieName: 'some-random-cookie',
  cookieOptions,
});

------------- Server Side > Configuring routes -------------

1. Define a “/csrf-token” endpoint that will initially issue the checksum cookie, as well as a corresponding CSRF token to the requesting client. Remember, that the token MUST be delivered to the client side via the response body and not through specific cookies or URL parameters:

// index.ts
import { generateToken, doubleCsrfProtection } from './csrf';

...

app.get('/api/csrf-token', (req, res) => {
  const generatedToken = generateToken(req, res);
  res.set('Content-Type', 'text/plain');

  if (!generatedToken) {
    return res.status(500).send('Cannot generate the requested content.');
  }

  return res.send(generatedToken);
});

2. Finally, apply the pre-configured doubleCsrfProtection() middleware to any state-mutating actions either globally or per-route basis:

...

app.post('/api/protected', doubleCsrfProtection, ({ body }, res) => {
  // other handling logic...

  console.log(`Json body: ${body}`);
  res.end('Request was successful.');
});

------------- Client Side -------------

1. On the initial page load (+ before any sensitive client action), re-request the checksum cookie and CSRF token from the “/csrf-token” endpoint. Do not forget to include the credentials (cookies) within a server response; otherwise, the cookie won’t be sent to the client and the intended action will fail:

const BASE_API_URL = 'http://localhost:8080';

export async function getCsrfToken(): Promise<string> {
  // fetching the Anti-CSRF token from the trusted source - a server
  const response = await fetch(`${BASE_API_URL}/api/csrf-token`, {
    mode: 'cors',
    signal: AbortSignal.timeout(2000),
    // necessary for sending a generated session cookie to the client
    credentials: 'include',
  });

  if (!response.ok) {
    throw new Error('Token was not received.');
  }

  return await response.text();
}

...

2. Append the CSRF header (“X-Csrf-Token”) to each action request, which will be sent alongside a checksum cookie and be verified by the server. As with the previous step, do not forget to include the credentials (cookies) within a request, otherwise they won’t be sent to the server:

...

declare type RequestOptions = {
 url: `/${string}`;
  payload: Record<string, string | number>;
 method?: 'POST' | 'PUT' | 'PATCH' | 'DELETE';
};

const unsafeMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];

export async function wrapCsrfRequest({
  url,
  payload,
  method = 'POST',
}: RequestOptions) {
  const csrfToken = await getCsrfToken();
  const isUnsafeMethod = unsafeMethods.includes(method);

  const headers: Record<string, string> = {
    'Content-Type': 'application/json',
  };

  if (isUnsafeMethod) {
    // sending the token for server-side comparison
    //  within the `X-Csrf-Token` header
    headers['X-Csrf-Token'] = csrfToken;
  }

  return await fetch(`${BASE_API_URL}${url}`, {
    headers,
    method,
    mode: 'cors',
    credentials: isUnsafeMethod ? 'include' : 'omit',
    body: JSON.stringify(payload),
  });
}

Now, whenever you need to perform a potentially risky and/or sensitive action, wrap your client request with the wrapCsrfRequest() function:

const res = await wrapCsrfRequest({
 method: 'POST',
 url: '/api/protected',
 payload: { message: 'Hello, World!' },
);

console.log(await res.text());

Now, let’s switch to a more secure version, utilizing the signed cookies for storing both tokens. For that, we need to tweak some configurations within our API:

------------- Server Side -------------

1. Make both cookies “signed”, which will provide an additional security layer by encrypting their contents:

// csrf.ts

const cookieOptions: DoubleCsrfCookieOptions = {
  maxAge: 24 * 60 * 60 * 1000, // 24 hours
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'lax',
  path: '/api',

  // making a cookie more secure by encrypting its contents
  signed: true,
};

...

2. Within the doubleCsrf() middleware configuration, specify that the CSRF token should be retrieved from an associated signed cookie upon the client’s request:

// csrf.ts
import type { Request } from 'express';

const csrfCookieName = process.env.CSRF_TOKEN_COOKIE ?? 'X-Csrf-Token';

function tokenExtractor(req: Request) {
  return req.signedCookies[csrfCookieName];
}


export const { doubleCsrfProtection, generateToken } = doubleCsrf({
  // ...
  getTokenFromRequest: tokenExtractor,
});

3. Now, within the “/csrf-token” endpoint, instead of sending a token in the response body, set its value as an “HttpOnly” CSRF cookie.
To add even more security, we can overwrite the values of both checksum and CSRF cookies before each sensitive client action. This is achieved by providing true as a third parameter to the generateToken() function.

...

app.get('/api/csrf-token', (req, res) => {
 // if a checksum cookie already exists, overwrite it on each subsequent request
  const generatedToken = generateToken(req, res, true);

  res.cookie(csrfTokenCookie, generatedToken, {
    ...cookieOptions,
    httpOnly: true,
  });
  res.end('Request was successful.');
});

------------- Client Side -------------

We no longer need to maintain a local token state (as it will always be persisted in a cookie), as well as sending a token through a custom HTTP header.
So, the resulting code becomes a little bit simpler:

const BASE_API_URL = 'http://localhost:8080';

async function getCsrfToken(): Promise<void> {
  // fetching the Anti-CSRF token from the trusted source - a server
  const response = await fetch(`${BASE_API_URL}/api/csrf-token`, {
    mode: 'cors',
    signal: AbortSignal.timeout(2000),
    // necessary for sending a generated session cookie to the client
    credentials: 'include',
  });

  if (!response.ok) {
    throw new Error('Token was not received.');
  }
}

async function wrapCsrfRequest({ url, payload, method = 'POST' }: RequestOptions) {
  // creating a CSRF token cookie from on the initial request
  await getCsrfToken();

  return await fetch(`${BASE_API_URL}${url}`, {
    // No longer sending `X-Csrf-Token` header
    headers: { 'Content-Type': 'application/json' },
    // ...
  });
}

// the rest remains the same...

Conclusion

In this part of our series, we have taken a deep dive into the world of CSRF threats, which has provided us with a solid understanding of this attack pattern, as well as essential knowledge of mitigation mechanisms that constrain such threats.
With all this, you are now well-equipped to secure any server-side applications and get ready to continue exploring the intricacies of CSRF on your own.

Additional Resources

For further exploration of CSRF vulnerabilities and security best practices, please refer to these external resources:

In the next article…

Very soon, we will continue to enhance our comprehension of the prevalent attack patterns within the domain of web security.
In particular, we shall delve into fundamental principles of the Server-Side Request Forgery (SSRF), a relatively new addition to the common threats list.

If that sounds interesting, stay tuned for further updates!
Until then, stay secure and happy coding!