Express CSRF Protection
Introduction
Cross-Site Request Forgery (CSRF or XSRF) is a type of security vulnerability that allows attackers to trick users into performing unwanted actions on websites where they're already authenticated. In simpler terms, it's like someone forging your signature to authorize transactions without your knowledge.
CSRF attacks exploit the trust that a website has in a user's browser. For example, if you're logged into your banking website and visit a malicious site in another tab, that malicious site could potentially make requests to your bank on your behalf without your knowledge.
In this tutorial, we'll explore how to implement CSRF protection in Express.js applications to prevent these types of attacks.
Understanding CSRF Attacks
Before we dive into protection techniques, let's understand how a CSRF attack works:
- You log in to a website (e.g., your bank)
- The website sets an authentication cookie in your browser
- Without logging out, you visit a malicious website
- That website contains code that makes a request to your bank (like transferring money)
- Because your browser still has the authentication cookie, the bank thinks it's a legitimate request from you
Implementing CSRF Protection in Express
The most common way to protect against CSRF attacks in Express is using the csurf
middleware. However, as of 2022, the csurf
package has been deprecated. The recommended alternative is to use csrf-csrf
, which provides similar functionality with modern security practices.
Step 1: Install the necessary packages
npm install csrf-csrf express-session cookie-parser
Step 2: Set up the middleware
const express = require('express');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const { csrfSync } = require('csrf-csrf');
const app = express();
// Middleware setup
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser('your-secret-key'));
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: true,
cookie: { secure: process.env.NODE_ENV === 'production' }
}));
// CSRF Protection
const { generateToken, csrfSynchronisedProtection } = csrfSync({
getTokenFromRequest: (req) => req.body._csrf || req.query._csrf || req.headers['x-csrf-token'],
});
// Apply CSRF protection to all routes
app.use(csrfSynchronisedProtection);
// Generate and expose CSRF token in res.locals for use in templates
app.use((req, res, next) => {
res.locals.csrfToken = generateToken(req);
next();
});
Step 3: Including the token in your forms
When building forms in your application, you need to include the CSRF token as a hidden field:
<form action="/submit" method="POST">
<input type="hidden" name="_csrf" value="${csrfToken}" />
<input type="text" name="username" placeholder="Username" />
<input type="password" name="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
In a templating engine like EJS, you would use:
<form action="/submit" method="POST">
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
<!-- Other form fields -->
</form>
Step 4: Including the token in AJAX requests
For AJAX requests, you would include the CSRF token in your headers:
// Using fetch API
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken // Get this from a meta tag or a global variable
},
body: JSON.stringify(data)
})
Complete Example: CSRF Protection in a Simple Application
Let's create a simple Express application with CSRF protection:
const express = require('express');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const { csrfSync } = require('csrf-csrf');
const path = require('path');
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser('cookie-secret'));
app.use(session({
secret: 'session-secret',
resave: false,
saveUninitialized: true,
cookie: { secure: process.env.NODE_ENV === 'production' }
}));
// Set up template engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// CSRF Protection
const { generateToken, csrfSynchronisedProtection } = csrfSync({
getTokenFromRequest: (req) => req.body._csrf || req.query._csrf || req.headers['x-csrf-token'],
});
// Apply CSRF protection
app.use(csrfSynchronisedProtection);
// Make CSRF token available to all templates
app.use((req, res, next) => {
res.locals.csrfToken = generateToken(req);
next();
});
// Routes
app.get('/', (req, res) => {
res.render('index');
});
app.post('/update-profile', (req, res) => {
// If we get here, CSRF validation passed
// Update user profile with req.body data
res.send('Profile updated successfully!');
});
// Error handler for CSRF validation failures
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
return res.status(403).send('CSRF validation failed. Form tampered with.');
}
next(err);
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Create a views folder with an index.ejs
file:
<!DOCTYPE html>
<html>
<head>
<title>CSRF Protection Example</title>
</head>
<body>
<h1>Update Profile</h1>
<form action="/update-profile" method="POST">
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
<div>
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</div>
<button type="submit">Update Profile</button>
</form>
</body>
</html>
When to Use CSRF Protection
You should implement CSRF protection in the following scenarios:
- For any non-GET requests that cause state changes (POST, PUT, DELETE, etc.)
- For authenticated actions (user settings, financial transactions, etc.)
- When forms submit data that modifies information in your database
CSRF Protection with Single-Page Applications (SPAs)
For SPAs using frameworks like React, Angular, or Vue.js, the approach is slightly different:
- When the SPA loads, make an API request to get a CSRF token
- Store this token in memory (not in localStorage or sessionStorage)
- Include the token in the headers of all subsequent API requests
Here's an example using React:
// In your React application
import axios from 'axios';
// Create axios instance with CSRF handling
const api = axios.create({
baseURL: '/api',
withCredentials: true
});
// Get CSRF token when app initializes
let csrfToken;
const fetchCsrfToken = async () => {
try {
const response = await axios.get('/api/csrf-token');
csrfToken = response.data.csrfToken;
// Set up axios to automatically include the token
api.interceptors.request.use(config => {
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});
} catch (error) {
console.error('Failed to fetch CSRF token:', error);
}
};
// Call this when your app initializes
fetchCsrfToken();
And the corresponding Express endpoint:
// In your Express backend
app.get('/api/csrf-token', (req, res) => {
// Generate a new token and send it to the client
res.json({ csrfToken: generateToken(req) });
});
Best Practices for CSRF Protection
- Don't rely on CSRF alone: Use it as part of a defense-in-depth security strategy
- Use SameSite cookie attribute: Set cookies with
SameSite=Lax
orSameSite=Strict
for additional protection - Use HTTPS: Encrypt all traffic to prevent token leakage
- Implement proper error handling: Provide clear but non-specific error messages on token validation failure
- Refresh tokens periodically: For critical applications, consider regenerating tokens after specific actions
- Keep dependencies updated: Security libraries frequently receive updates to address vulnerabilities
Common Pitfalls and Troubleshooting
"Invalid CSRF token" errors
If you're getting these errors, check:
- Are you including the token in your forms/requests?
- Is the token being generated correctly?
- Are cookies being properly set and sent?
CSRF token not working with AJAX requests
Make sure:
- You're sending the token in the correct header (usually
X-CSRF-Token
) - Your CORS settings allow the necessary headers
- The session/cookie is being properly sent with the request
Multiple forms on a single page
You can use the same CSRF token for all forms on a page, but be careful not to reuse the token across page loads.
Summary
CSRF attacks exploit a user's authenticated session to perform unwanted actions on their behalf. By implementing CSRF protection in your Express applications, you're adding a crucial layer of security that validates that requests actually come from your frontend, not from malicious sites.
Key points to remember:
- CSRF protection validates that requests come from your application
- Use
csrf-csrf
middleware in your Express applications - Include CSRF tokens in all forms and AJAX requests that modify data
- Combine CSRF protection with other security measures like HTTPS and secure cookies
By following these practices, you can significantly reduce the risk of CSRF attacks against your application and better protect your users' data and actions.
Additional Resources
Exercises
- Create a simple Express application with a login form and a profile update form, both protected with CSRF tokens.
- Modify the example to use a different template engine (e.g., Handlebars or Pug).
- Implement CSRF protection for a React application that communicates with an Express backend.
- Add error handling that logs attempts to bypass CSRF protection but shows user-friendly error messages.
- Research and implement the double-submit cookie pattern as an alternative to the synchronizer token pattern shown in this tutorial.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)