When users interact with your Node.js web application, they trust that their actions are intentional and secure. But what happens when that trust gets weaponized against them? Cross-Site Request Forgery (CSRF) attacks do exactly thatโthey turn your users’ legitimate sessions into attack vectors that can drain bank accounts, modify sensitive data, or compromise entire systems.
CSRF attacks are particularly dangerous because they’re nearly invisible to both users and developers. A user might click an innocent-looking link in an email or visit a compromised website, and suddenly they’ve unknowingly authorized a harmful action in your application.
This comprehensive guide will teach you everything you need to protect your Node.js applications from CSRF vulnerabilities. We’ll explore real-world attack scenarios, examine why traditional security measures fall short, and implement robust protection mechanisms that work across different application architectures. Letโs start by looking at what CSRF is.
What Is CSRF?
Cross-Site Request Forgery (CSRF), also known as XSRF, is an attack that exploits the trust relationship between a user and a web application. Unlike other security vulnerabilities that directly target your server or code, CSRF attacks manipulate authenticated users into performing actions they never intended to take.
The fundamental principle behind CSRF attacks is deceptively simple: when users are authenticated to your application (usually through cookies or sessions), their browser automatically includes authentication credentials with every request to your domain. Attackers exploit this automatic credential inclusion by tricking users into making requests to your application from malicious websites, emails, or other sources.
The Anatomy of a CSRF Attack
CSRF attacks succeed because they exploit several fundamental aspects of how web browsers and applications interact:
Automatic Credential Inclusion: Browsers automatically include cookies, HTTP authentication headers, and other credentials when making requests to a domain, regardless of which website initiated the request.
User Trust Exploitation: Users cannot easily distinguish between legitimate requests they intended to make and malicious requests initiated by attackers.
The combination of these factors creates scenarios where legitimate user credentials can be weaponized against your application without users realizing they’re under attack.
Do You Need CSRF Protection?
Before implementing CSRF protection, it’s important to understand whether your Node.js application actually needs it. Not every application faces the same level of CSRF risk, and the protection strategies you choose should match your application’s specific security requirements and architecture.
Needs CSRF Protection | Lower CSRF Risk |
Session-Based Authentication: Uses cookies or sessions for user authentication | Pure Token-Only APIs: Uses only Authorization headers, never cookies |
State-Changing Operations: Allows data modification, transfers, settings changes | Read-Only APIs: No state-changing operations |
Browser-Based Clients: Accepts requests from web browsers | Server-to-Server APIs: Only communicates with other servers |
Mixed Authentication: Uses both cookies and tokens | Stateless Mobile APIs: Only serves mobile apps with API keys |
Important Clarification: Stateless API-only applications have virtually eliminated CSRF risk only if they exclusively accept tokens in non-cookie headers (like the Authorization header) and never use cookies for authentication. The moment an API accepts cookies in any formโeven alongside tokensโa CSRF vulnerability returns.
Common CSRF Attack Scenarios
Understanding how CSRF attacks work in practice helps illustrate why protection is so critical and informs the defensive strategies we’ll implement.
The Classic Email Attack
A user receives an email that appears to come from their bank with urgent language about account security. The email contains a link that appears legitimate. When clicked, the user is directed to what seems like the legitimate website, but hidden within the page is a malicious request to transfer money or change account settings, all using the user’s legitimate credentials.
Hidden Form Attacks
Another common attack involves hidden forms embedded in websites or emails:
<!-- Hidden in a malicious website -->
<form action="https://yourbank.com/transfer" method="POST" id="maliciousForm">
<input type="hidden" name="amount" value="10000">
<input type="hidden" name="recipient" value="attacker-account">
</form>
<script>
document.getElementById('maliciousForm').submit();
</script>
If a user visits this site while logged into their banking application, their browser will automatically submit the form with their authentication cookies.
Which Node.js Framework Are You Using?
The specific approach to CSRF protection varies depending on your Node.js framework and architecture. While the fundamental principles remain consistent, different frameworks provide different tools and patterns for implementing protection.
Express.js: As the most popular Node.js web framework, Express.js offers several CSRF protection options, including the widely used csurf middleware and newer alternatives. Most examples in this guide focus on Express.js implementations.
Fastify: Fastify applications can use the @fastify/csrf-protection plugin, which provides similar functionality to Express.js solutions but integrates with Fastify’s plugin architecture.
Koa.js: Koa applications typically use koa-csrf or similar middleware that follows Koa’s async/await patterns.
Next.js and Other Full-Stack Frameworks: Full-stack frameworks often include built-in CSRF protection or provide official plugins that integrate seamlessly with their authentication and routing systems.
For this guide, we’ll focus primarily on Express.js implementations since they represent the most common Node.js web application pattern, but the concepts translate readily to other frameworks.
Basic CSRF Protection Strategies
Before diving into specific implementation details, it’s important to understand the fundamental strategies for CSRF protection. These approaches work by breaking the basic assumptions that make CSRF attacks possible.
Proper HTTP Method Usage
The foundation of CSRF protection starts with proper HTTP method usage. Many CSRF attacks exploit applications that perform state-changing operations using GET requests, which can be triggered by simple image tags or link clicks.
The Problem: Applications that allow state changes via GET requests are vulnerable to simple CSRF attacks:
// VULNERABLE: State change via GET request
app.get('/delete-account', (req, res) => {
// This can be triggered by an image tag or malicious link
deleteUserAccount(req.session.userId);
res.redirect('/goodbye');
});
The Solution: Restrict state-changing operations to appropriate HTTP methods (POST, PUT, DELETE). This follows HTTP specification guidelines and is essential for both security and proper API design:
// SECURE: State change via POST request
app.post('/delete-account', (req, res) => {
// This requires a form submission or AJAX call and follows HTTP specs
deleteUserAccount(req.session.userId);
res.redirect('/goodbye');
});
Itโs important to note that GET requests should never mutate state. This is not just for CSRF protection but for HTTP specification compliance. Search engines, browsers prefetching, and other automated tools assume GET requests are safe to repeat.
This simple change eliminates the most basic CSRF attacks, though more sophisticated attacks using JavaScript can still bypass this protection.
SameSite Cookie Attribute
Modern browsers support the SameSite cookie attribute, which provides significant CSRF protection with minimal implementation effort:
app.use(session({
secret: 'your-secret-key',
cookie: {
sameSite: 'strict', // or 'lax' for more compatibility
secure: true, // Enable in production with HTTPS
httpOnly: true // Prevents JavaScript access to session cookie
}
}));
SameSite Options:
- strict: Cookies never sent with cross-site requests (most secure)
- lax: Cookies sent with top-level navigation but not embedded requests (good balance)
- none: Cookies always sent (requires secure: true, use with caution)
While SameSite cookies provide excellent baseline protection, they should not be relied upon as the sole CSRF control:
- Browser Support: Older browsers (Safari โค12, Internet Explorer, older Chrome/Firefox versions) may not respect SameSite=strict/lax settings, leaving these users unprotected
- Mobile Apps: Non-web clients often have limited or inconsistent SameSite support
- Complex Integrations: Some OAuth flows, embedded widgets, or cross-site authentication may break with strict SameSite settings
Therefore, SameSite should be used as part of a defense-in-depth strategy with fallback token-based validation to remain robust across all client environments, not as a replacement for CSRF tokens.
Referrer Header Validation
Another protective measure involves validating the Referer header to ensure requests originate from your application:
function validateReferrer(req, res, next) {
const referer = req.get('Referer');
const allowedOrigins = ['https://yourapp.com', 'https://www.yourapp.com'];
if (!referer || !allowedOrigins.some(origin => referer.startsWith(origin))) {
return res.status(403).json({ error: 'Invalid referer' });
}
next();
}
app.post('/sensitive-action', validateReferrer, (req, res) => {
// Protected route
});
However, referrer validation has limitationsโsome browsers or privacy tools may strip referrer headers, and the header can be spoofed in certain circumstances.
Implementing CSRF Tokens
The most robust and widely adopted CSRF protection mechanism uses cryptographically secure tokens that prove a request originated from your application. This approach works by embedding unique, unpredictable tokens in your forms and AJAX requests, then validating these tokens on the server.
How CSRF Tokens Work
CSRF tokens leverage the same-origin policy that prevents malicious websites from reading content from your application. The protection flow works like this:
- Token Generation: The Server generates a unique, unpredictable token for each user session
- Token Embedding: Token is embedded in forms or made available to JavaScript
- Token Transmission: Users submit the token with their requests
- Token Validation: The Server validates the token before processing requests
Malicious websites cannot obtain valid tokens because browsers prevent them from reading content from your domain due to same-origin policy restrictions.
Token Generation Flow
Understanding when and how tokens are created helps ensure proper implementation:
- Session-Based: One token per user session (used by most implementations)
- Per-Request: New token for every request (maximum security, added complexity)
- Per-Form: Unique token for each form (balance between security and usability)
Why CSRF Tokens Work
The security of CSRF tokens relies on two fundamental web security principles:
- Same-Origin Policy: Malicious sites cannot read responses from your domain
- Unpredictability: Tokens use cryptographically secure random generation
This combination means attackers cannot obtain valid tokens to include in their malicious requests.
Setting Up CSRF Protection with Express.js
The original csurf package was deprecated and archived in September 2022 due to identified security issues in its token validation approach and lack of maintenance. Here are a couple recommended alternatives and how to use them in your code:
- @dr.pogodin/csurf – A well-maintained community fork of the original csurf, preserving its API while offering critical updates:
const express = require('express');
const session = require('express-session');
const csrf = require('@dr.pogodin/csurf'); // Community-maintained fork
const app = express();
// Session middleware is required for CSRF token storage and validation
app.use(session({
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
httpOnly: true, // Prevents JavaScript access to session cookie
maxAge: 24 * 60 * 60 * 1000, // 24 hours
sameSite: 'strict' // Prevents cross-site cookie transmission
}
}));
// Body parsing middleware to handle form submissions and JSON requests
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
// Initialize CSRF protection middleware
const csrfProtection = csrf({
cookie: {
httpOnly: true, // CSRF cookie can't be accessed via JavaScript
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}
});
// Apply CSRF protection to all routes
app.use(csrfProtection);
This approach uses the Synchronizer Token Pattern, where tokens are stored server-side and validated against user sessions.
- csrf-csrf – Implements a stateless double-submit cookie CSRF protection pattern, suitable for stateless or SPA architectures:
const { doubleCsrf } = require('csrf-csrf');
const { generateToken, doubleCsrfProtection } = doubleCsrf({
// Secret used for cryptographic token generation
getSecret: () => process.env.CSRF_SECRET || 'default-csrf-secret',
cookieName: 'csrf-token', // Name of cookie that stores the token
cookieOptions: {
httpOnly: true, // Token cookie can't be accessed via JavaScript
sameSite: 'strict', // Prevents cross-site cookie transmission
secure: process.env.NODE_ENV === 'production' // HTTPS only in production
},
size: 64, // Token size in bytes (larger = more secure)
// HTTP methods that don't require CSRF token validation
ignoredMethods: ['GET', 'HEAD', 'OPTIONS'],
});
// Apply CSRF protection middleware to all routes
app.use(doubleCsrfProtection);
This approach uses the Double-Submit Cookie Pattern where tokens are stateless and validated by comparing cookie values with request headers or form fields.
Note that if you require session-based synchronizer token patterns instead of stateless double-submit cookies, consider csrf-sync or equivalent functionality.
The __Host- cookie prefix requires HTTPS and must be set at the root path (/). If your application doesn’t meet these requirements, use a regular cookie name.
This setup provides several important security features:
- Secure Token Generation: Uses cryptographically secure random token generation
- Automatic Validation: Middleware automatically validates tokens on protected requests
- Cookie Security: Implements secure cookie practices, including HttpOnly and SameSite attributes
- Environment Awareness: Adjusts security settings based on development vs production environments
Making CSRF Tokens Available to Forms
Once you’ve configured CSRF protection middleware, you need to make tokens available to your HTML forms and templates:
// Route that renders a form with CSRF token
app.get('/profile', (req, res) => {
// With @dr.pogodin/csurf, the token is available as req.csrfToken()
// With csrf-csrf, use the generateToken function
const csrfToken = req.csrfToken(); // or generateToken(req, res) for csrf-csrf
// Pass the token to your template engine (EJS, Handlebars, Pug, etc.)
res.render('profile', {
user: req.user,
csrfToken: csrfToken
});
});
In your HTML template, include the token as a hidden form field:
<!-- EJS template example -->
<form action="/update-profile" method="POST">
<!-- CSRF token as hidden field - this is what prevents CSRF attacks -->
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<label for="email">Email:</label>
<input type="email" name="email" value="<%= user.email %>" required>
<label for="name">Name:</label>
<input type="text" name="name" value="<%= user.name %>" required>
<button type="submit">Update Profile</button>
</form>
When the form is submitted, the middleware automatically validates the _csrf field against the stored token, rejecting requests with missing or invalid tokens.
Handling CSRF Tokens in AJAX Requests
Modern web applications often use AJAX requests for better user experiences. These requests also need CSRF protection, which requires making tokens available to JavaScript:
// Route that provides CSRF token for AJAX requests
app.get('/api/csrf-token', (req, res) => {
const token = req.csrfToken(); // or generateToken(req, res) for csrf-csrf
res.json({ csrfToken: token });
});
Exposing CSRF tokens via /api/csrf-token is safe only when you use SameSite and HttpOnly cookies for session management, which prevents malicious sites from accessing both the session and the token endpoint.
On the client side, you can fetch and use the token in AJAX requests:
// Fetch CSRF token and store it for AJAX requests
let csrfToken;
async function fetchCsrfToken() {
try {
const response = await fetch('/api/csrf-token');
const data = await response.json();
csrfToken = data.csrfToken;
} catch (error) {
console.error('Failed to fetch CSRF token:', error);
}
}
// Include CSRF token in AJAX requests
async function updateProfile(profileData) {
try {
const response = await fetch('/update-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken // Include token in custom header
},
body: JSON.stringify(profileData)
});
if (response.ok) {
console.log('Profile updated successfully');
} else {
console.error('Failed to update profile');
}
} catch (error) {
console.error('Network error:', error);
}
}
// Initialize CSRF token when page loads
document.addEventListener('DOMContentLoaded', fetchCsrfToken);
Configuring Token Validation
Different applications may need different token validation approaches. You can configure how and where tokens are expected:
const { doubleCsrf } = require('csrf-csrf');
const {
doubleCsrfProtection,
} = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET,
cookieName: '__Host-psifi.x-csrf-token',
cookieOptions: {
httpOnly: true,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production'
},
size: 64,
ignoredMethods: ['GET', 'HEAD', 'OPTIONS'],
// Accept tokens from multiple sources
getTokenFromRequest: (req) => {
return req.body._csrf || // Form field
req.headers['x-csrf-token'] || // Custom header
req.headers['csrf-token']; // Alternative header name
}
});
This configuration allows tokens to be submitted via form fields, custom headers, or alternative header names, providing flexibility for different types of requests.
Advanced CSRF Protection Techniques
While basic CSRF token implementation provides strong protection, modern applications often require more sophisticated approaches to handle complex scenarios and provide better user experiences.
Double-Submit Cookie Pattern vs Synchronizer Token Pattern
Understanding the trade-offs between CSRF protection approaches helps you choose the right implementation:
Synchronizer Token Pattern (used by @dr.pogodin/csurf):
- Security: Stronger protection – server validates tokens against stored session data, reducing spoofing risks
- Server State: Requires server-side token storage (memory, database, or Redis)
- Scalability: May require session affinity in distributed systems, imposing server-state overhead
- Recommended for: Most web applications, especially those already using sessions
Double-Submit Cookie Pattern (used by csrf-csrf):
- Security: Good protection when implemented correctly – relies on same-origin policy and cookie security
- Server State: Stateless – no server-side token storage required
- Scalability: Better for distributed/stateless systems
- Implementation Risks: Sensitive to implementation correctness; vulnerable to header or cookie spoofing if same-origin enforcement fails, or if attackers control subdomains
- Recommended for: High-scale stateless applications where session storage is impractical
OWASP Recommendation: “Stateful software should use the synchronizer token pattern. Stateless software should use double submit cookies.”
The Synchronizer Token Pattern is generally considered more robust because it doesn’t rely solely on client-side mechanisms and provides server-side validation of token authenticity, making it harder to spoof than double-submit cookie implementations.
const crypto = require('crypto');
function generateDoubleSubmitToken() {
return crypto.randomBytes(32).toString('hex');
}
// Middleware to set double-submit cookie and validate requests
function doubleSubmitCsrfProtection(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
// Set token cookie for safe methods
const token = generateDoubleSubmitToken();
res.cookie('csrf-token', token, {
httpOnly: false, // Must be readable by JavaScript
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
});
return next();
}
// Validate token for state-changing methods
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({
error: 'CSRF token validation failed'
});
}
next();
}
app.use(doubleSubmitCsrfProtection);
This approach works because malicious sites cannot read the cookie value due to the same-origin policy, so they cannot include the correct token in their requests.
Per-Request Token Rotation
For maximum security, some applications rotate CSRF tokens with every request:
// Enhanced token management with rotation
class CsrfTokenManager {
constructor() {
// WARNING: Do not use in-memory storage like Map() in production
// This is not shared across server instances - use Redis instead
this.tokens = new Map();
}
generateToken(sessionId) {
const token = crypto.randomBytes(32).toString('hex');
const expiry = Date.now() + (60 * 60 * 1000); // 1 hour expiry
// In production: store in Redis or distributed cache
this.tokens.set(sessionId, { token, expiry });
return token;
}
validateAndRotateToken(sessionId, submittedToken) {
const stored = this.tokens.get(sessionId);
if (!stored || stored.expiry < Date.now() || stored.token !== submittedToken) {
this.tokens.delete(sessionId);
return false;
}
// Generate new token for next request
const newToken = this.generateToken(sessionId);
return { valid: true, newToken };
}
}
Header Validation Best Practices
Origin and referrer header validation can provide additional defense-in-depth protection, but should never be used as a standalone CSRF prevention method:
Origin Header (Preferred):
– More reliable in modern browsers for cross-origin contexts
– Consistently sent by browsers for cross-origin requests
– Less likely to be stripped by privacy tools
Referer Header (Less Reliable):
– More prone to being stripped or modified by privacy tools
– Inconsistent behavior across older browsers
– May be missing due to HTTPS-to-HTTP transitions
Here is an example of how to use the preferred method, using the Origin header, and the Referrer header as the backup mechanism:
function validateRequestOrigin(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const origin = req.get('Origin');
const referer = req.get('Referer');
const allowedOrigins = [
'https://yourapp.com',
'https://www.yourapp.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null
].filter(Boolean);
// Prefer Origin header validation
if (origin && !allowedOrigins.includes(origin)) {
return res.status(403).json({ error: 'Request origin not allowed' });
}
// Fallback to Referer header (less reliable)
if (!origin && referer) {
const isValidReferer = allowedOrigins.some(allowed =>
referer.startsWith(allowed)
);
if (!isValidReferer) {
return res.status(403).json({ error: 'Request referer not allowed' });
}
}
if (!origin && !referer) {
console.warn('Request with no Origin or Referer header - may be legitimate privacy protection');
}
next();
}
// Always use alongside CSRF tokens, never as standalone protection
app.use(validateRequestOrigin);
app.use(csrfProtection);
These headers can be spoofed client-side (if Cross-Origin Resource Sharing or CORS isn’t properly enforced) and may be omitted by privacy plugins, certain browsers, or cross-origin redirect chains.
Testing Your CSRF Protection
Proper testing ensures your CSRF protection works correctly and doesn’t interfere with legitimate application functionality. A comprehensive testing strategy includes both automated tests and manual verification.
Automated Testing
Platforms like StackHawk can help with automated testing of CSRF and the vulnerabilities it exposes. We will cover the particulars of how to do this a bit later. However, you can also check for issues within your code directly. For instance, you can create automated tests that verify your CSRF protection works correctly as a first-level check before moving on to more advanced testing, like with StackHawk. For example, here is a unit test that can check that a request gets accepted and rejected based on CSRF tokens:
const request = require('supertest');
const app = require('../app'); // Your Express app
describe('CSRF Protection', () => {
let agent;
let csrfToken;
beforeEach(async () => {
// Create a persistent session for testing
agent = request.agent(app);
// Get initial CSRF token
const response = await agent.get('/api/csrf-token');
csrfToken = response.body.csrfToken;
});
test('should reject POST requests without CSRF token', async () => {
const response = await agent
.post('/update-profile')
.send({ name: 'Test User' })
.expect(403);
expect(response.body.error).toMatch(/csrf/i);
});
test('should accept POST requests with valid CSRF token', async () => {
const response = await agent
.post('/update-profile')
.set('X-CSRF-Token', csrfToken)
.send({ name: 'Test User' })
.expect(200);
expect(response.body.success).toBe(true);
});
test('should reject requests with invalid CSRF token', async () => {
const response = await agent
.post('/update-profile')
.set('X-CSRF-Token', 'invalid-token')
.send({ name: 'Test User' })
.expect(403);
expect(response.body.error).toMatch(/csrf/i);
});
test('should allow GET requests without CSRF token', async () => {
const response = await agent
.get('/profile')
.expect(200);
// GET requests should not require CSRF tokens
expect(response.status).toBe(200);
});
});
Manual Testing with Browser Developer Tools
Use browser developer tools to verify CSRF protection manually:
- Network Tab Inspection: Open your browser’s Network tab and observe requests to ensure CSRF tokens are being included in form submissions and AJAX requests.
- Console Testing: Use the browser console to attempt requests without CSRF tokens:
// This should fail with CSRF protection enabled
fetch('/update-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Test' })
})
.then(response => console.log('Status:', response.status))
.catch(error => console.log('Error:', error));
- Cross-Origin Testing: Create a simple HTML file served from a different origin and attempt to make requests to your application to verify they’re properly blocked.
Security Testing with Curl
Use command-line tools to test CSRF protection:
# Test that requests without tokens are rejected
curl -X POST \
-H "Content-Type: application/json" \
-d '{"name":"Test User"}' \
http://localhost:3000/update-profile
# Test with cookies but no CSRF token (should fail)
curl -X POST \
-H "Content-Type: application/json" \
-H "Cookie: sessionid=abc123" \
-d '{"name":"Test User"}' \
http://localhost:3000/update-profile
# Test with both cookies and CSRF token (should succeed)
curl -X POST \
-H "Content-Type: application/json" \
-H "Cookie: sessionid=abc123" \
-H "X-CSRF-Token: valid-token-here" \
-d '{"name":"Test User"}' \
http://localhost:3000/update-profile
These commands help you verify that your CSRF configuration responds correctly to both valid and invalid requests.
Common Pitfalls and Solutions
Even with proper configuration, you might encounter CSRF-related issues. Understanding common pitfalls can help you troubleshoot problems quickly.
Multiple CSRF Headers: If you see duplicate CSRF headers in your responses, it means multiple layers of your infrastructure are trying to handle CSRF. This commonly happens when both your Node.js application and your web server (Nginx, Apache) or CDN are configured to add CSRF headers. Ensure that only your Node.js application is handling CSRF by checking your web server and CDN configurations.
Credentials with Wildcards: One of the most common CSRF mistakes is trying to use overly permissive CORS settings alongside CSRF protection. If your application accepts cross-origin requests, ensure your CORS configuration doesn’t inadvertently bypass CSRF protections.
Development vs Production Settings: It’s easy to accidentally deploy development CSRF settings to production, which can create security vulnerabilities. Use environment-specific configuration files and deployment processes that prevent overly permissive development configurations from reaching production.
Token Expiration Issues: CSRF tokens that expire too quickly can create usability problems, while tokens that never expire create security risks. Find the right balance for your application’s usage patterns.
Caching Problems: Sometimes CSRF problems persist even after fixing your configuration due to browser or proxy caching. When troubleshooting, try clearing browser cache or testing in an incognito/private window to ensure you’re seeing the effects of your latest configuration changes.
Are There Other Considerations?
Node.js CSRF middleware processes requests through your application framework, which means it relies on your requests actually reaching your middleware. If you’re using custom middleware that generates responses early in the request cycle, or if you have web server configurations that bypass Node.js for certain requests, ensure that CSRF headers are still being validated appropriately.
For production deployments, establish a regular review process for your CSRF configuration, especially when adding new client applications, changing domains, or modifying your application architecture. CSRF configuration that becomes overly permissive over time can create security vulnerabilities.
Additionally, consider monitoring your application logs for CSRF-related errors, which can help you identify configuration issues or potential security threats from unexpected cross-origin request attempts.
Framework Integration: If you’re using full-stack frameworks like Next.js, Nuxt.js, or SvelteKit, check if they provide built-in CSRF protection before implementing your own. These frameworks often include optimized CSRF handling that integrates seamlessly with their routing and authentication systems.
API Documentation: When building APIs that require CSRF protection, ensure your API documentation clearly explains how clients should handle CSRF tokens. Include examples for different client types (web browsers, mobile apps, server-to-server) and specify which endpoints require tokens.
Why Automated CSRF Testing Matters
CSRF vulnerabilities are among the most exploited attack vectors in web applications today. Manual testing often misses subtle implementation flaws, and it’s easy to create security gaps while attempting to balance usability with protection.
A common mistake when implementing CSRF protection is creating configurations that appear to work but have critical weaknesses. For example, using predictable token generation, failing to validate tokens properly across all endpoints, or inadvertently exposing token values in logs or URLs.
One of the most trusted security platforms for this type of testing is StackHawk. By using StackHawk to test for vulnerabilities in your Node.js applications and APIs, you get:
- Early detection of CSRF vulnerabilities during development
- Automated testing of complex attack scenarios you might not think to test manually
- Clear remediation guidance with specific examples of vulnerable endpoints
- Verification that CSRF protections don’t break legitimate functionality
By integrating CSRF security testing into your development workflow, you can ensure that your application is protected against request forgery attacks while maintaining a smooth user experience.
Getting Started with StackHawk
To make your AI-generated APIs more secure by default with StackHawk, youโll need an account. You canย sign up for a trial account. If youโre using an AI-coding assistant like Cursor or Claude Code, sign up for our $5/month single-user plan,ย Vibe, to find and fix vulnerabilities 100% in natural language.
Validating Your CSRF Configuration with StackHawk
Once you’ve implemented CSRF protection, it’s crucial to ensure your configuration is both functional and secure. A common mistake is implementing token validation that appears to work but inadvertently creates security vulnerabilities, such as using weak token generation, exposing tokens in unsafe ways, or failing to validate tokens consistently across all state-changing endpoints.
This is where automated security testing becomes invaluable. Instead of manually testing every possible CSRF attack scenario and hoping you haven’t introduced security gaps, you can use StackHawk to automatically validate that your CSRF implementation works correctly without creating new attack vectors.
While static testing might help you spot some CSRF implementation errors, you’ll need dynamic testing to ensure the runtime environment where CSRF protection operates is also free of vulnerabilities. StackHawk is a DAST tool (Dynamic Application Security Testing) that includes specific CSRF vulnerability scanners to test your implementation, including:
- Absence of Anti-CSRF Tokens: Testing if forms and state-changing endpoints are missing CSRF token protection entirely
- Anti-CSRF Tokens Scanner: Checking if your application is vulnerable to cross-site request forgery attacks despite having tokens implemented
- Unauthorized action execution: Identifying scenarios where attackers could perform actions on behalf of users without their consent
- Session hijacking vulnerabilities: Testing for CSRF attacks that could compromise user sessions and lead to account takeover
Instead of waiting for our AppSec team to notify us of a CSRF vulnerability later in development, let’s use StackHawk’s CSRF vulnerability scanners to automatically identify these issues for us. To do this, youโll need to ensure you have a StackHawk account. If you need one, you can sign up for a trial account or log into an existing account.
If youโre logging into an existing StackHawk account, from the Applications screen, youโll click Add Application.
If youโre new to StackHawk, youโll be automatically brought into the Add an App flow. On the Scanner screen, youโll see the instructions for installing the StackHawk CLI. Since we will be running our testing locally, we will use this option. Once the hawk init command is executed successfully, click the Next button.
On the next screen, you will fill out an Application Name, Environment, and URL. Once filled out, click Next.
Since we will be testing a RESTful API, on the next page, we will choose our Application Type as โDynamic Web Application/Single Page Applicationโ.
Depending on what you have set up as a backend API, youโll also plug these details in. For example, if you have a REST API running, set the API Type to โREST / OpenAPIโ and point to our OpenAPI Specification file by selecting the URL Path and entering the path to your OpenAPI spec in the text box. Once complete, click Next.
Lastly, we will need to add a stackhawk.yml file to the root of our project. Once the file is added, copy the screenโs contents, paste them into the file, and save it. Lastly, we can click the Finish button.
In our root directory, we should see the stackhawk.yml file weโve added:
Run HawkScan
Next, we can go ahead with testing our application. In a terminal pointing to the root of our project, we will run HawkScan using the following command:
hawk scan
After running the command, the tests should execute in the terminal.
Note that if you get an error similar to:
HawkScan Target Not Found Error: Unable to access https://localhost:4000. Check if the web server is listening on the specified port.
This means that your API is not running in HTTPS, and that is how HawkScan is trying to call the API. To fix this, either add HTTPS capabilities to your API or, more simply, change the host entry in your stackhawk.yml to use only “http”.
This will run tests against our Node.js application and the corresponding backend. Once the tests have run, we can begin to explore any findings that were discovered.
Explore The Initial Findings
Once the tests are complete, the terminal will contain some information about any vulnerabilities found. Below, we can see that StackHawk has found a few CSRF vulnerabilities within my code that are present on multiple paths.
To explore this further, we will click on the test link at the bottom of the output. This will take us into the StackHawk platform to explore further.
After clicking on the link, we can now view the test results in a nicely formatted display. Next, we will click on the Anti CSRF Tokens Scanner entry.
Within this entry, we can see an Overview and Resources that can help us with fixing this vulnerability, as well as the Request and Response that the API returned on the right side of the screen. Above this, you will also see a Validate button, which will display a cURL command with the exact HTTP request used to expose the vulnerability.
Understanding and Fixing CSRF Security Issues
When StackHawk identifies CSRF vulnerabilities, you’ll see detailed findings that indicate which paths are affected and provide potential remediation techniques (as shown in the image above). Using the techniques in this guide or the CSRF remediation advice provided by StackHawk for the vulnerability, you can then make the necessary adjustments to your code and configuration. Once fixed, ensure that you stop your web servers and redeploy the latest code. Next, we can ensure that the fix implemented actually resolves our CSRF misconfiguration issues.
Confirm the fix!
With the latest code deployed, letโs confirm the fix in StackHawk. To do this, we will click the “Rescan Findings” button in StackHawk.
Then, we will see a modal containing the โhawk rescanโ command that includes the correct Scan ID. Youโll run this command in the same terminal where you ran the initial set of tests.
In the output, you will again see any vulnerabilities that were found during the scan. In this case, youโll see that the CSRF vulnerabilities are no longer showing. Clicking on the link at the bottom of the terminal output, you can confirm that the CSRF vulnerabilities have now been added to the Fixed Findings from Previous Scan, confirming that the vulnerability has been successfully fixed and has passed any vulnerability tests.
With that, weโve successfully remedied and retested our application to ensure its safety from potential CSRF attacks.
Summary
Protecting Node.js applications from CSRF attacks requires a comprehensive approach that combines multiple security mechanisms. The key principles to remember are:
- Use established libraries like @dr.pogodin/csurf or csrf-csrf rather than building custom CSRF protection
- Choose the right pattern – Synchronizer Token Pattern for stateful applications, Double-Submit Cookie for stateless systems
- Layer your defenses – Combine CSRF tokens with SameSite cookies, proper HTTP methods, and origin validation
- Test thoroughly – Verify your protection works correctly across different browsers and client types
- Monitor and maintain – Regularly review your CSRF configuration and update dependencies
CSRF attacks remain one of the most common web vulnerabilities, but with the proper implementation of the techniques covered in this guide, you can effectively protect your Node.js applications and users.
If you’d like to gain an understanding of CSRF protection in frameworks other than Node.js, check out one of our other guides:
- FastAPI CSRF โ Python async framework implementation
- .NET CSRF โ Cross-platform web application security
- Spring CSRF โ Java enterprise framework protection
Want to ensure your Node application is secure from CSRF attacks and other vulnerabilities? Sign up for StackHawk today for a free 14-day trial to automatically test your CSRF configuration and detect potential vulnerabilities.