Skip to main content

How to protect Node.js apps from CSRF attacks

著者:
Victor Ikechukwu
Victor Ikechukwu
feature-csrf-node-js

2023年10月17日

0 分で読めます

A cross-site request forgery attack (CSRF) attack is a security vulnerability capitalizing on trust between a web browser and a legitimate website. Crafty attackers manipulate browsers into executing malicious actions on websites where users authenticate themselves and log in. Often, these attacks start when users click a link attached to a deceptive email or land on a compromised website, unaware of the logic executing in the background.

The impact of successful CSRF attacks can range from financial losses and reputational damage for individuals and businesses to compromised user accounts, unauthorized transactions, and even legal liabilities. As CSRF attacks continue to evolve and become more sophisticated, web developers and organizations must implement robust countermeasures to safeguard the integrity of their web applications.

This article explores how CSRF attacks work in Node.js applications and how to protect ourselves against them. We’ll look at real-world examples with practical steps and code snippets, methods to test the protections, and best practices to secure Node.js applications against CSRF attacks.

We also offer a free, hands-on CSRF lesson at Snyk Learn if you want to jump straight into a lesson.

Understanding CSRF attacks

CSRF attacks exploit the trust web applications place in authenticated user sessions. By tricking users into unintended actions, attackers can manipulate or disclose sensitive data without the user’s knowledge. Before we learn how to protect Node.js applications from such threats, let's review the underlying mechanics of these attacks and their potential consequences.

When users log into a site, they remain logged in for a certain period — varying from days to months — before needing to re-authenticate themselves. This period in which they're authenticated is called a session. During an active session, the server creates a random unique identifier or token — a session ID — and associates it with that session. The server then returns the session ID to the user, stored in the browser cookies (data attached to the user’s browser during a session).

Now, every time the user makes a request that modifies the website's state (for example, submitting a form), the session ID accompanies the request. The server compares the ID with the token on the server. If the values match, it authorizes the request, and the application executes the action. Otherwise, it revokes access.

In a typical CSRF attack, attackers exploit the trust in authenticated user sessions by crafting malicious requests that impersonate an action on behalf of a logged-in victim. Malicious actors can successfully execute CSRF attacks using social engineering strategies, such as embedding harmful links in emails or websites frequented by targeted users. Unsecured web applications may inadvertently authorize these requests as legitimate user actions by attaching valid credentials.

The stakes are high for CSRF attacks. For example, Facebook's 2018 security breach affected over 50 million accounts worldwide. The cause? Insufficiently protected access tokens, which are susceptible to cross-origin misuse in authenticating client-side requests. This lack of adequate security compromised sensitive user data, resulting in financial loss, reputational damage, and legal repercussions. Breaches in security can cause a loss of customer trust, undermining the reputation of an organization and its services.

The impact of successful CSRF attacks varies depending on the target application and the capabilities it exposes to the user. Potential implications of a successful CSRF attack include:

  • Data manipulation — CSRF attacks can let attackers manipulate sensitive data within a web application. This manipulation could involve modifying a user’s profile settings, changing account preferences and settings, and tampering with data they can modify within their access levels.

  • Unauthorized actions — An attacker can forge requests to execute actions the authenticated user didn't authorize. Examples include submitting forms, making financial transactions, and deleting content.

  • Information disclosure — Attackers can exploit vulnerabilities, tricking users into revealing confidential data such as private messages and financial details. They can also trick the user into revealing proprietary data stored within the application.

  • Account takeover — By tricking users into performing actions that modify their account credentials, such as changing their login details, attackers can gain unauthorized access and control over the compromised accounts. This access allows them to impersonate the legitimate user, reach restricted resources, and potentially launch further attacks within the application or on other users.

CSRF protection strategies

Primary techniques to safeguard Node.js applications from CSRF attacks include the following:

Use the synchronizer token pattern (STP)

The synchronizer token pattern requires generating a unique token for each user session. It's embedded in form submissions or via an AJAX request as a custom header value or part of a JSON payload. The server validates this token upon receiving requests.

The STP generates a unique random token for each user session — a CSRF token. As part of the response payload, such as an HTML or JSON response, the server sends the CSRF token to the user. The application includes the token in the request headers or as a custom POST parameter for each subsequent request. Then, the server verifies the received token exists and matches the user session token. If so, a legitimate user with a valid session made the request.

