CSRF - Cross-Site Request Forgery: forcing a victim's browser to act on their behalf
CSRF (Cross-Site Request Forgery) exploits the trust a server places in requests sent by a logged-in user's browser. The attacker causes the victim, without their knowledge, to execute an action on a site where they're authenticated. The server receives an apparently legitimate request, with a valid session cookie, and processes it.
Why the browser is complicit
Browsers automatically attach a domain's cookies to all requests toward that domain, whether those requests come from the site itself or from a third-party page. This is the default behaviour, originally designed for user convenience. CSRF exploits it.
If Alice is logged in to bank.com and visits malicious.com, Alice's browser will send her bank.com cookies for any request to bank.com, even one initiated from malicious.com.
Attack examples
Attack via a GET request: If an action is triggered by a GET (bad practice but common), a simple image tag is enough. The victim doesn't need to click anything.
CSRF via img tag: no click required
<!-- Inséré dans une page ou un email HTML -->
<!-- Le navigateur charge l'image, déclenchant le virement -->
<img src="https://bank.com/transfer?to=attacker&amount=5000"
width="1" height="1" style="display:none">
Attack via a POST request: For POST actions, the attacker creates a hidden HTML form on their page that auto-submits on load. In a few milliseconds, the victim has executed the action without seeing anything.
CSRF via auto-submitting form
<!-- Page malicious.com/hack.html -->
<form id="csrf-form"
action="https://bank.com/transfer"
method="POST">
<input type="hidden" name="to" value="attacker-account">
<input type="hidden" name="amount" value="5000">
<input type="hidden" name="currency" value="EUR">
</form>
<script>document.getElementById('csrf-form').submit();</script>
More discreet vectors: The attacker can also use a hidden <iframe> pointing to a page that triggers the request, or send an HTML email loading an image from the target URL. fetch() and XMLHttpRequest cross-origin URLs are blocked by CORS, but native HTML forms and image tags are not.
What doesn't work
Checking the Referer header: The Referer can be absent (proxies, browser extensions, referrer policy settings), spoofed in some legacy contexts, or poorly validated (a check that accepts bank.com.evil.com doesn't protect). It's not a reliable standalone protection.
Using POST instead of GET: As shown above, an auto-submitting form also exploits POST requests. The HTTP method choice does not protect against CSRF.
HTTPS authentication: HTTPS protects the transport channel, not the origin of the request. CSRF works just as well over HTTPS.
Remediation
1. Synchronizer Token Pattern: This is the most robust protection. The server generates a unique random token per session (or per form), stores it server-side, and includes it in every HTML form. On submission, the server verifies the received token matches the expected one. A malicious page cannot read this token (blocked by the browser's Same-Origin Policy).
CSRF token in an HTML form
<!-- Token CSRF inclus dans le formulaire -->
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<input type="text" name="to" placeholder="Bénéficiaire">
<input type="number" name="amount" placeholder="Montant">
<button type="submit">Transférer</button>
</form>
Server-side verification: Express.js (csurf or manual implementation)
// Génération du token à l'initialisation de la session
import crypto from 'crypto';
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
}
// Vérification à la réception du formulaire
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF token invalide' });
}
// Traitement de l'action
});
For JSON APIs: If your API accepts only JSON (Content-Type: application/json), native HTML forms cannot trigger a CSRF (they can only send application/x-www-form-urlencoded or multipart/form-data). Explicitly verify the Content-Type header server-side.
2. SameSite attribute on cookies: This attribute tells the browser in which cross-site contexts it should send the cookie. It's the simplest protection to implement and the most transparent for users.
Cookie configuration with SameSite
# SameSite=Strict - le cookie n'est jamais envoyé dans un contexte cross-site
# Protection maximale, mais peut bloquer des liens légitimes depuis d'autres sites
Set-Cookie: session=abc123; SameSite=Strict; HttpOnly; Secure
# SameSite=Lax - le cookie est envoyé uniquement pour les navigations top-level (clic sur lien)
# N'est PAS envoyé pour les requêtes initiées par img, iframe, fetch cross-origin
# Bon compromis utilisabilité / sécurité - valeur par défaut dans les navigateurs récents
Set-Cookie: session=abc123; SameSite=Lax; HttpOnly; Secure
# SameSite=None - envoi dans tous les contextes (compatibilité ancienne)
# Requiert obligatoirement Secure
Set-Cookie: session=abc123; SameSite=None; Secure
SameSite=Lax is now the default value in Chrome and Firefox for cookies without an explicit attribute. This protects against the majority of common CSRF attacks. However, SameSite=Strict is recommended for sensitive applications.
3. Double Submit Cookie: Alternative to synchronised tokens, useful for stateless architectures. The server generates a random token, places it in a cookie AND in a custom header (or request body). Server-side, it verifies that the two match. A malicious page cannot read or write cookies from another domain.
Double Submit Cookie: client side
// Lire le token depuis le cookie (accessible en JS car pas HttpOnly)
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
// L'envoyer dans un header custom sur chaque requête API
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('csrf_token') // valeur lue du cookie
},
body: JSON.stringify({ to: 'beneficiary', amount: 500 })
});
4. Check the Origin header: For modern requests, checking the Origin header (or Referer as fallback) server-side is a valid additional defence layer, to combine with the previous protections, not to use alone.
Origin header check
// Middleware Express.js
app.use((req, res, next) => {
if (req.method !== 'GET' && req.method !== 'HEAD') {
const origin = req.headers.origin || req.headers.referer || '';
if (!origin.startsWith('https://myapp.com')) {
return res.status(403).json({ error: 'Origin non autorisée' });
}
}
next();
});
Protection summary
- Enable
SameSite=Strict(orLaxat minimum) on all session cookies - Implement CSRF tokens on all HTML forms that trigger state-changing actions
- For JSON APIs: explicitly verify
Content-Type: application/jsonserver-side - Never trigger state-changing actions (modification, deletion, transaction) via GET requests
- Use a framework that handles CSRF tokens automatically (Rails, Laravel, Django, Spring Security) and don't disable it
Further reading
A question after reading?
Doubts about the protection of your forms or action endpoints? Send a message.
Or book a 30-minute scoping call directly
Book a call →