CSRF Protection in MPAs — Preventing Cross-Site Request Forgery
In this tutorial, you will learn about CSRF Protection in MPAs. We cover key concepts, practical examples, and best practices to help you master this topic.
CSRF protection in MPAs uses synchronized tokens, SameSite cookies, and double-submit patterns to prevent attackers from forging authenticated state-changing requests on behalf of users.
What You'll Learn
By the end of this tutorial, you will understand what CSRF Attacks are, how they exploit session cookies, and how to prevent them with synchronized CSRF tokens, SameSite cookie attributes, double-submit cookie patterns, and custom request headers.
Why It Matters
CSRF is one of the most common web application vulnerabilities. Without protection, an attacker can trick authenticated users into performing actions like changing passwords, transferring money, or placing orders simply by visiting a malicious website. CSRF protection is essential for any MPA that handles state-changing requests.
Real-World Use
In 2008, a major social network suffered a CSRF attack where attackers embedded a hidden request on a third-party site. When authenticated users visited the malicious page, their browsers sent requests to change account settings without the user's knowledge. After implementing synchronized CSRF tokens, such attacks were prevented.
CSRF Attack vs Protection
┌──────────────────────────────────────────────────────────┐
│ CSRF Attack Without Protection │
├──────────────────────────────────────────────────────────┤
│ │
│ 1. User logs into bank.example.com │
│ Browser stores session cookie │
│ │
│ 2. User visits attacker.com (in another tab) │
│ │
│ 3. Attacker's page contains: │
│ <img src="bank.example.com/transfer?to=attacker& │
│ amount=1000"> │
│ │
│ 4. Browser sends request with bank's session cookie │
│ Server thinks user authorized the transfer │
│ │
│ ─── With CSRF Token Protection ────────────────────── │
│ │
│ 1. Form has hidden <input name="_csrf" value="xyz"> │
│ 2. Attacker cannot read the token (same-origin policy) │
│ 3. Request without valid token is rejected (403) │
│ │
└──────────────────────────────────────────────────────────┘
Think of CSRF like a forged signature. An attacker tricks you into signing a document you have not read. The bank (server) sees your signature (session cookie) and thinks you authorized the Transaction. CSRF tokens are like requiring a unique verification code that changes with each request — the attacker cannot guess it.
Synchronized Token Pattern
// Server-side CSRF token generation and validation
const crypto = require('crypto');
class CSRFProtection {
constructor() {
this.tokenLength = 32;
}
// Generate a new CSRF token
generateToken() {
return crypto.randomBytes(this.tokenLength).toString('hex');
}
// Store token in session and provide to form
getToken(req) {
if (!req.session.csrfToken) {
req.session.csrfToken = this.generateToken();
}
return req.session.csrfToken;
}
// Middleware to validate CSRF token on POST requests
validate(req, res, next) {
// Skip validation for safe methods
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const token = req.body._csrf ||
req.headers['x-csrf-token'] ||
req.headers['xsrf-token'];
const storedToken = req.session.csrfToken;
if (!token || !storedToken || token !== storedToken) {
return res.status(403).render('error', {
title: 'Request Blocked',
message: 'Invalid or expired form token. Please refresh the page and try again.',
errorCode: 'CSRF_TOKEN_INVALID'
});
}
// Regenerate token after use to prevent reuse
req.session.csrfToken = this.generateToken();
next();
}
// Rotate token on login
rotateOnLogin(req) {
req.session.csrfToken = this.generateToken();
}
}
const csrf = new CSRFProtection();
// In templates:
// <form method="POST" action="/transfer">
// <input type="hidden" name="_csrf" value="<%= csrfToken %>">
// <input name="amount" type="number">
// <input name="toAccount" type="text">
// <button type="submit">Transfer</button>
// </form>
SameSite Cookie Attribute
// SameSite cookie configuration
const sessionConfig = {
cookie: {
httpOnly: true,
secure: true,
// SameSite=Strict: Cookie sent only for same-site requests
// Most secure, but may break legitimate cross-site flows
sameSite: 'strict',
// SameSite=Lax: Cookie sent for top-level navigation
// Good balance of security and usability
// sameSite: 'lax',
// SameSite=None: Cookie sent for cross-site requests
// Must also set Secure=true
// Only use when necessary (e.g., embedded widgets)
// sameSite: 'none'
}
};
// SameSite=Lax is the recommended default for most MPAs
// It allows:
// - Same-site requests (user clicks link on your site) ✓
// - Top-level navigation (user clicks link from Google) ✓
// - GET requests from other sites ✓
//
// It blocks:
// - POST requests from other sites ✗
// - Requests from embedded images/iframes ✗
// - fetch()/XMLHttpRequest from other origins ✗
// Test SameSite behavior
console.log('Cookie configuration:');
console.log(' SameSite: strict');
console.log(' Secure: true');
console.log(' HttpOnly: true');
console.log('');
console.log('Attack scenario:');
console.log(' POST /api/transfer from attacker.com');
console.log(' Cookie sent? No (blocked by SameSite=Strict)');
Double-Submit Cookie Pattern
// Double-submit cookie — send token in cookie AND request body
class DoubleSubmitCSRF {
constructor() {
this.cookieName = 'csrf-token';
}
// Set CSRF cookie (separate from session cookie)
setCookie(req, res) {
if (!req.cookies[this.cookieName]) {
const token = crypto.randomBytes(32).toString('hex');
res.cookie(this.cookieName, token, {
httpOnly: false, // JavaScript needs to read it
secure: true,
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000
});
}
}
// Validate that cookie value matches header/body value
validate(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const cookieToken = req.cookies[this.cookieName];
const bodyToken = req.body._csrf || req.headers['x-csrf-token'];
if (!cookieToken || !bodyToken || cookieToken !== bodyToken) {
return res.status(403).json({
error: 'CSRF token validation failed'
});
}
next();
}
}
// JavaScript reads cookie and sets it in request header
// <script>
// const csrfToken = document.cookie
// .split('; ')
// .find(c => c.startsWith('csrf-token='))
// .split('=')[1];
//
// document.querySelector('form').addEventListener('submit', (e) => {
// const input = document.createElement('input');
// input.type = 'hidden';
// input.name = '_csrf';
// input.value = csrfToken;
// e.target.appendChild(input);
// });
// </script>
Common Mistakes
- Relying only on SameSite cookies. Older browsers do not support SameSite. Always implement token-based CSRF protection as the primary defense and use SameSite as defense-in-depth.
- Exposing CSRF tokens in URLs. GET requests should not be state-changing. If a CSRF token appears in a URL, it can leak via Referer header or browser history.
- Not rotating tokens after use. A token used once should be regenerated. Otherwise, an attacker who intercepts a token can reuse it for multiple requests.
- Using GET for state-changing operations. Actions that change data (delete, transfer, update) must use POST, PUT, or DELETE. GET requests should be idempotent.
- Not protecting API endpoints. CSRF protection is not just for HTML forms. API endpoints that accept cookies for authentication also need CSRF protection.
Practice Questions
- How does a CSRF attack work against an MPA?
- What is the difference between SameSite=Strict and SameSite=Lax?
- How does the synchronized token pattern protect against CSRF?
- What is the double-submit cookie pattern and when would you use it?
- Why should CSRF tokens be rotated after each use?
Challenge: Build a CSRF-protected MPA with three layers of defense: synchronized CSRF tokens on all forms, SameSite=Strict cookies, and a Content Security Policy that restricts form actions. Test the protection by creating a malicious page that tries to submit forged requests and verify they are blocked.
FAQ
Mini Project
Build an MPA with a money transfer form protected against CSRF: synchronized token pattern with server-side generation and validation, SameSite=Strict session cookies, double-submit cookie pattern as additional layer, CSRF token rotation after each form submission, and a test page that attempts CSRF from a different origin to verify protection works.
What's Next
You understand CSRF protection. Now learn about MPA SEO to understand why MPAs naturally excel at search engine optimization.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro