Lesson 16-Node Security Technology Application

CSRF

Introduction to CSRF

What is CSRF?

Cross-Site Request Forgery (CSRF) is a type of attack where an attacker tricks a victim’s browser into sending unauthorized requests to a web application where the victim is authenticated. The attacker exploits the trust that the web application has in the victim’s browser, leveraging the fact that browsers automatically include credentials (e.g., cookies, session tokens) in requests to the target site.

For example, imagine a user is logged into their banking website, and they visit a malicious site in another tab. The malicious site could send a request to the banking site (e.g., to transfer money) without the user’s knowledge. Since the browser includes the user’s session cookie, the banking site processes the request as if it came from the legitimate user.

Why is CSRF Dangerous?

CSRF attacks are dangerous because they:

  • Exploit authenticated sessions, allowing attackers to perform actions on behalf of users without their consent.
  • Can lead to severe consequences, such as unauthorized fund transfers, account modifications, or data leaks.
  • Are difficult for users to detect, as the attack occurs silently in the background.
  • Target common web application features like form submissions, API endpoints, or state-changing requests.

Goals of This Tutorial

This tutorial aims to:

  • Explain the mechanics of CSRF attacks in detail.
  • Demonstrate how to implement CSRF protection in Node.js applications using Express.
  • Provide step-by-step code examples with thorough analysis.
  • Cover advanced topics, such as CSRF token management, edge cases, and performance considerations.
  • Highlight best practices and common mistakes to avoid.
  • Include practical exercises to reinforce learning.

Understanding CSRF Attacks

How CSRF Works

To understand CSRF, let’s break down the attack flow:

  1. User Authentication: A user logs into a legitimate web application (e.g., bank.com), and the server issues a session cookie that the browser stores.
  2. Malicious Interaction: The user visits a malicious website (e.g., evil.com) or clicks a malicious link while still authenticated with bank.com.
  3. Forged Request: The malicious site sends a request (e.g., a POST to bank.com/transfer) to the target application. This could be triggered via:
  • A hidden form that auto-submits using JavaScript.
  • An <img> tag with a crafted src attribute.
  • An AJAX request initiated by JavaScript.
  1. Browser Behavior: The browser, unaware of the malicious intent, includes the user’s session cookie in the request to bank.com.
  2. Server Processing: The server, trusting the session cookie, processes the request as if it came from the user, performing the action (e.g., transferring money).

Example of a CSRF Attack

Consider a banking application with a form to transfer money:

<!-- Form on bank.com -->
<form action="/transfer" method="POST">
  <input type="text" name="amount" value="1000" />
  <input type="text" name="toAccount" value="123456" />
  <button type="submit">Transfer</button>
</form>

The server processes this form via a POST endpoint:

app.post('/transfer', (req, res) => {
  const { amount, toAccount } = req.body;
  // Perform transfer logic
  res.send('Transfer successful');
});

An attacker creates a malicious page on evil.com:

<!-- Malicious page on evil.com -->
<form action="https://bank.com/transfer" method="POST" id="maliciousForm">
  <input type="hidden" name="amount" value="10000" />
  <input type="hidden" name="toAccount" value="attackerAccount" />
</form>
<script>
  document.getElementById('maliciousForm').submit();
</script>

If the user is logged into bank.com and visits evil.com, the form auto-submits, sending a POST request to bank.com/transfer with the attacker’s parameters. The browser includes the user’s session cookie, and the server processes the transfer, unaware it was unauthorized.

Conditions for a Successful CSRF Attack

A CSRF attack requires:

  • The target application relies on cookies or other automatically included credentials for authentication.
  • The application has state-changing endpoints (e.g., POST, PUT, DELETE) that don’t validate request origins.
  • The user is authenticated with the target application during the attack.
  • The attacker can predict or craft the request parameters (e.g., form fields).

CSRF vs. XSS

CSRF is often confused with Cross-Site Scripting (XSS), but they are distinct:

  • CSRF: Exploits trust in the user’s authenticated session to send unauthorized requests. No code execution in the target application is required.
  • XSS: Injects malicious scripts into the target application, running code in the user’s browser. XSS can be used to facilitate CSRF by dynamically crafting requests.

CSRF Protection Mechanisms