STP is simple to implement and effective against common attack vectors. However, it may require additional server-side state management. When using cookies to store CSRF tokens with STP, prefix them with __Host- to enhance cookie security and make them inaccessible on domains other than the domain you set them on.

Implement SameSite cookies

The SameSite cookies strategy involves setting a SameSite attribute on session cookies so the application only sends them with requests originating from the same domain as target websites. This method prevents requests from including cross-origin cookies. The SameSite attribute accepts two possible values: strict and lax.

When set to strict, the browser only sends session cookies in first-party contexts — it won't send them when a user navigates to a different website. This method effectively prevents CSRF attacks from malicious third-party sites.

When set to lax, the browser sends the cookie with requests originating from the same website (domain) as the one that set the cookie and for top-level GET requests.

Although modern browsers automatically enforce SameSite cookie attributes, older browsers and non-web clients, like mobile apps, offer limited support. This limitation significantly impacts the effectiveness of SameSite cookies as a defense mechanism against CSRF attacks. Therefore, developers must consider alternative CSRF protection strategies and ensure comprehensive protection across many client environments.

Use the Double Submit Cookie pattern

The Double Submit Cookie pattern issues an additional cookie containing a unique token alongside standard session identifiers. This cookie attaches to client-side request headers or form data during submissions. The Double Submit Cookie pattern reduces state-management burden without impacting overall performance significantly. Consequently, it's ideal for stateless applications featuring diverse content distribution networks or microservices architectures.

However, this method doesn't protect against advanced attacks targeting browser-based storage mechanisms that hold secondary tokens. One such attack vector is cross-site scripting (XSS). Applications vulnerable to XSS attacks can let the attacker retrieve the unique token from a cookie and use it in subsequent malicious requests.

Additionally, the Double Submit Cookie pattern is vulnerable to man-in-the-middle (MITM) attacks when users don't set proper security measures. The attacker can intercept the original CSRF token from the client's initial request and use it to create malicious requests.

You can use signed double submit cookies to make the Double Submit Cookie pattern approach more robust. This strategy uses a secret key exclusively known to the server, guaranteeing an attacker can’t generate and insert their own CSRF token.

Additional strengthening measures include enforcing the HTTP Strict-Transport-Security (HSTS) response header and using cookie prefixes, such as __Host-. However, at the time of this article, 25% of browsers don't support cookie prefixes.

Implementing CSRF protection in a Node.js app

Now that we've examined strategies for securing our applications against CSRF attacks, let's review how to implement them. To follow along, ensure you have:

We’ll create a simple Node.js app that helps users transfer funds to other users. This application will use the Express web framework.

First, create a folder for the project and name it nodejs-csrf-strategies. Open this folder in a terminal and run the command npm init -y to initialize a Node.js project. Next, execute the command npm i express to install the Express web framework.

Create a file named index.js and paste the code below.

// import dependencies
const express = require('express');

// create express app
const app = express();

// enable parsing of request body
app.use(express.urlencoded({ extended: false }));

// home route to render the form
app.get('/', (req, res) => {
 res.send(`
   <html>
     <body>
       <h1>CSRF Demo App</h1>
       <form method="POST" action="/transfer">
         <input type="text" name="amount" placeholder="Amount" /><br />
         <input type="submit" value="Transfer" />
       </form>
     </body>
   </html>
 `);
});

// process the transfer
app.post('/transfer', function (req, res) {
   // Perform the transfer operation
   // (in a real application, you would validate and process the data here)
   res.send('Successfully transferred the set amount')
})

// Start the server
app.listen(3000, () => {
   console.log('Server listening on port 3000');
});

In this sample application, a / route renders the HTML form to capture the number of funds it will transfer. Then, the /transfer route returns a message to indicate the amount entered in the form that it has successfully transferred. In a real-world application, we would handle the transfer logic here. Note that the code used in this application is vulnerable to CSRF attacks — do not use it in production.

After installing the Snyk CLI on your computer, opt into Snyk Code, and scan the sample project for vulnerabilities using Snyk Code. The Snyk CLI enables you to integrate Snyk Code functionality into your development workflow and run Snyk Code tests locally to check your application code for security vulnerabilities.

To do this, open the project folder in the terminal. Execute the command snyk code test. You should get output like the following screenshot, with details about cleartext transmission of sensitive information, use of password hash with insufficient computational effort, information exposure, CSRF, regular expression denial of service (ReDOS), and XSS.

blog-csrf-cli

