Un attacco CSRF (Cross-Site Request Forgery) sfrutta la sessione attiva di un utente per eseguire azioni che l’utente non ha mai voluto: cambiare la password, trasferire denaro, eliminare un account. Il browser allega automaticamente i cookie di sessione a ogni richiesta verso il tuo dominio, anche quando la richiesta parte da un sito malevolo. Senza una protezione CSRF, la tua applicazione Node.js non riesce a distinguere una richiesta legittima da una falsificata.
Questo tutorial ti guida in 12 step concreti verso una protezione CSRF solida su Express 5, usando il pattern Signed Double Submit Cookie con il pacchetto csrf-csrf. Tempo stimato: circa 30 minuti. Alla fine avrai un progetto completo e funzionante, con form HTML protetti, una variante per SPA (React, Vue, Angular), gestione degli errori, test di attacco simulato e una checklist di difese a strati allineata alla OWASP CSRF Prevention Cheat Sheet 2025.
Cos’è un attacco CSRF e perché colpisce ogni app Node.js
Un attacco CSRF funziona perché l’autenticazione basata su cookie è ambientale. Quando ti autentichi su banca.it, il server imposta un cookie di sessione. Da quel momento il browser invia quel cookie a banca.it in ogni richiesta, indipendentemente da quale pagina abbia originato la richiesta. Se visiti una pagina malevola mentre la tua sessione su banca.it è ancora valida, quella pagina può inviare di nascosto una richiesta POST verso banca.it e il cookie viaggia insieme alla richiesta.
Ecco la versione più semplice dell’attacco. Una pagina ostile contiene un form nascosto che si invia da solo al caricamento:
<!-- Pagina malevola: evil.example/regalo.html -->
<body onload="document.forms[0].submit()">
<form action="https://banca.it/trasferisci" method="POST">
<input type="hidden" name="importo" value="5000">
<input type="hidden" name="destinatario" value="IT60-CONTO-ATTACCANTE">
</form>
</body>
La vittima non clicca nulla. Il form si invia automaticamente, il browser allega il cookie di sessione e il server, senza una protezione CSRF, esegue il trasferimento come se fosse legittimo. Lo stesso schema si realizza con un tag <img> per le richieste GET non protette, motivo per cui le richieste GET non devono mai modificare lo stato del server.
La difesa fondamentale è un segreto che il sito malevolo non può conoscere né leggere: un token CSRF imprevedibile, legato alla sessione, che deve accompagnare ogni richiesta che cambia stato (POST, PUT, PATCH, DELETE). Il browser invia automaticamente i cookie, ma non può fabbricare un token valido per un dominio diverso dal proprio, e la Same-Origin Policy impedisce a uno script cross-site di leggere il token. Su questo principio si basa l’intero tutorial.
Prerequisiti e versioni richieste
Prima di scrivere codice, allinea l’ambiente. Le versioni qui sotto sono lo standard di riferimento a giugno 2026. Usa una versione LTS di Node.js: le versioni dispari non ricevono supporto a lungo termine e non vanno in produzione.
| Componente | Versione consigliata (2026) | Ruolo nel progetto |
|---|---|---|
| Node.js | 22 LTS (o 24 LTS) | Runtime; modulo crypto nativo per HMAC |
| Express | 5.x | Framework HTTP e middleware |
| csrf-csrf | 4.x | Pattern Signed Double Submit Cookie |
| cookie-parser | 1.4.x | Lettura e firma dei cookie |
| express-session (opzionale) | 1.18.x | Sessioni lato server, se usate |
| helmet (consigliato) | 8.x | Header di sicurezza HTTP |
| npm | 10.x o superiore | Gestione dipendenze |
Verifica subito la versione di Node con node -v. Se è inferiore alla 22, aggiorna prima di proseguire: Express 5 richiede Node 18 o superiore e diverse correzioni di sicurezza sui cookie sono arrivate solo nelle versioni LTS recenti. Una conoscenza di base di JavaScript asincrono (Promise, async/await) e del modello richiesta/risposta di Express è data per scontata.
Una nota importante sul pacchetto storico csurf: è deprecato e abbandonato dal 2020. Non riceve aggiornamenti, non gestisce correttamente l’attributo SameSite moderno e ha problemi noti. Non usarlo in nessun progetto nuovo. Il sostituto mantenuto è csrf-csrf, che useremo qui.
I tre pattern di protezione CSRF a confronto
Esistono tre famiglie di difesa contro gli attacchi CSRF, più l’attributo cookie SameSite come barriera complementare. Capire le differenze ti permette di scegliere il pattern giusto per la tua architettura. Le applicazioni stateful con sessioni lato server tendono al Synchronizer Token Pattern; le API stateless e le SPA preferiscono il Double Submit Cookie firmato.
| Pattern | Come funziona | Stato lato server | Caso d’uso ideale | Debolezza |
|---|---|---|---|---|
| Synchronizer Token (STP) | Token unico per sessione salvato sul server, inserito in un campo nascosto o header | Sì, richiede storage | App stateful con rendering server-side | Più memoria e gestione dello stato |
| Double Submit Cookie | Token in un cookie e replicato nel body o header; il server confronta i due valori | No | API stateless e SPA | Vulnerabile se il token non è firmato |
| Signed Double Submit | Come sopra ma il token è firmato con HMAC e un segreto server | No | SPA e microservizi moderni | Resta sensibile all’XSS |
| SameSite cookie | Il browser limita l’invio del cookie nelle richieste cross-site | No | Difesa di base per ogni app | Supporto e comportamento variabili sui browser legacy |
In questo tutorial implementiamo il Signed Double Submit Cookie tramite csrf-csrf. È il compromesso migliore per le applicazioni Node.js moderne: non richiede storage di sessione dedicato, funziona per form tradizionali e SPA, e la firma HMAC impedisce a un attaccante di fabbricare un token valido anche conoscendo l’algoritmo. Lo abbineremo a SameSite, alla validazione dell’header Origin e a HTTPS per ottenere una difesa a strati, esattamente come raccomanda OWASP.
Step 1-2: Inizializzare il progetto Express 5
Crea la cartella del progetto e inizializza npm. Useremo i moduli ES ("type": "module") perché sono lo standard per i progetti Node.js nel 2026.
mkdir csrf-demo && cd csrf-demo
npm init -y
npm pkg set type="module"
npm install express csrf-csrf cookie-parser helmet
npm install --save-dev nodemon
Aggiungi uno script di avvio nel package.json per ricaricare il server a ogni modifica durante lo sviluppo:
// package.json (estratto)
"scripts": {
"dev": "nodemon app.js",
"start": "node app.js"
}
Crea ora app.js con uno scheletro Express 5 minimo. Express 5 introduce una gestione degli errori più rigorosa e una migliore propagazione delle Promise rifiutate, quindi il codice asincrono è più sicuro per impostazione predefinita rispetto a Express 4.
// app.js
import express from "express";
import helmet from "helmet";
import cookieParser from "cookie-parser";
const app = express();
app.use(helmet());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server in ascolto su http://localhost:${PORT}`);
});
Avvia con npm run dev. Dovresti vedere il messaggio di ascolto sulla porta 3000. Questo è il punto di partenza: un server che al momento non ha alcuna protezione CSRF. Nei prossimi step la aggiungiamo.
Step 3-4: Configurare cookie-parser e il segreto CSRF
Il pattern Signed Double Submit ha bisogno di due cose: la capacità di leggere e scrivere cookie, e un segreto HMAC stabile con cui firmare i token. Registra cookie-parser prima della protezione CSRF, perché il middleware CSRF legge il token dal cookie.
// app.js (aggiunte)
import crypto from "node:crypto";
// cookie-parser con un segreto per i cookie firmati
const COOKIE_SECRET = process.env.COOKIE_SECRET ||
crypto.randomBytes(32).toString("hex");
app.use(cookieParser(COOKIE_SECRET));
Il segreto CSRF non deve mai essere scritto in chiaro nel codice. Generane uno forte (almeno 32 byte, 256 bit) e caricalo da una variabile d’ambiente. In sviluppo va bene generarlo a runtime, ma in produzione il segreto deve essere fisso e persistente, altrimenti tutti i token diventano invalidi a ogni riavvio del server.
# Genera un segreto CSRF robusto da terminale
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Esempio di output:
# 9f2c1a7b4e8d0c63a5f1b9d72e4c8a0f6b3d51e9c7a2f48b1d6e0a93c5f7b284
Salva il valore generato in un file .env (mai versionato in git) come CSRF_SECRET e COOKIE_SECRET. In produzione, usa il gestore di segreti del tuo provider (ad esempio le variabili d’ambiente del servizio o un vault dedicato). Un segreto debole o hardcoded è uno degli errori più comuni che vanifica l’intera protezione CSRF.
Step 5-6: Installare e configurare csrf-csrf
Ora il cuore della protezione. La funzione doubleCsrf di csrf-csrf restituisce un middleware e una funzione per generare il token. Configuriamola con un segreto HMAC, le opzioni del cookie e la logica di estrazione del token dalla richiesta.
// csrf.js
import { doubleCsrf } from "csrf-csrf";
const isProd = process.env.NODE_ENV === "production";
export const {
doubleCsrfProtection, // middleware da applicare alle rotte
generateCsrfToken, // genera e imposta il token
invalidCsrfTokenError // errore tipizzato per la gestione
} = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET,
getSessionIdentifier: (req) => req.sessionID || req.ip,
cookieName: isProd ? "__Host-psifi.x-csrf-token" : "x-csrf-token",
cookieOptions: {
httpOnly: true,
sameSite: "strict",
secure: isProd,
path: "/"
},
size: 32,
ignoredMethods: ["GET", "HEAD", "OPTIONS"],
getTokenFromRequest: (req) =>
req.headers["x-csrf-token"] || req.body?._csrf
});
Analizziamo le opzioni principali. getSecret fornisce il segreto HMAC con cui il token viene firmato e verificato. getSessionIdentifier lega il token a un’identità (la sessione se presente, altrimenti l’IP) per evitare il riuso del token tra utenti diversi. ignoredMethods esclude i metodi sicuri e idempotenti: GET, HEAD e OPTIONS non devono mai cambiare stato, quindi non richiedono un token CSRF.
Le cookieOptions sono critiche. httpOnly: true impedisce a JavaScript di leggere il cookie, riducendo l’impatto di un eventuale XSS. sameSite: "strict" dice al browser di non inviare il cookie nelle richieste cross-site. secure: true in produzione impone HTTPS. Il prefisso __Host- sul nome del cookie in produzione aggiunge una garanzia extra: il browser accetta quel cookie solo se è impostato con Secure, path=/ e senza dominio esplicito.
La funzione getTokenFromRequest definisce dove il server cerca il token nella richiesta in arrivo: prima nell’header x-csrf-token (usato dalle SPA), poi nel campo nascosto _csrf del body (usato dai form HTML tradizionali). Questo doppio supporto rende lo stesso backend compatibile sia con i form che con le SPA.
Step 7: Proteggere le rotte che cambiano stato
Applica il middleware doubleCsrfProtection alle rotte che modificano lo stato. Hai due opzioni: applicarlo globalmente (e poi escludere le rotte pubbliche) oppure applicarlo per rotta. L’approccio per rotta è più esplicito e meno soggetto a errori, quindi è quello che useremo.
// app.js (aggiunte)
import { doubleCsrfProtection, generateCsrfToken } from "./csrf.js";
// Endpoint per ottenere il token (usato da form e SPA)
app.get("/csrf-token", (req, res) => {
const token = generateCsrfToken(req, res);
res.json({ csrfToken: token });
});
// Rotta protetta: cambia stato, richiede un token valido
app.post("/account/email", doubleCsrfProtection, (req, res) => {
const { nuovaEmail } = req.body;
// ... logica di aggiornamento ...
res.json({ ok: true, email: nuovaEmail });
});
La rotta GET /csrf-token genera il token, lo imposta nel cookie firmato e ne restituisce il valore al client. La rotta POST /account/email è protetta: se arriva senza un token valido, il middleware blocca la richiesta prima che raggiunga la tua logica. Ogni endpoint POST, PUT, PATCH o DELETE che modifica dati deve avere doubleCsrfProtection nella sua catena di middleware.
Un principio da non violare mai: le rotte GET devono restare sicure e idempotenti. Se hai una rotta come GET /account/elimina che cancella dati, non c’è token CSRF che tenga, perché un semplice tag <img src="..."> su una pagina ostile la attiverebbe. Sposta sempre le azioni distruttive su POST o DELETE.
Step 8: Servire il token a un form HTML
Per un form tradizionale con rendering server-side, inietta il token in un campo nascosto. Aggiungiamo una rotta che genera il token e lo inserisce nell’HTML. Useremo template literals per semplicità, ma in un progetto reale lo passeresti al tuo motore di template (EJS, Pug, Handlebars).
// app.js (aggiunte)
app.get("/profilo", (req, res) => {
const token = generateCsrfToken(req, res);
res.send(`
<!DOCTYPE html>
<html lang="it">
<body>
<h1>Cambia email</h1>
<form action="/account/email" method="POST">
<input type="hidden" name="_csrf" value="${token}">
<input type="email" name="nuovaEmail" required>
<button type="submit">Salva</button>
</form>
</body>
</html>
`);
});
Quando l’utente invia il form, il browser allega sia il cookie del token (firmato HMAC) sia il valore nel campo _csrf. Il middleware confronta i due e verifica la firma. Un sito malevolo può forzare il browser a inviare il cookie, ma non conosce il valore del token da inserire nel body, e non può leggerlo a causa della Same-Origin Policy. Per questo l’attacco fallisce.
Genera sempre un token fresco al rendering della pagina. Non riutilizzare lo stesso token statico tra pagine diverse e non incorporarlo nell’URL: gli URL finiscono nei log, nella cronologia del browser e nell’header Referer, esponendo il token.
Step 9: Inviare il token da una SPA (fetch e Axios)
Le Single Page Application (React, Vue, Angular) non usano form server-side. Recuperano il token da un endpoint dedicato all’avvio e lo allegano a ogni richiesta che cambia stato tramite un header personalizzato. Questo approccio è più sicuro dei campi nascosti perché gli header personalizzati non possono essere impostati da uno script cross-site senza superare la Same-Origin Policy.
// client.js (frontend SPA)
// 1. Recupera il token all'avvio dell'app
async function initCsrf() {
const res = await fetch("/csrf-token", { credentials: "include" });
const { csrfToken } = await res.json();
return csrfToken;
}
// 2. Allega il token a ogni richiesta che cambia stato
async function aggiornaEmail(csrfToken, nuovaEmail) {
const res = await fetch("/account/email", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"x-csrf-token": csrfToken
},
body: JSON.stringify({ nuovaEmail })
});
return res.json();
}
L’opzione credentials: "include" è obbligatoria: senza di essa il browser non invia il cookie del token e la verifica fallisce sempre. Con Axios, puoi automatizzare l’allegato del token con un interceptor, così non devi ricordartene a ogni chiamata:
// Axios: interceptor globale
import axios from "axios";
const api = axios.create({ withCredentials: true });
let csrfToken = null;
export async function setupCsrf() {
const { data } = await api.get("/csrf-token");
csrfToken = data.csrfToken;
}
api.interceptors.request.use((config) => {
if (["post", "put", "patch", "delete"].includes(config.method)) {
config.headers["x-csrf-token"] = csrfToken;
}
return config;
});
export default api;
Conserva il token nello stato dell’applicazione (variabile di modulo, store Redux/Pinia), non in localStorage. Il localStorage è leggibile da qualsiasi script nella pagina, quindi un XSS lo esporrebbe immediatamente. Se il token scade o una richiesta restituisce un errore CSRF, richiama setupCsrf() per ottenerne uno nuovo.
Step 10: Difese a strati con SameSite, Origin e HTTPS
Un token CSRF è la difesa primaria, ma OWASP raccomanda di non affidarsi a un solo controllo. Aggiungiamo tre barriere complementari che rendono l’attacco molto più difficile anche in caso di errore di configurazione.
Cookie SameSite sulla sessione
Imposta SameSite=Strict o Lax su tutti i cookie di sessione, non solo sul token CSRF. Strict blocca l’invio del cookie in qualsiasi richiesta cross-site. Lax lo consente solo nelle navigazioni di primo livello (GET su un link), offrendo un buon equilibrio tra sicurezza ed esperienza utente. Non usare mai SameSite=None senza Secure.
Validazione dell’header Origin
Come difesa secondaria, verifica l’header Origin (o in mancanza Referer) contro una lista di domini fidati. Le richieste senza un Origin valido vengono respinte prima ancora di controllare il token.
// originGuard.js
const ORIGINI_FIDATE = new Set([
"https://app.tuodominio.it",
"https://tuodominio.it"
]);
export function originGuard(req, res, next) {
const metodiSicuri = ["GET", "HEAD", "OPTIONS"];
if (metodiSicuri.includes(req.method)) return next();
const origin = req.headers.origin ||
(req.headers.referer && new URL(req.headers.referer).origin);
if (!origin || !ORIGINI_FIDATE.has(origin)) {
return res.status(403).json({ errore: "Origin non valida" });
}
next();
}
Applica originGuard prima di doubleCsrfProtection sulle rotte sensibili. Questo doppio controllo è particolarmente efficace contro attacchi che riescono a superare uno solo dei due meccanismi.
HTTPS obbligatorio
Senza HTTPS, un attaccante in posizione di rete può leggere il cookie del token e l’intera protezione crolla. In produzione, imponi HTTPS, attiva il flag secure sui cookie e aggiungi l’header HSTS. Se non hai ancora un certificato, configurarne uno gratuito è semplice e veloce.
Step 11: Gestire gli errori CSRF in modo pulito
Quando un token manca o non è valido, csrf-csrf lancia un errore tipizzato. Devi intercettarlo con un gestore di errori Express per restituire una risposta 403 chiara, invece di esporre uno stack trace. Registra il gestore dopo tutte le rotte.
// app.js (in fondo, dopo le rotte)
import { invalidCsrfTokenError } from "./csrf.js";
app.use((err, req, res, next) => {
if (err === invalidCsrfTokenError || err.code === "EBADCSRFTOKEN") {
return res.status(403).json({
errore: "Token CSRF mancante o non valido",
azione: "Ricarica la pagina e riprova"
});
}
next(err);
});
Lato client, intercetta la risposta 403 e gestiscila con eleganza: richiedi un nuovo token e, se opportuno, riprova la richiesta una sola volta. Non mostrare mai il messaggio di errore grezzo all’utente finale e non loggare il valore del token nei log applicativi, perché renderebbe vana la sua segretezza.
Step 12: Testare la protezione CSRF
Una protezione non testata non è una protezione. Verifichiamo tre scenari: una richiesta senza token deve fallire, una con token valido deve passare, e cookie e token incoerenti devono essere respinti. Usiamo curl per simulare le richieste.
# 1. Richiesta SENZA token: deve essere respinta con 403
curl -i -X POST http://localhost:3000/account/email \
-H "Content-Type: application/json" \
-d '{"nuovaEmail":"[email protected]"}'
# Atteso: HTTP/1.1 403 Forbidden
# {"errore":"Token CSRF mancante o non valido", ...}
# 2. Ottieni un token e salva i cookie
curl -c cookies.txt http://localhost:3000/csrf-token
# Output: {"csrfToken":"a1b2c3...."}
# 3. Richiesta CON token valido e cookie: deve passare
curl -i -X POST http://localhost:3000/account/email \
-b cookies.txt \
-H "Content-Type: application/json" \
-H "x-csrf-token: a1b2c3...." \
-d '{"nuovaEmail":"[email protected]"}'
# Atteso: HTTP/1.1 200 OK
# {"ok":true,"email":"[email protected]"}
Il primo comando deve restituire 403, confermando che le richieste prive di token vengono bloccate. Il terzo deve restituire 200, confermando che una richiesta legittima con cookie e header coerenti passa. Se inverti i due, ad esempio fornendo il token ma non il cookie (o viceversa), la richiesta deve fallire: il pattern Double Submit richiede entrambi.
Per un test automatizzato, puoi scrivere una suite con Supertest che esegue questi tre scenari in un test di integrazione. Includi sempre il caso negativo (token mancante) tra i test, perché è quello che garantisce che la protezione sia davvero attiva e non solo presente nel codice.
Il progetto completo funzionante
Ecco l’app.js completo che mette insieme tutti gli step. Con i file csrf.js e originGuard.js mostrati sopra, questo è un server Express 5 con protezione CSRF pronta per essere estesa.
// app.js (completo)
import express from "express";
import helmet from "helmet";
import cookieParser from "cookie-parser";
import crypto from "node:crypto";
import {
doubleCsrfProtection,
generateCsrfToken,
invalidCsrfTokenError
} from "./csrf.js";
import { originGuard } from "./originGuard.js";
const app = express();
const isProd = process.env.NODE_ENV === "production";
app.use(helmet());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
const COOKIE_SECRET = process.env.COOKIE_SECRET ||
crypto.randomBytes(32).toString("hex");
app.use(cookieParser(COOKIE_SECRET));
if (isProd) app.set("trust proxy", 1);
// Endpoint token per SPA e form
app.get("/csrf-token", (req, res) => {
res.json({ csrfToken: generateCsrfToken(req, res) });
});
// Form server-side
app.get("/profilo", (req, res) => {
const token = generateCsrfToken(req, res);
res.send(`<form action="/account/email" method="POST">
<input type="hidden" name="_csrf" value="${token}">
<input type="email" name="nuovaEmail" required>
<button>Salva</button></form>`);
});
// Rotta protetta con difesa a strati
app.post("/account/email", originGuard, doubleCsrfProtection,
(req, res) => {
res.json({ ok: true, email: req.body.nuovaEmail });
});
// Gestore errori CSRF
app.use((err, req, res, next) => {
if (err === invalidCsrfTokenError || err.code === "EBADCSRFTOKEN") {
return res.status(403).json({ errore: "Token CSRF non valido" });
}
next(err);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`In ascolto su :${PORT}`));
La struttura del progetto resta minima: app.js, csrf.js, originGuard.js, un file .env con i segreti e package.json. Da qui puoi collegare un database, un sistema di sessioni con express-session o un frontend SPA senza modificare la logica di protezione.
6 errori comuni nella protezione CSRF
Anche con la libreria giusta, alcuni errori ricorrenti vanificano la protezione. Ecco i sei più diffusi e come evitarli.
- Usare ancora csurf. È deprecato dal 2020 e non gestisce
SameSitein modo corretto. Migra acsrf-csrfo alla protezione nativa del tuo framework. - Segreto CSRF hardcoded o debole. Un segreto breve o scritto nel codice permette di fabbricare token validi. Usa almeno 32 byte casuali da una variabile d’ambiente persistente.
- Azioni che cambiano stato su GET. Una rotta
GET /eliminaè attivabile con un tag<img>e nessun token può proteggerla. Usa POST, PUT, PATCH o DELETE. - Dimenticare credentials: “include”. Senza questa opzione il browser non invia il cookie del token e ogni richiesta SPA fallisce, spingendo a disabilitare la protezione per errore.
- Salvare il token in localStorage. È leggibile da qualsiasi script. Un XSS ruberebbe il token. Tieni il token nello stato in memoria dell’app.
- SameSite=None senza Secure. Disattiva la protezione del browser ed espone il cookie. Se non ti serve il cross-site, usa
StrictoLax.
Risoluzione dei problemi (troubleshooting)
Quando la protezione CSRF non si comporta come previsto, la causa è quasi sempre una di queste. La tabella collega il sintomo alla causa e alla soluzione.
| Sintomo | Causa probabile | Soluzione |
|---|---|---|
| Tutte le POST restituiscono 403 | Il cookie del token non viene inviato | Aggiungi credentials: "include" (fetch) o withCredentials: true (Axios) |
| Funziona in locale, fallisce in produzione | secure: true richiede HTTPS | Servi su HTTPS e imposta trust proxy dietro un reverse proxy |
| I token diventano invalidi dopo ogni deploy | Segreto CSRF generato a runtime | Fissa CSRF_SECRET in una variabile d’ambiente persistente |
Cookie __Host- non impostato | Manca Secure, path=/ o c’è un dominio esplicito | Rimuovi domain, imposta path: "/" e secure: true |
| Errore CORS prima del controllo CSRF | Origin non in lista o credenziali non abilitate | Configura CORS con credentials: true e origini esplicite |
| Il token nel form non viene letto | getTokenFromRequest non guarda nel body | Verifica che cerchi req.body._csrf e che urlencoded sia attivo |
| Le richieste SPA passano ma i form no | Header presente, campo nascosto assente | Inserisci <input type="hidden" name="_csrf"> nel form |
| 403 intermittente per alcuni utenti | Più istanze server con segreti diversi | Condividi lo stesso CSRF_SECRET tra tutte le istanze |
| Test con curl falliscono sempre | Cookie e header non coerenti | Usa -c/-b per salvare e reinviare i cookie |
Se il problema persiste, attiva un log temporaneo che stampi se l’header e il cookie del token sono entrambi presenti (senza loggarne il valore). Nella stragrande maggioranza dei casi scoprirai che uno dei due manca, e da lì la causa è immediata.
Consigli avanzati per la protezione CSRF
Una volta che la protezione di base funziona, alcune tecniche aggiuntive alzano ulteriormente il livello di sicurezza, soprattutto per applicazioni con dati sensibili o requisiti di conformità come il GDPR e la direttiva NIS2.
- Token per richiesta sulle azioni critiche. Per operazioni ad alto rischio (cambio password, trasferimenti, eliminazione account) genera un token monouso valido per una sola richiesta, oltre al token di sessione. Riduce la finestra di replay.
- Rotazione del segreto. Pianifica una rotazione periodica di
CSRF_SECRETsupportando due segreti contemporaneamente durante la transizione, così i token già emessi restano validi fino alla scadenza. - Rate limiting sugli endpoint sensibili. Abbina la protezione CSRF a un limitatore di frequenza per mitigare anche tentativi di brute-force e abusi automatizzati.
- Difesa contro l’XSS prima di tutto. Il Double Submit firmato resta vulnerabile a un XSS che legge il body o inietta richieste. Una Content Security Policy rigorosa con
helmete l’escaping dell’output sono prerequisiti, non optional. - Protezione nativa del framework. Se usi Fastify, valuta
@fastify/csrf-protection, che offre sia il Synchronizer Token sia il Double Submit. Scegli sempre la protezione integrata quando disponibile. - Audit periodico delle rotte. A ogni nuova rotta che cambia stato, verifica in code review che il middleware CSRF sia applicato. Una rotta dimenticata è un buco.
Per una panoramica autorevole e sempre aggiornata sulle contromisure, consulta la OWASP CSRF Prevention Cheat Sheet e la spiegazione tecnica dell’attacco su PortSwigger Web Security Academy. Per il comportamento dell’attributo cookie, il riferimento è la documentazione MDN su SameSite.
CSRF e il contesto normativo europeo
In Europa, una protezione CSRF carente non è solo un problema tecnico ma anche di conformità. Un attacco CSRF riuscito che porta alla modifica non autorizzata di dati personali può configurare una violazione ai sensi del GDPR, con obbligo di notifica al Garante entro 72 ore e potenziali sanzioni. La direttiva NIS2, recepita in Italia, impone misure di sicurezza adeguate per i soggetti essenziali e importanti, e i controlli applicativi contro le richieste falsificate rientrano tra le buone pratiche attese.
Documentare la presenza di una protezione CSRF nelle tue applicazioni, insieme ai test che ne verificano l’efficacia, è utile sia in fase di audit sia in caso di incidente. Una difesa a strati come quella di questo tutorial, token firmato HMAC più SameSite più validazione Origin più HTTPS, è esattamente il tipo di misura tecnica che dimostra diligenza nel trattamento dei dati. Per approfondire l’attacco dal punto di vista del difensore, la scheda OWASP sul Cross-Site Request Forgery resta il punto di partenza.
La protezione CSRF si integra con le altre difese applicative. Va vista insieme all’autenticazione robusta, alla gestione sicura delle sessioni e all’hashing corretto delle credenziali. Nessuna di queste misure, da sola, basta: insieme formano una postura di sicurezza coerente.
Domande frequenti sulla protezione CSRF
Il pacchetto csurf è ancora utilizzabile nel 2026?
No. csurf è deprecato e abbandonato dal 2020. Non riceve correzioni di sicurezza e non gestisce correttamente i cookie SameSite moderni. Per ogni nuovo progetto usa csrf-csrf o la protezione nativa del tuo framework, come @fastify/csrf-protection per Fastify.
Se uso SameSite=Strict, ho ancora bisogno di un token CSRF?
Sì. SameSite è una difesa preziosa ma non sufficiente da sola: il comportamento varia tra browser, non copre tutti i casi e i browser più datati lo gestiscono in modo incoerente. OWASP raccomanda di combinare il token CSRF con SameSite, non di sostituirlo. La difesa a strati è la strategia corretta.
Le API REST con autenticazione tramite token Bearer sono vulnerabili a CSRF?
Se l’autenticazione si basa esclusivamente su un token Bearer inviato in un header Authorization (e non su cookie), l’attacco CSRF classico non si applica, perché il browser non allega automaticamente quell’header. Il rischio CSRF nasce quando l’autenticazione viaggia nei cookie. Se la tua SPA usa cookie di sessione, la protezione CSRF resta necessaria.
Qual è la differenza tra CSRF e XSS?
Il CSRF induce il browser della vittima a inviare richieste indesiderate sfruttando la sessione esistente, senza leggere dati. L’XSS esegue codice malevolo nel contesto del sito, potendo leggere token, cookie e dati. Sono attacchi distinti ma collegati: un XSS può aggirare la protezione CSRF leggendo il token, motivo per cui difendersi dall’XSS è prioritario.
Devo generare un token CSRF nuovo a ogni richiesta?
Non è obbligatorio. Un token per sessione, firmato HMAC come fa csrf-csrf, è sicuro per la maggior parte delle applicazioni. Il token per richiesta aggiunge protezione contro il replay ed è consigliato solo per le operazioni più critiche, dove vale la complessità aggiuntiva di gestione.
La protezione CSRF rallenta l’applicazione?
L’impatto è trascurabile. La generazione e la verifica di un token HMAC con il modulo crypto nativo di Node.js richiedono microsecondi per richiesta, ben al di sotto della latenza di rete o di database. Il pattern Double Submit non richiede storage lato server, quindi non aggiunge carico al database.
Come gestisco il token CSRF in una SPA con più schede aperte?
Con il pattern Double Submit legato al cookie, tutte le schede dello stesso dominio condividono il cookie del token, quindi funzionano in modo coerente. Recupera il token all’avvio di ogni istanza dell’app e, se una richiesta restituisce 403, richiedine uno nuovo dall’endpoint dedicato e ripeti l’operazione una sola volta.
Posso usare lo stesso backend per form HTML e SPA?
Sì, ed è esattamente ciò che fa la configurazione di questo tutorial. La funzione getTokenFromRequest cerca il token prima nell’header x-csrf-token (SPA) e poi nel campo _csrf del body (form). Lo stesso endpoint protetto accetta entrambe le modalità senza modifiche.
Approfondimenti correlati
- Autenticazione JWT in Node.js: 12 Step [2026]
- Hashing Password con bcrypt in Node.js: 12 Step [2026]
- Crittografia End-to-End in Node.js: 12 Step [2026]
- HTTPS e TLS: come viene protetta una connessione web
- Sicurezza delle password: lunghezza, hashing e secondo fattore
- Let’s Encrypt e Certbot: HTTPS Gratis in 10 Step [2026]
- Sicurezza online: la guida pratica
La protezione CSRF non è un componente opzionale da aggiungere alla fine: è una parte fondamentale di ogni applicazione Node.js che gestisce sessioni basate su cookie. Con i 12 step di questo tutorial hai un’implementazione completa, testata e allineata alle raccomandazioni OWASP 2025, pronta per essere portata in produzione e adattata alle esigenze del tuo progetto.




