Mappa CSRF#
CSRF (Cross-Site Request Forgery) forza il browser di un utente autenticato a inviare richieste non intenzionali verso un sito su cui e' loggato. Il principio e' che i browser inviano automaticamente i cookie di sessione a ogni richiesta verso un dominio, indipendentemente da quale pagina ha originato la richiesta. L'attaccante non ha bisogno di vedere la risposta: gli basta che l'azione (un trasferimento, un cambio password, un'azione amministrativa) venga eseguita.

Il meccanismo CSRF#
Per capire CSRF bisogna capire come funzionano i cookie. Quando sei loggato su banca.com, il server ha impostato un cookie di sessione nel browser: Set-Cookie: session=abc123. Da quel momento in poi, il browser invia quel cookie a ogni richiesta verso banca.com — che la richiesta provenga dalla pagina di banca.com stessa, da un link su un altro sito, o da un form su una pagina malevola. Il browser non distingue l'origine della richiesta.
L'attaccante costruisce una pagina che invia una richiesta a banca.com a insaputa della vittima:
<!-- Pagina su evil.com -->
<form action="https://banca.com/bonifico" method="POST" style="display:none">
<input name="importo" value="5000">
<input name="iban" value="IT_ATTACCANTE_IBAN_123">
</form>
<script>document.forms[0].submit()</script>Quando la vittima (loggata su banca.com) visita evil.com, il form si invia automaticamente. Il browser allega il cookie di sessione alla POST verso banca.com. Il server riceve una richiesta autenticata con cookie valido e processa il bonifico. La vittima non ha cliccato nulla.
Dev parallel: Questo spiega perche' le applicazioni web tradizionali (con sessioni basate su cookie) hanno sempre incluso CSRF protection nei form. In Symfony, il Form Component include automaticamente un token CSRF in ogni form: ogni
$form->isValid()include implicitamente la verifica del token. Se hai mai visto il campo nascosto_tokennei form Symfony, quello e' il CSRF token. In Laravel lo stesso campo si chiama@csrf. Se sviluppi con form HTML custom senza usare il form builder, devi aggiungere e verificare il token manualmente.
CSRF Token - Difesa classica#
Il CSRF token e' un valore casuale generato dal server, incluso nel form come campo nascosto, e verificato dal server quando il form viene inviato. Il sito malevolo non puo' leggere il valore del token perche' vive in un'altra origin (Same Origin Policy impedisce la lettura cross-origin del DOM).
<!-- Form legittimo con CSRF token -->
<form action="/bonifico" method="POST">
<input type="hidden" name="csrf_token" value="a8f9f2c3d4e5f6b7">
<input name="importo" value="100">
<button type="submit">Conferma</button>
</form>Il server verifica che il token nel form corrisponda al token generato per quella sessione. Se non coincide, la richiesta viene rifiutata. Un sito malevolo non puo' costruire un form con il token corretto perche' non puo' leggere il token dalla pagina di banca.com (Same Origin Policy).
Dev parallel: In Symfony con il Security Component, il CSRF e' gestito automaticamente per i form creati con il Form Component. Per form custom o API, usa
csrf_token()nel template e$csrfTokenManager->isTokenValid()nel controller. In Express/Node, il packagecsrf-csrf(successor dicsurf) genera e verifica i token. La configurazione richiede che i cookie siano abilitati e che il middleware sia applicato prima delle route che lo richiedono.
SameSite Cookies - Difesa moderna#
L'attributo SameSite nei cookie e' la difesa moderna contro CSRF. Dice al browser quando deve o non deve allegare il cookie nelle richieste cross-site. Non richiede logica applicativa aggiuntiva: il browser gestisce tutto.
Tre valori possibili:
| Valore | Comportamento | Protezione CSRF |
|---|---|---|
Strict | cookie MAI inviato in request cross-site | Completa — anche i link da siti esterni non mandano il cookie |
Lax | cookie inviato solo su top-level navigation GET | Parziale — protegge da POST/PUT/DELETE cross-site, non da link |
None | cookie sempre inviato (richiede anche Secure) | Nessuna — comportamento pre-SameSite |
SameSite=Strict e' la protezione piu' forte. Il cookie non viene mai inviato in richieste originate da domini diversi. Lo svantaggio pratico: se un utente clicca un link a banca.com da una email o da un altro sito, il cookie non viene inviato — l'utente risulta non autenticato e deve fare login. Per siti in cui l'esperienza di "tornare autenticati da un link esterno" e' importante, si usa Lax.
SameSite=Lax e' il default nei browser moderni (Chrome, Firefox) per i cookie che non specificano il valore. Protegge da form POST cross-site (il caso CSRF classico) ma permette al cookie di viaggiare su link GET (top-level navigation).
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600Dev parallel: In PHP,
session_set_cookie_params(['samesite' => 'Strict'])prima disession_start(). In Express/Node, quando configuriexpress-session:cookie: { sameSite: 'strict', httpOnly: true, secure: true }. In Symfony,framework.yaml:session.cookie_samesite: strict. Nota pratica: se stai sviluppando in locale su HTTP,SameSite=Nonerichiede ancheSecure, che richiede HTTPS. Per lo sviluppo locale, usaSameSite=Laxo un proxy HTTPS locale.
Double Submit Cookie#
Pattern stateless alternativo al CSRF token classico. Il server genera un token, lo mette sia nel cookie che nel form field. Al submit, verifica che i due valori coincidano. Non richiede stato server-side (nessun token da memorizzare nella sessione).
Cookie: csrf_token=x8f9f2c3
Form field: <input type="hidden" name="csrf" value="x8f9f2c3">
Server verifica: cookie.csrf_token === form.csrfFunziona perche' il sito malevolo non puo' leggere il cookie di banca.com (Same Origin Policy), quindi non puo' conoscere il valore da includere nel form field.
API REST - Immunita' naturale#
Le API REST che usano Authorization: Bearer <token> nel header HTTP sono naturalmente immuni da CSRF. I browser non inviano automaticamente header custom nelle richieste cross-site: inviano automaticamente i cookie, ma non header arbitrari come Authorization.
Questo e' il motivo per cui le SPA (Single Page Application) con JWT in header non hanno bisogno di CSRF protection per le loro API:
# Browser invia automaticamente in richieste cross-site:
Cookie: session=abc123 ← CSRF vulnerabile senza protezione
# Browser NON invia automaticamente in richieste cross-site:
Authorization: Bearer eyJ... ← CSRF immuneDev parallel: Questo e' il vantaggio pratico delle API stateless con JWT rispetto alle sessioni con cookie. In NestJS con
@nestjs/passporte strategia JWT in header, non serve nessun CSRF middleware. Ma attenzione: se per qualsiasi motivo decidi di mettere il JWT in un cookie (ad esempio per impedire XSS che lo ruba dalocalStorage), hai riintrodotto il vettore CSRF e devi reimplementare SameSite + CSRF token. La regola pratica: cookie di sessione = CSRF protection obbligatoria su POST/PUT/DELETE. JWT inAuthorizationheader = immune da CSRF, ma potenzialmente esposto a XSS se inlocalStorage.
Relazione CSRF e XSS#
CSRF e XSS sono attacchi distinti ma spesso combinati:
- CSRF puro: l'attaccante non ha bisogno di iniettare codice nel sito legittimo. Gli basta una pagina esterna con un form nascosto.
- XSS per bypassare CSRF: se un sito ha CSRF protection ma e' vulnerabile a XSS, l'attaccante usa il payload XSS per leggere il CSRF token dal DOM e costruire la richiesta malevola con il token corretto — bypassando la protezione.
XSS payload che bypassa CSRF:
fetch('/profile')
.then(r => r.text())
.then(html => {
const token = html.match(/csrf_token.*?value="(.*?)"/)[1];
return fetch('/change-email', {
method: 'POST',
body: `email=attacker@evil.com&csrf_token=${token}`
});
});CSRF non ruba nulla e non inietta codice: INDUCE la vittima a compiere un'azione. XSS inietta script nel browser vittima. Spesso usati insieme: XSS per rubare il token CSRF. "Steal credentials" o "inject script" = XSS. "Force authenticated action" o "unauthorized transfer" = CSRF.
Tre segnali che indicano CSRF: (1) la vittima e' autenticata da qualche parte, (2) viene visitata una pagina esterna, (3) avviene un'azione non voluta sul sito autenticato. Se tutti e tre sono presenti = CSRF.


