Understanding and Implementing CSRF Protection in Express

A Comprehensive Guide to Securing Your Web Applications

Introduction to CSRF

Imagine you're at home, and someone calls pretending to be your bank. They sound legitimate and ask you to make a transfer. Without proper verification, you might accidentally send money to a fraudster. This is similar to how Cross-Site Request Forgery (CSRF) works in web applications - a malicious site pretends to be your legitimate application and makes requests on behalf of your users.

What is CSRF?

CSRF (Cross-Site Request Forgery) is like a forged signature on a check. Just as a forged signature can authorize fraudulent transactions, CSRF attacks can trick your application into executing unwanted actions on behalf of authenticated users.

Project Setup

Let's create a new project to demonstrate CSRF protection. First, set up your project structure:

projectroot/
├── src/
│   ├── views/
│   │   ├── login.ejs
│   │   └── dashboard.ejs
│   ├── routes/
│   │   └── auth.js
│   └── app.js
├── package.json
└── README.md

Initialize your project and install the necessary dependencies:

npm init -y
npm install express express-session csurf cookie-parser ejs

Basic Implementation

Let's start with the core application setup in src/app.js:

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

const app = express();

// Middleware setup
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(session({
    secret: 'your-secret-key',
    resave: false,
    saveUninitialized: false
}));

// Initialize CSRF protection
const csrfProtection = csrf({ cookie: true });
app.use(csrfProtection);

// Error handler for CSRF token errors
app.use((err, req, res, next) => {
    if (err.code === 'EBADCSRFTOKEN') {
        res.status(403);
        res.send('Invalid CSRF token. Please try again.');
    } else {
        next(err);
    }
});

Understanding the Code

Think of CSRF protection like a special handshake between your server and client:

  • cookieParser(): Acts like a mailroom, sorting through incoming cookies
  • session(): Creates a secure locker for each user's session
  • csrf(): Generates unique tokens, like special stamps that verify authenticity

Implementing Protected Forms

Create a login form in src/views/login.ejs:

<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
</head>
<body>
    <form action="/login" method="POST">
        <input type="hidden" name="_csrf" value="<%= csrfToken %>">
        <input type="email" name="email" required>
        <input type="password" name="password" required>
        <button type="submit">Login</button>
    </form>
</body>
</html>

Set up the route handler in src/routes/auth.js:

const express = require('express');
const router = express.Router();

router.get('/login', (req, res) => {
    // Generate and pass CSRF token to view
    res.render('login', { csrfToken: req.csrfToken() });
});

router.post('/login', (req, res) => {
    // CSRF token is automatically validated
    // Handle login logic here
    res.redirect('/dashboard');
});

module.exports = router;

Real-World Applications

Banking Transactions

Consider an online banking application where users can transfer money. Here's how you'd implement CSRF protection for a transfer form:

<!-- transfer.ejs -->
<form action="/transfer" method="POST">
    <input type="hidden" name="_csrf" value="<%= csrfToken %>">
    <input type="text" name="recipient" placeholder="Recipient Account">
    <input type="number" name="amount" placeholder="Amount">
    <button type="submit">Transfer</button>
</form>
// Transfer route handler
router.post('/transfer', (req, res) => {
    // CSRF token is validated automatically
    const { recipient, amount } = req.body;
    // Perform transfer logic
    res.redirect('/transfer/success');
});

Security Best Practices

Token Management

Just as you wouldn't use the same key for every lock in your house, avoid reusing CSRF tokens. The csurf middleware automatically generates unique tokens for each request.

Error Handling

Implement proper error handling to gracefully manage token validation failures:

app.use((err, req, res, next) => {
    if (err.code === 'EBADCSRFTOKEN') {
        // Log the error for monitoring
        console.error('CSRF Token Error:', err);
        
        // Redirect to login or show error page
        res.status(403).render('error', {
            message: 'Security validation failed. Please try again.'
        });
    } else {
        next(err);
    }
});

Hands-On Exercise

Building a Secure Profile Update Form

Create a profile update form that allows users to change their display name and email. Implement CSRF protection and proper error handling.

// Profile update form template (profile.ejs)
<form action="/profile/update" method="POST">
    <input type="hidden" name="_csrf" value="<%= csrfToken %>">
    <input type="text" name="displayName" value="<%= user.displayName %>">
    <input type="email" name="email" value="<%= user.email %>">
    <button type="submit">Update Profile</button>
</form>

Implement the route handler:

// Profile routes (routes/profile.js)
const express = require('express');
const router = express.Router();

router.get('/profile', (req, res) => {
    res.render('profile', {
        csrfToken: req.csrfToken(),
        user: req.user
    });
});

router.post('/profile/update', (req, res) => {
    const { displayName, email } = req.body;
    // Implement update logic
    res.redirect('/profile');
});

Additional Topics to Explore

Related Security Concepts

  • Same-Origin Policy and its relationship with CSRF
  • Double Submit Cookie Pattern
  • SameSite Cookie Attribute
  • Content Security Policy (CSP)