Sicurezza · 7 settembre 2026

Entità HTML — quando fare l'escape, quando fare l'unescape e perché XSS è a un carattere escape di distanza

Le entità HTML trasformano caratteri speciali in sequenze sicure. Prevengono XSS, preservano l'integrità del contenuto e fanno inciampare ogni sviluppatore almeno una volta. Ecco il quadro completo.

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 &lt;script&gt; e impedisce che il commento di un utente diventi un attacco XSS.

Ma fare l’escape è solo metà della faccenda. Fare l’unescape — trasformare &amp; 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
< &lt; Inizia un tag. <script> è un tag.
> &gt; Termina un tag.
& &amp; Inizia un’entità. &lt; è un’entità.
" &quot; Delimita i valori degli attributi.
' &#39; 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: &amp;, &lt;, &gt;, &quot;, &nbsp; — facili da leggere, set limitato.
  • Entità numeriche: &#60; (decimale), &#x3C; (esadecimale) — qualsiasi carattere Unicode per code point.
  • Entità nominate per simboli: &copy; (©), &euro; (€), &mdash; (—) — nomi di comodo.

L’entità &amp; è speciale perché è l’escape per & stesso. Se fai l’escape di & per primo, poi di < e >, non fai doppio escape. L’ordine conta:

Corretto:  & → &amp;  →  &amp;lt;   (corretto)
Sbagliato: < → &lt;   →  &amp;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: &lt; → <, &amp; → &. È 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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

L’ordine conta: & per primo, poi gli altri. Se fai l’escape di < prima di &, ottieni &amp;lt; invece di &lt;.

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: &amp;lt; invece di &lt;.
  • Dimenticare il contesto attributo — fare l’escape di < e > ma non di " consente l’iniezione di attributi.
  • Usare innerHTML senza 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.