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 cookiessession(): Creates a secure locker for each user's sessioncsrf(): 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)