CSRF Protection — Complete Implementation Guide
In this tutorial, you will learn about CSRF Protection. We cover key concepts, practical examples, and best practices to help you master this topic.
Cross-Site Request Forgery (CSRF) is an attack that tricks an authenticated user into executing unwanted actions on a web application where they are currently authenticated, and CSRF protection prevents this by ensuring that requests originate from the legitimate application.
What You'll Learn
By the end of this lesson, you will implement CSRF protection using synchronized tokens, double-submit cookies, and SameSite cookies, understand when CSRF protection is needed, and secure both traditional web apps and APIs.
Why It Matters
CSRF Attacks exploit the browser's automatic inclusion of cookies with every request. Without CSRF protection, an attacker can forge requests on behalf of authenticated users. Doda Browser's web interface uses CSRF tokens on all state-changing requests to protect user accounts.
Real-World Use
A user is logged into their banking website. In another tab, they visit a malicious site that contains a hidden form submitting a money transfer request to the bank's API. Since the user's session cookie is automatically included, the transfer goes through. CSRF protection would block this because the malicious form cannot include the CSRF token.
CSRF Attack Flow
sequenceDiagram
participant User
participant Bank as Banking App
participant Attacker as Malicious Site
User->>Bank: Login
Bank-->>User: Session cookie set
User->>Attacker: Browse malicious site
Attacker->>User: Hidden form targeting Bank
User->>User: Auto-submit form
User->>Bank: POST /transfer (with session cookie)
Note over Bank: No CSRF check - request processed!
Bank-->>User: Transfer completed (unauthorized)
Synchronized Token Pattern
const express = require("express");
const crypto = require("crypto");
const cookieParser = require("cookie-parser");
const app = express();
app.use(express.json());
app.use(cookieParser());
const TOKEN_SECRET = crypto.randomBytes(32).toString("hex");
function generateCsrfToken() {
const token = crypto.randomBytes(32).toString("hex");
const hmac = crypto.createHmac("sha256", TOKEN_SECRET)
.update(token).digest("hex");
return `${token}.${hmac}`;
}
function verifyCsrfToken(token) {
const [randomPart, signature] = token.split(".");
const expected = crypto.createHmac("sha256", TOKEN_SECRET)
.update(randomPart).digest("hex");
return signature === expected;
}
app.get("/api/csrf-token", (req, res) => {
const token = generateCsrfToken();
res.cookie("csrf-token", token, {
httpOnly: false, // JavaScript needs to read this
secure: true,
sameSite: "strict",
});
res.json({ csrfToken: token });
});
function csrfProtection(req, res, next) {
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
return next();
}
const headerToken = req.headers["x-csrf-token"];
const cookieToken = req.cookies["csrf-token"];
if (!headerToken || !cookieToken) {
console.log(`[CSRF] Missing token`);
return res.status(403).json({ error: "CSRF token required" });
}
if (headerToken !== cookieToken) {
console.log(`[CSRF] Token mismatch`);
return res.status(403).json({ error: "CSRF token mismatch" });
}
if (!verifyCsrfToken(headerToken)) {
console.log(`[CSRF] Invalid token signature`);
return res.status(403).json({ error: "Invalid CSRF token" });
}
next();
}
app.post("/api/transfer", csrfProtection, (req, res) => {
const { amount, to } = req.body;
console.log(`[Transfer] Processing: ${amount} to ${to}`);
res.json({ message: "Transfer processed" });
});
app.listen(3000);
CSRF Protection with SameSite Cookies
// Modern CSRF protection using SameSite cookies
// SameSite=Strict prevents the cookie from being sent on cross-origin requests
app.use(session({
cookie: {
httpOnly: true,
secure: true,
sameSite: "strict", // Built-in CSRF protection
maxAge: 24 * 60 * 60 * 1000,
},
}));
// For cases where SameSite is not sufficient:
// - POST forms from same-site but different origins
// - Legacy browsers that don't support SameSite
function doubleSubmitCookie(req, res, next) {
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
return next();
}
const cookieValue = req.cookies["csrf-double"];
const headerValue = req.headers["x-csrf-token"];
if (!cookieValue || !headerValue) {
return res.status(403).json({ error: "CSRF tokens missing" });
}
if (cookieValue !== headerValue) {
return res.status(403).json({ error: "CSRF tokens don't match" });
}
next();
}
Django CSRF Protection
# Django has built-in CSRF protection
# settings.py
MIDDLEWARE = [
"django.middleware.csrf.CsrfViewMiddleware",
# ...
]
# In templates, include the CSRF token:
# <form method="POST">
# {% csrf_token %}
# ...
# </form>
# For Django REST Framework:
from rest_framework.views import APIView
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
@method_decorator(ensure_csrf_cookie, name="dispatch")
class CSRFView(APIView):
def get(self, request):
return Response({"csrfToken": self.get_csrf_token(request)})
def get_csrf_token(self, request):
from django.middleware.csrf import get_token
return get_token(request)
# For AJAX requests, include the CSRF token:
# const csrfToken = document.cookie
# .split('; ')
# .find(row => row.startsWith('csrftoken'))
# .split('=')[1];
# fetch('/api/data', {
# method: 'POST',
# headers: {
# 'X-CSRFToken': csrfToken,
# },
# credentials: 'include',
# });
Common Mistakes
- Disabling CSRF protection for API endpoints that use cookie authentication.
- Using GET requests for state-changing operations (CSRF via image tags).
- Not protecting CSRF token endpoints with authentication (anyone can get a token).
- Using the same CSRF token for the entire session instead of per-request tokens.
- Not including CSRF protection on Websocket connections.
- Relying solely on CORS for CSRF protection (CORS blocks reading responses, not sending requests).
Practice Questions
- How does SameSite=Strict prevent CSRF attacks?
SameSite=Strict tells the browser to never send the cookie with cross-origin requests. Since CSRF attacks rely on the browser automatically including cookies, SameSite prevents the cookie from being sent to the attacker's forged request.
- What is the difference between synchronized token and double-submit cookie?
Synchronized token: Server generates a token, stores it in session, and requires it in requests. Double-submit: Server sets a cookie with a random value, and the client must send the same value in a header. No server-side storage needed.
- Why do APIs not need CSRF protection when using token-based auth?
CSRF attacks exploit automatic cookie inclusion. Token-based auth (Bearer tokens in Authorization header) is not automatically included by the browser. The attacker's malicious site cannot read the token to include it.
- Challenge: Implement CSRF protection for a traditional session-based web app with per-request tokens, token rotation after each state-changing request, defense against token leakage via Referer headers, and support for mobile API clients that cannot read cookies.
FAQ
Mini Project: CSRF Testing Tool
Build a CLI tool that tests web applications for CSRF vulnerabilities by submitting forged requests without CSRF tokens.
import requests
import sys
class CSRFScanner:
def __init__(self, base_url):
self.base_url = base_url
self.session = requests.Session()
def check_csrf_on_form(self, form_url, form_data, action_url=None):
"""Test if a form submission is protected by CSRF."""
print(f"Testing CSRF protection on {form_url}")
initial = self.session.get(form_url)
csrf_token = self._extract_csrf_token(initial.text)
if csrf_token:
print(f" CSRF token found: {csrf_token[:20]}...")
else:
print(f" WARNING: No CSRF token found in form!")
target = action_url or form_url
result = self.session.post(target, data=form_data)
if result.status_code in (200, 201, 302):
print(f" Request succeeded (status {result.status_code})")
if not csrf_token:
print(f" VULNERABLE: No CSRF protection detected!")
return False
elif result.status_code in (403, 401):
print(f" Request blocked (status {result.status_code})")
print(f" CSRF protection present")
return True
else:
print(f" Unknown status: {result.status_code}")
return True
def _extract_csrf_token(self, html):
import re
patterns = [
r'name="csrf_token" value="([^"]+)"',
r'name="csrfmiddlewaretoken" value="([^"]+)"',
r'csrf-token[^>]*content="([^"]+)"',
r'X-CSRF-Token[^>]*content="([^"]+)"',
]
for pattern in patterns:
match = re.search(pattern, html)
if match:
return match.group(1)
return None
scanner = CSRFScanner("http://localhost:3000")
scanner.check_csrf_on_form("/login", {"email": "test@test.com", "password": "test"})
What's Next
Learn about auth middleware for backend request authentication, then explore security headers for web application security.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro