Ogni talk sulla sicurezza web finisce sulla stessa slide: fai sempre l’escape dell’input utente prima di inserirlo in HTML. Il motivo sono le entità HTML — il meccanismo che trasforma <script> in <script> e impedisce che il commento di un utente diventi un attacco XSS.
Ma fare l’escape è solo metà della faccenda. Fare l’unescape — trasformare & di nuovo in & — è altrettanto importante e altrettanto pericoloso quando fatto male. Questo post copre entrambe le direzioni, i caratteri che contano e gli errori che portano a vulnerabilità reali.
I cinque caratteri che contano
HTML ha cinque caratteri che sono strutturalmente significativi — delimitano tag, attributi e entità. Se qualcuno di questi appare nel contenuto utente e non viene escaped, il browser li interpreta come markup HTML:
| Carattere | Entità | Perché è importante |
|---|---|---|
< |
< |
Inizia un tag. <script> è un tag. |
> |
> |
Termina un tag. |
& |
& |
Inizia un’entità. < è un’entità. |
" |
" |
Delimita i valori degli attributi. |
' |
' |
Delimita gli attributi tra virgolette singole. |
Se stai inserendo testo nel contenuto di un elemento HTML, devi fare l’escape di <, > e &. Se stai inserendo testo in un valore di attributo, devi anche fare l’escape di " (o ' se l’attributo usa virgolette singole).
Come funzionano le entità
Un’entità HTML è una sequenza speciale che il browser interpreta come un singolo carattere:
- Entità nominate:
&,<,>,", — facili da leggere, set limitato. - Entità numeriche:
<(decimale),<(esadecimale) — qualsiasi carattere Unicode per code point. - Entità nominate per simboli:
©(©),€(€),—(—) — nomi di comodo.
L’entità & è speciale perché è l’escape per & stesso. Se fai l’escape di & per primo, poi di < e >, non fai doppio escape. L’ordine conta:
Corretto: & → & → &lt; (corretto)
Sbagliato: < → < → &lt; (doppio escape)
Fai sempre l’escape di & per primo. Poi gli altri caratteri. Così non farai doppio escape.
Dove l’escape è richiesto
Contenuto generato dall’utente. Commenti, post del forum, recensioni, messaggi — qualsiasi testo che viene da un utente e appare su una pagina deve essere escaped. Un commento contenente <img src=x onerror=alert(1)> è un attacco XSS se non fai l’escape di < e >.
Dati negli attributi. Se metti dati utente in un attributo href, src, alt o title, fai l’escape dei caratteri entità. Un valore come foo" onclick="alert(1) esce dall’attributo e inietta un event handler.
Interpolazione stringhe JavaScript. Se stai costruendo HTML dentro JavaScript:
// PERICOLOSO: interpolazione raw
element.innerHTML = `<p>${userComment}</p>`;
// SICURO: interpolazione escaped
element.innerHTML = `<p>${escapeHtml(userComment)}</p>`;
innerHTML interpreta la stringa come HTML. Se userComment contiene markup, viene eseguito. Fai sempre l’escape prima di interpolare in HTML.
Rendering lato server. I template engine (Handlebars, Jinja, EJS, Razor) di solito fanno l’escape automaticamente per impostazione predefinita. Ma se usi un filtro “safe” o “raw” (come le parentesi graffe triple di Handlebars {{{ o |safe di Django), stai disabilitando l’escape. Fallo solo quando controlli il contenuto.
Quando fare l’unescape
L’unescape inverte il processo: < → <, & → &. È necessario quando:
- Mostrare dati codificati. Se un’API restituisce HTML escaped (alcune lo fanno per impedire il rendering), devi fare l’unescape prima di mostrare.
- Elaborare dati modulo. Le inviazioni modulo possono contenere valori codificati in URL o codificati come entità.
- Analizzare l’input utente. Se un utente incolla testo codificato in HTML e vuoi mostrarlo così com’è, fai prima l’unescape — poi ri-escape se lo stai reinserendo in HTML.
Il pericolo: fai l’unescape solo quando stai per mostrare, mai quando stai per memorizzare. Memorizza la forma originale escaped. Fai l’unescape al momento del rendering.
XSS: a un carattere escape di distanza
Il cross-site scripting (XSS) succede quando l’input utente non escaped raggiunge il parser HTML del browser. Le varianti più comuni:
XSS memorizzato. Un utente invia <script>steal(document.cookie)</script> come commento. Il server lo memorizza senza escape. Ogni visitatore che vede il commento esegue lo script.
XSS riflesso. Un URL contiene ?q=<script>alert(1)</script>. Il server riflette il parametro q nella pagina senza escape. Chiunque clicca il link esegue lo script.
XSS basato su DOM. JavaScript legge location.hash e lo inserisce nel DOM tramite innerHTML. L’hash contiene markup. Il browser lo renderizza.
Tutti e tre sono prevenuti dalla stessa cosa: fai l’escape di <, >, &, " e ' prima di inserire contenuto in HTML.
La funzione di escape
Ogni linguaggio ne ha una. Il pattern è lo stesso:
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
L’ordine conta: & per primo, poi gli altri. Se fai l’escape di < prima di &, ottieni &lt; invece di <.
Per sistemi ad alto throughput, usa una libreria. DOMPurify sanitizza HTML (consentendo tag sicuri, rimuovendo quelli pericolosi). Il DOMParser integrato della piattaforma Web e textContent gestiscono anche l’escape implicitamente.
Errori comuni
- Fare l’escape di
&dopo<— causa doppio escape:&lt;invece di<. - Dimenticare il contesto attributo — fare l’escape di
<e>ma non di"consente l’iniezione di attributi. - Usare
innerHTMLsenza escape — il vettore XSS più comune. - Fidarsi delle risposte del server — le API che restituiscono HTML “sanitizzato” potrebbero non sanitizzare tutto. Fai l’escape anche lato client.
- Fare l’unescape troppo presto — fare l’unescape al momento della memorizzazione significa che il payload raw resta nel tuo database, pronto a eseguire se il rendering dimentica di ri-escape.
Prova
Se hai testo codificato in HTML da decodificare o testo raw da fare l’escape per l’inserimento sicuro in HTML, usa uno strumento basato su browser affinché la conversione resti locale.