This result means the application is vulnerable to CSRF attacks and others listed.

Hover over the line that initializes the Express app — const app = express() — to review Snyk's detailed information about CSRF attacks and best practices to prevent them, like in the following screenshot. This real-time vulnerability detection feature is made possible by the Snyk VS Code extension, helping you discover vulnerabilities as you write code.

blog-csrf-ide

Now that Snyk Code has identified that our Node.js app is vulnerable to CSRF attacks, let’s explore how the CSRF protections we mentioned will help.

Protecting our app using STP

Let’s implement the STP in the sample Node.js app. First, install the csurf middleware via the command `npm install csurf`. This middleware requires you to initialize a session middleware or a cookie parser. In this example, we’ll use the cookie parser.

Install a cookie parser by running the command npm install cookie-parser.

Next, import the middleware and cookie parser at the top of your JavaScript file.

const cookieParser = require('cookie-parser')
const csrf = require('csurf')

Enable cookie parsing and set up route middleware by adding the following code just before the GET route.

app.use(cookieParser())

const csrfProtection = csrf({ cookie: true })

We must parse cookies because the cookie option setting is true in csrfProtection.

Now, include the generated CSRF token as a hidden input field when serving the HTML form. This action renders the CSRF token in an HTML form for submission. Replace the GET route code with the following code.

app.get('/', csrfProtection, function (req, res) {
   // retrieves the CSRF token
   const csrfToken = req.csrfToken()

   // Render the form with the csrf token as a hidden field
   res.send(`
       <html>
       <head>
           <title>CSRF Demo</title>
       </head>
       <body>
           <h1>CSRF Demo</h1>
           <form action="/transfer" method="POST">
           <input type="text" name="amount" placeholder="Amount" required>
           <input type="submit" value="Transfer">
           <input type="hidden" name="_csrf" value="${csrfToken}">
           </form>
       </body>
       </html>
   `);
})

Modify the POST route with the code below to apply the csrfProtection middleware.

app.post('/transfer', csrfProtection, function (req, res) {
   // Perform the transfer operation
   // (in a real application, you would validate and process the data here)
   res.send('Successfully transferred the set amount')
})

This middleware verifies the incoming POST requests by comparing the _csrf field value the application submitted in the request body with the CSRF token stored in the user's session. The latter is associated with their cookies.

Protecting our app using SameSite cookies

Let's implement SameSite cookies on the Node.js app we created earlier. First, install cookie-parser middleware by running npm install cookie-parser. Import the cookie-parser middleware into the app and initialize it with a secret key using the code below.

const cookieParser = require('cookie-parser');
// …
app.use(cookieParser('<your-secret-key>', {
   sameSite: 'strict'
}))

The value of <your-secret-key> should be a unique string of characters used for signing cookies — an unpredictable, large random value generated through a cryptographically secure method.

Signing a cookie using the unique string hashes its content, creating a unique signature. When the server receives the cookie from the browser later, the server can verify the cookie’s integrity by checking its signature with the same unique secret key.

This method is helpful against session hijacking, where an attacker tries to steal or impersonate a user’s session. If an attacker modifies the cookie, the cookie signature no longer matches, and the server detects that the cookie has been tampered with. Consequently, attackers can’t modify the session-related data stored in cookies.

Protecting our app using the Double Submit Cookie pattern

To implement the Double Submit Cookie pattern, install cookie-parser. Then, add the following code to the app to import the required packages and enable cookie parsing.

const cookieParser = require('cookie-parser');
const crypto = require('crypto');

app.use(cookieParser());

Now, create middleware functions to generate and validate CSRF tokens using the code below.

// generate CSRF token middleware
function generateCSRFToken(req, res, next) {
 const csrfToken = crypto.randomBytes(16).toString('hex');
 console.log(csrfToken)
 res.cookie('mycsrfToken', csrfToken);
 req.csrfToken = csrfToken;
 next();
}

// validate CSRF token middleware
function validateCSRFToken(req, res, next) {
 const csrfToken = req.cookies.mycsrfToken;
 if (req.body.csrfToken === csrfToken) {
   next();
 } else {
   res.status(403).send('Invalid CSRF token');
 }
}

