L'escape HTML sostituisce quella manciata di caratteri che altrimenti verrebbero letti come markup con riferimenti a entità, così che il testo resti testo. È un'operazione piccola con una reputazione sproporzionata, perché sbagliarla è il modo in cui accade il cross-site scripting — e perché "fai l'escape dell'input" è un consiglio sottilmente, pericolosamente incompleto.
Cinque caratteri, e l'ordine in cui li sostituisci
L'insieme è piccolo: & diventa &, < diventa <, > diventa >, " diventa " e ' diventa '. Tutto il resto può restare com'è.
In quell'elenco si nasconde un bug classico. Se sostituisci i caratteri uno alla volta e gestisci & per ultimo, distruggi il tuo stesso lavoro: < diventa <, poi la & che hai appena introdotto diventa &, e l'output è &lt; — un letterale, visibile "<" sulla pagina invece di un segno di minore. La e commerciale va sempre codificata per prima, perché ogni altra sostituzione ne introduce una.
Questo strumento aggira la trappola del tutto facendo il match di tutti e cinque i caratteri in una passata sola e sostituendo ognuno esattamente una volta, così l'ordine non si pone nemmeno. È la forma più robusta per questo tipo di codice, e vale la pena copiarla: una singola regex con una tabella di lookup non può codificare due volte, mentre una catena di replace dipende da uno sviluppatore che si ricordi un invariante.
Perché l'apostrofo è numerico
Guarda bene e l'apostrofo diventa ' e non ', che accanto a " sembra incoerente. È voluto. ' è stato definito in XML e ha raggiunto HTML solo nella versione 5: non fa parte di HTML 4, e i parser molto vecchi lo renderanno letteralmente come i quattro caratteri ' invece che come un apostrofo.
Il riferimento numerico ' ha sempre funzionato ovunque. È una piccola cosa, ma è il tipo di piccola cosa che salta fuori come testo storpiato in un client di posta che renderizza HTML del 2003, e la forma numerica non costa niente.
L'escape dipende dal contesto — ed è questa la parte che conta
Ecco l'equivoco che vale la pena smontare: non esiste "HTML con l'escape" in astratto. Esiste solo testo codificato correttamente per un posto specifico, e lo stesso escape perfettamente sicuro in una posizione è inutile in un'altra.
Tra i tag, codificare questi cinque caratteri è sufficiente. Dentro un attributo quotato è anch'esso sufficiente — a patto che l'attributo abbia davvero le virgolette. Scrivi <img src=VALORE> senza, e un valore codificato che contiene uno spazio esce comunque: x onerror=alert(1) non contiene nessun carattere che l'escape HTML tocchi, e diventa un attributo. Sono le virgolette a fare il lavoro di sicurezza, non l'escape.
Altrove va peggio. Dentro un blocco <script> le regole HTML non valgono: serve l'escape delle stringhe JavaScript, e la sequenza </script> ovunque dentro un letterale stringa chiude il blocco a prescindere da cosa la circonda. Dentro un attributo URL, l'escape HTML non fa nulla contro javascript: come schema. Dentro un attributo style, il CSS ha la sua sintassi di escape. Cinque contesti diversi, cinque regole diverse, e un template engine che ne conosce solo una produrrà allegramente un buco.
Decodificare è l'opposto della sicurezza
Codificare rende il testo inerte. Decodificare prende testo inerte e lo rende di nuovo vivo. Quindi decodificare qualcosa e poi metterlo in una pagina con innerHTML annulla esattamente la protezione che la codifica forniva — ed è uno schema reale in codice reale, di solito scritto da qualcuno che pensava che "decodificare" significasse "ripulire".
Questo strumento è sicuro con cui decodificare perché non fa niente col risultato se non mostrartelo in una textarea, che non può eseguire nulla. È in quello che fai dopo che vive il rischio. Se stai decodificando entità per inserire il risultato in una pagina, usa textContent invece di innerHTML; e se hai davvero bisogno di renderizzare HTML fornito dagli utenti, ti serve un sanitizzatore come DOMPurify — una lista di tag e attributi consentiti — non un decodificatore di entità.
Domande frequenti
Il mio testo viene inviato a un server?
No. Questo strumento è marcato "client": la sostituzione gira nella scheda del tuo browser e non viene trasmesso né salvato niente.
Codificare e poi decodificare mi restituisce esattamente quello da cui sono partito?
Sì. Le cinque sostituzioni sono reversibili e non ambigue, quindi un giro completo attraverso questo strumento restituisce il testo originale carattere per carattere, e commerciali e virgolette incluse. Non è vero di ogni schema di escape, ma è vero di questo.
L'escape basta a prevenire l'XSS?
Solo quando corrisponde al contesto, come descritto sopra. La risposta breve e onesta è che l'escape manuale è il modo fragile di farlo. I framework moderni — React, Vue, Angular, e qualsiasi template engine lato server attuale — fanno l'escape per default e sanno in quale contesto si trovano. È quella la difesa vera. Uno strumento come questo serve a ispezionare e capire un valore, non a essere lo strato di sicurezza di un'applicazione.
E e tutte le altre entità con nome?
La specifica HTML ne definisce oltre duemila, e quasi nessuna serve più. In un documento UTF-8 puoi scrivere é, ©, — e 😀 direttamente, ed è più leggibile di quanto é, © e — siano mai stati. Le eccezioni da tenere sono le cinque strutturali, più dove vuoi specificamente uno spazio che non vada a capo. Le entità con nome altrove sono un'abitudine ereditata da un'epoca in cui le codifiche erano inaffidabili.
Perché il mio testo codificato mostra &lt; invece di <?
Qualcosa l'ha codificato due volte. La prima passata ha trasformato < in <, e la seconda ha visto la & in quel risultato e l'ha trasformata in &. Cerca un template che fa l'escape di un valore già codificato a monte: capita spesso quando uno strato codifica premurosamente e un altro lo rifà "per sicurezza". Codifica una volta sola, nel punto di output.