To prevent CSRF attacks, applications must ensure that requests are intentionally sent by the authenticated user. Common protection mechanisms include:

  1. CSRF Tokens: Include a unique, unpredictable token in each state-changing request, validated by the server.
  2. SameSite Cookies: Configure cookies to restrict cross-site requests.
  3. Origin and Referer Headers: Verify the request’s origin to ensure it comes from a trusted source.
  4. Custom Headers: Require headers (e.g., X-Requested-With) that attackers cannot set in cross-site requests.
  5. User Interaction: Require user confirmation (e.g., re-authentication) for sensitive actions.

This tutorial focuses primarily on CSRF tokens, as they are the most widely used and robust solution, but we’ll also cover SameSite cookies and other techniques.

Setting Up a Node.js Application

Let’s create a Node.js application using Express to demonstrate CSRF protection. We’ll build a simple banking application with a transfer feature and progressively add CSRF security.

Prerequisites

Ensure you have:

  • Node.js (version 14 or later) installed.
  • A code editor (e.g., VS Code).
  • Basic knowledge of Express and HTTP.

Initialize the Project

Create a new directory and initialize a Node.js project:

mkdir node-csrf-demo
cd node-csrf-demo
npm init -y

Install dependencies:

npm install express express-session csurf cookie-parser body-parser
  • express: Web framework for Node.js.
  • express-session: Manages user sessions.
  • csurf: CSRF protection middleware.
  • cookie-parser: Parses cookies.
  • body-parser: Parses incoming request bodies (e.g., form data).

Basic Application Setup

Create app.js with a simple Express server:

const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const path = require('path');

const app = express();

// Middleware
app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(
  session({
    secret: 'your-secret-key',
    resave: false,
    saveUninitialized: false,
    cookie: { secure: false }, // Set to true in production with HTTPS
  })
);

// Set view engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

// Mock user authentication (for demo purposes)
app.use((req, res, next) => {
  req.session.user = { id: 1, username: 'testuser' }; // Simulate logged-in user
  next();
});

// Routes
app.get('/', (req, res) => {
  res.render('index', { user: req.session.user });
});

app.get('/transfer', (req, res) => {
  res.render('transfer');
});