Then, apply the generateCSRFToken middleware to the GET route.

 app.get('/', generateCSRFToken, (req, res) => {
 res.send(`
   <html>
     <body>
       <h1>CSRF Demo App</h1>
       <form method="POST" action="/transfer">
         <input type="hidden" name="csrfToken" value="${req.csrfToken}" />
         <input type="text" name="amount" placeholder="Amount" /><br />
         <input type="submit" value="Transfer" />
       </form>
     </body>
   </html>
 `);
});

Finally, apply the validateCSRFToken middleware function to the POST route.

app.post('/transfer', validateCSRFToken, (req, res) => {
 // Perform the transfer operation
 // (in a real application, you would validate and process the data here)
 res.send(`Successfully transferred the set amount`);
});

Testing CSRF protection

Testing CSRF protection is crucial to:

  • Prevent unauthorized actions — You can verify that the Node.js application and other implemented strategies properly validate and verify requests, preventing unauthorized actions.

  • Protect user data and privacy — You can ensure user data remains secure and protected during CSRF attacks that aim to expose or manipulate sensitive data.

  • Comply with security standards — Many security standards and regulations, such as the Payment Card Industry Data Security Standard (PCI DSS), require organizations to implement CSRF protection. Testing CSRF protection ensures compliance with these standards and helps avoid penalties or legal issues.

  • Identify vulnerabilities — By simulating attacks and testing the robustness of CSRF security measures, you can identify and address vulnerabilities before malicious individuals exploit them.

We can use a custom HTML form to simulate a CSRF attack on the original, unedited index.js code from the beginning of this tutorial. Consider the following HTML form.

<html>
<body>
  <h1>Welcome to the Lucky Prize Game!</h1>
  <p>Congratulations! You have won $100!</p>
  <p>Click the button below to claim your prize:</p>
  <form id="csrfForm" action="http://localhost:3000/transfer" method="POST">
	<input type="hidden" name="amount" value="100" />
	<input type="submit" value="Claim Prize" />
  </form>
  <script>
	document.getElementById('csrfForm').submit();
  </script>
</body>
</html>

This custom HTML form presents the user with an enticing message. It states that they've won a $100 prize and prompts them to claim their prize by clicking the button. Simultaneously, a hidden form submits the amount to the /transfer endpoint without the user's knowledge. The code inside the <script> tag in the HTML form automatically submits the form once the page loads. In the original code, the transfer would succeed as the server couldn't detect CSRF threats.

To test one of the CSRF protection strategies we implemented, run the server after applying the STP strategy. Then, try to submit the custom HTML form. The server will raise the ForbiddenError: invalid csrf token error, and the transfer isn't processed. This result shows that the CSRF protection strategy works as expected.

To test each implemented strategy:

  • STP — Craft a request without the correct token or with an expired one. Observe if the server rejects it.

  • SameSite cookies — Perform cross-origin requests from different domains. Verify if servers disallow unauthorized access due to absent session cookies.

  • Double Submit Cookies — Send a request with mismatched tokens in both submitted data or headers and session cookies, ensuring proper validation by servers.

Now, use Snyk to test the updated code for vulnerabilities. You can test the code for other strategies, but for this demonstration, we'll focus on the code implementing the STP strategy.

Run the command snyk code test to check for vulnerabilities. You should get a result like the following screenshot.

blog-csrf-xss

The results show the code has information exposure and XSS vulnerabilities but not CSRF vulnerabilities. This result means we fixed the CSRF vulnerability that we detected earlier.

Best practices for CSRF protection in Node.js applications

In addition to implementing the strategies above, strengthen CSRF protection with these best practices:

  • Regularly update dependencies and middleware to ensure up-to-date security configurations. Tools like Snyk Open Source streamline this process by identifying outdated components and suggesting patches.

  • Implement a Content Security Policy (CSP) that restricts requests to trusted sources, reducing potential attack vectors.

  • Combine multiple CSRF protection techniques such as STP with SameSite cookies or leverage Double Submit Cookies alongside CSP enforcement. This layered approach minimizes vulnerabilities arising from relying solely on a single strategy.

Next steps

Understanding CSRF attacks and their implications is crucial for securing Node.js applications. Implementing robust protection strategies, testing them effectively, and following best practices enhances the application's defense against threats.

Regularly testing for vulnerabilities — such as CSRF — and implementing robust security mechanisms helps mitigate risks and prevent unauthorized actions. Use proactive security measures and up-to-date techniques to build secure Node.js applications while addressing vulnerabilities before malicious actors exploit them.

To secure your Node.js applications, apply the concepts and strategies discussed throughout this article and try Snyk Code.