How do you prevent CSRF in Express?
Cross-Site Request Forgery (CSRF) is a type of malicious exploit where unauthorized commands are transmitted from a user that the web application trusts. In Express.js applications, preventing CSRF is crucial to protect users from unwanted actions. This guide outlines common strategies and best practices for robust CSRF protection.
Understanding CSRF Attacks
CSRF attacks work by tricking an authenticated user's browser into sending a forged request to a web application. Since the browser automatically sends session cookies with the request, the application treats it as a legitimate request from the user, even though it was initiated maliciously. This can lead to unauthorized actions like changing passwords, transferring funds, or making purchases.
Common CSRF Prevention Strategies in Express.js
Several methods can be employed to mitigate CSRF risks, often used in combination for robust protection.
1. Synchronizer Token Pattern (using `csurf` middleware)
The Synchronizer Token Pattern is one of the most common and robust defenses against CSRF. It involves generating a unique, cryptographically secure token for each user session. This token is then embedded in HTML forms or HTTP headers for all state-changing requests. The server validates this token with every incoming request before processing the action.
Express.js provides a convenient middleware called csurf that implements this pattern by generating and validating CSRF tokens.
const express = require('express');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const csrf = require('csurf');
const app = express();
// 1. Setup essential middleware first
app.use(cookieParser());
app.use(session({
secret: 'your-secret-key-that-is-very-long-and-random', // Use a strong, unique secret
resave: false,
saveUninitialized: true,
cookie: {
secure: process.env.NODE_ENV === 'production', // Use secure cookies in production
httpOnly: true, // Prevent client-side JavaScript from accessing cookies
sameSite: 'Lax' // Essential for CSRF protection alongside tokens
}
}));
app.use(express.urlencoded({ extended: false })); // For parsing x-www-form-urlencoded data
app.use(express.json()); // For parsing JSON data
// 2. Enable CSRF protection
// csurf creates a req.csrfToken() function and validates tokens
// Tokens can be stored in the session or a cookie (cookie: true)
app.use(csrf({ cookie: true }));
// 3. CSRF error handling middleware
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
res.status(403).json({ error: 'Invalid CSRF token' });
} else {
next(err);
}
});
// Example route to get a CSRF token (e.g., for AJAX forms)
app.get('/api/csrf-token', (req, res) => {
res.json({ csrfToken: req.csrfToken() }); // Send token to client
});
// Example protected route that requires CSRF token
app.post('/api/process-data', (req, res) => {
// `csurf` middleware automatically validates the token on POST requests.
// It expects the token in req.body._csrf, req.query._csrf, or X-CSRF-TOKEN header.
res.json({ message: 'Data processed successfully with valid CSRF token.', data: req.body });
});
// Start the server
const PORT = 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
When using csurf:
- Server-Side: The
csurfmiddleware generates a_csrftoken (or a name you configure) and stores it securely, typically in the session or a cookie.req.csrfToken()is used to retrieve the current token for a request. - Client-Side: The retrieved token must be sent back with every non-GET (state-changing) request (e.g., POST, PUT, DELETE). It can be included as a hidden input field in HTML forms or as a custom HTTP header (e.g.,
X-CSRF-TOKEN) for AJAX requests.
<!-- For HTML forms -->
<form action="/api/process-data" method="POST">
<input type="hidden" name="_csrf" value="YOUR_CSRF_TOKEN_HERE"> <!-- Replace with token from server -->
<label for="item">Item:</label>
<input type="text" id="item" name="item">
<button type="submit">Add Item</button>
</form>
<!-- For AJAX requests (example using fetch) -->
<script>
async function sendProtectedData() {
// 1. Fetch the CSRF token from the server
const csrfResponse = await fetch('/api/csrf-token');
const { csrfToken } = await csrfResponse.json();
// 2. Include the token in the request (e.g., as a header or in the body)
fetch('/api/process-data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken // Common practice for AJAX/APIs
},
body: JSON.stringify({
item: 'New Product',
// Optionally, include _csrf in body for consistency with form-urlencoded
// _csrf: csrfToken
})
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
// sendProtectedData(); // Call this function to send data
</script>
2. SameSite Cookies
The SameSite attribute for cookies provides a strong layer of defense against CSRF by restricting when a browser sends cookies with cross-site requests. This helps prevent browsers from automatically attaching session cookies to requests originating from third-party sites.
Lax(recommended default): Cookies are sent with top-level navigations (e.g., clicking a link from another site to yours) and GET requests from third-party sites, but *not* with POST requests or embedded content from third-party sites. This offers a good balance of security and usability, protecting against common CSRF vectors while allowing some legitimate cross-site linking.Strict: Cookies are *only* sent for same-site requests. This provides the strongest CSRF protection but can break functionality for legitimate cross-site links (e.g., if an external site links to your site and requires immediate authentication).None: Cookies are sent with all cross-site requests, but *only* if the cookie is also markedSecure(i.e., only sent over HTTPS). This option explicitly allows cross-site requests and offers no CSRF protection on its own. It's used for services that explicitly need cross-site cookie functionality (e.g., embedded widgets, federated logins).
app.use(session({
secret: 'your-secret-key-that-is-very-long-and-random',
resave: false,
saveUninitialized: true,
cookie: {
secure: process.env.NODE_ENV === 'production', // Use secure cookies in production
httpOnly: true, // Prevent client-side JavaScript from accessing cookies
sameSite: 'Lax' // Or 'Strict' for stronger protection
}
}));
While SameSite cookies are valuable, they should not be relied upon as the sole defense, especially against more sophisticated attacks or when older browser versions (which might not fully support SameSite) are in use. They work best in conjunction with token-based methods.
3. Double Submit Cookie Pattern
This pattern involves sending a CSRF token in two places: a cookie and a hidden form field or a custom HTTP header. The server verifies that both values match. The primary advantage is that it doesn't require server-side state (like sessions) to store the token, making it suitable for stateless APIs. csurf can be configured to use a cookie for token storage, which aligns closely with this pattern, effectively achieving similar protection.
Best Practices for CSRF Prevention
- Combine Methods: Always use a robust token-based solution (like
csurf) in conjunction withSameSite=LaxorStrictfor session cookies. This layered approach provides the strongest defense. - HTTPS Everywhere: Ensure your entire application uses HTTPS to protect tokens and cookies during transit from eavesdropping and man-in-the-middle attacks.
- Stateless Token Delivery (for APIs): For RESTful APIs, sending CSRF tokens in custom HTTP headers (e.g.,
X-CSRF-TOKEN) is often preferred over form fields. Clients retrieve the token from an initial GET request and include it in subsequent POST/PUT/DELETE requests. - Token Expiration: While
csurfoften ties tokens to session expiration, implement or ensure token expiration and regeneration for enhanced security. - Proper CORS Configuration: If your Express API is consumed by different origins, ensure your Cross-Origin Resource Sharing (CORS) policy is configured correctly to prevent unintended access and potential bypasses.
- Avoid GET for State Changes: Never use GET requests to perform actions that modify server-side state. GET requests should always be idempotent and safe, as they are inherently vulnerable to CSRF.
- Strong Session Management: Implement secure session management practices, including strong session secrets, reasonable session lifetimes, and proper invalidation upon logout.