app.post('/transfer', (req, res) => {
  const { amount, toAccount } = req.body;
  // Mock transfer logic
  console.log(`Transferring ${amount} to account ${toAccount}`);
  res.send('Transfer successful');
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Create a views directory with two EJS templates:

views/index.ejs:

<!DOCTYPE html>
<html>
<head>
  <title>Banking App</title>
</head>
<body>
  <h1>Welcome, <%= user.username %></h1>
  <a href="/transfer">Make a Transfer</a>
</body>
</html>

views/transfer.ejs:

<!DOCTYPE html>
<html>
<head>
  <title>Transfer Money</title>
</head>
<body>
  <h1>Transfer Money</h1>
  <form action="/transfer" method="POST">
    <label>Amount:</label>
    <input type="text" name="amount" value="1000" />
    <label>To Account:</label>
    <input type="text" name="toAccount" value="123456" />
    <button type="submit">Transfer</button>
  </form>
</body>
</html>

Run the application:

node app.js

Visit http://localhost:3000 to see the homepage, and click the link to access the transfer form. Submitting the form sends a POST request to /transfer.

Current Vulnerability

This application is vulnerable to CSRF because:

  • It uses session cookies for authentication.
  • The /transfer endpoint accepts POST requests without validating their origin.
  • An attacker could craft a malicious form to trigger unauthorized transfers.

Let’s simulate a CSRF attack to understand the risk.

Simulating a CSRF Attack

Create a malicious HTML file (evil.html) to mimic an attacker’s website:

<!DOCTYPE html>
<html>
<head>
  <title>Evil Site</title>
</head>
<body>
  <h1>You’ve been hacked!</h1>
  <form action="http://localhost:3000/transfer" method="POST" id="maliciousForm">
    <input type="hidden" name="amount" value="10000" />
    <input type="hidden" name="toAccount" value="attackerAccount" />
  </form>
  <script>
    document.getElementById('maliciousForm').submit();
  </script>
</body>
</html>

Host this file using a simple server (e.g., Python’s http.server):

python3 -m http.server 8000

Open http://localhost:8000/evil.html in a browser where you’re logged into the banking app. The form auto-submits, and the server logs:

Transferring 10000 to account attackerAccount

The transfer succeeds because the browser included the session cookie, and the server trusted the request. This confirms the CSRF vulnerability.

Implementing CSRF Protection with Tokens

CSRF Token Overview

CSRF tokens are unique, unpredictable values generated by the server and included in state-changing requests (e.g., forms, AJAX calls). The server validates the token to ensure the request originates from a legitimate source. Tokens are typically:

  • Stored in the user’s session.
  • Embedded in forms as hidden fields or sent in headers for AJAX requests.
  • Validated on the server before processing the request.

We’ll use the csurf middleware to add CSRF protection to our application.

Step 1: Install and Configure csurf

The csurf middleware is already installed. Let’s integrate it into app.js.

Modify app.js to include csurf and generate tokens for the transfer form:

const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const csrf = require('csurf'); // Import csurf
const path = require('path');

const app = express();

// Middleware
app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(
  session({
    secret: 'your-secret-key',
    resave: false,
    saveUninitialized: false,
    cookie: { secure: false },
  })
);
app.use(csrf()); // Add CSRF middleware

// Set view engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

// Mock user authentication
app.use((req, res, next) => {
  req.session.user = { id: 1, username: 'testuser' };
  next();
});

// Routes
app.get('/', (req, res) => {
  res.render('index', { user: req.session.user });
});

app.get('/transfer', (req, res) => {
  res.render('transfer', { csrfToken: req.csrfToken() }); // Pass CSRF token to form
});

app.post('/transfer', (req, res) => {
  const { amount, toAccount } = req.body;
  console.log(`Transferring ${amount} to account ${toAccount}`);
  res.send('Transfer successful');
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Update views/transfer.ejs to include the CSRF token:

<!DOCTYPE html>
<html>
<head>
  <title>Transfer Money</title>
</head>
<body>
  <h1>Transfer Money</h1>
  <form action="/transfer" method="POST">
    <input type="hidden" name="_csrf" value="<%= csrfToken %>" /> <!-- Add CSRF token -->
    <label>Amount:</label>
    <input type="text" name="amount" value="1000" />
    <label>To Account:</label>
    <input type="text" name="toAccount" value="123456" />
    <button type="submit">Transfer</button>
  </form>
</body>
</html>

Step 2: Test the Protection

Restart the server and visit http://localhost:3000/transfer. Submit the form to confirm it works. The server validates the _csrf token automatically.

Now, revisit http://localhost:8000/evil.html. The malicious form fails with a 403 Forbidden error because it lacks a valid CSRF token. The server logs:

ForbiddenError: invalid csrf token

How csurf Works

  1. Token Generation: The csurf middleware generates a unique token for each user session, stored in req.session._csrf.
  2. Token Embedding: The req.csrfToken() method retrieves the token, which we include in the form as a hidden field (_csrf).
  3. Token Validation: On POST requests, csurf checks the _csrf field (or X-CSRF-Token header for AJAX) against the session token. If invalid or missing, it throws a ForbiddenError.

Step 3: Handling CSRF Errors

To provide a better user experience, let’s handle CSRF errors gracefully:

// Add error handling middleware
app.use((err, req, res, next) => {
  if (err.code === 'EBADCSRFTOKEN') {
    res.status(403).send('Invalid CSRF token. Please try again.');
  } else {
    next(err);
  }
});

Place this middleware at the end of app.js. Now, invalid CSRF tokens return a user-friendly message instead of a generic error.

Step 4: Supporting AJAX Requests

For AJAX-based forms, include the CSRF token in a custom header. Update transfer.ejs to use JavaScript:

<!DOCTYPE html>
<html>
<head>
  <title>Transfer Money</title>
</head>
<body>
  <h1>Transfer Money</h1>
  <form id="transferForm">
    <label>Amount:</label>
    <input type="text" name="amount" value="1000" />
    <label>To Account:</label>
    <input type="text" name="toAccount" value="123456" />
    <button type="submit">Transfer</button>
  </form>
  <script>
    const form = document.getElementById('transferForm');
    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      const formData = new FormData(form);
      const data = {
        amount: formData.get('amount'),
        toAccount: formData.get('toAccount'),
      };
      const response = await fetch('/transfer', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': '<%= csrfToken %>', // Include CSRF token
        },
        body: JSON.stringify(data),
      });
      const result = await response.text();
      alert(result);
    });
  </script>
</body>
</html>

Update the /transfer endpoint to parse JSON:

app.use(bodyParser.json()); // Add JSON parser
app.post('/transfer', (req, res) => {
  const { amount, toAccount } = req.body;
  console.log(`Transferring ${amount} to account ${toAccount}`);
  res.send('Transfer successful');
});

Test the form again. The AJAX request includes the CSRF token in the X-CSRF-Token header, and csurf validates it.

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Share your love