HTML-entiteiten coderen

Plak tekst of een stuk markup en krijg het terug met de tekens ontsnapt die anders als HTML gelezen zouden worden. Standaard zijn dat de vijf die de parse veranderen — `&`, `<`, `>`, `"` en `'` — en er is een tweede stand die alles boven ASCII er ook in zet, voor een keten die geen UTF-8 aankan. Het gebeurt allemaal op deze pagina.

Het eerste is wat een sjabloonmotor doet. Het tweede is voor een verwerkingsketen die niet netjes met UTF-8 omgaat.

Resultaat

Het antwoord verschijnt hier terwijl je typt.

  • Waar het draait

    Er wordt niets geüpload, want er is geen bestand — het wordt in deze pagina uitgerekend.

  • Geen wachtrij, geen account

    Het antwoordt zo snel als je machine kan, en vraagt nooit wie je bent.

  • Zo vaak als je wilt

    Er wordt niets geteld en niets begrensd — nog eens antwoorden kost ons niets.

Hoe het werkt

  1. Plak de tekst of het stuk markup.
  2. Kies hoe ver je ontsnapt: alleen wat de parse verandert, of daarbovenop alles boven ASCII.
  3. Kopieer het resultaat. Het heeft je machine niet verlaten.

De vijf tekens waar dit over gaat

Het kleiner-dan opent een tag, de ampersand opent een verwijzing, en het aanhalingsteken — enkel of dubbel — sluit een attribuutwaarde af. Het groter-dan is strikt genomen niet altijd nodig en gaat mee omdat het inconsequent zou zijn het weg te laten. Samen zijn dat de tekens die van tekst structuur maken.

Al het andere is gewoon tekst. Een pagina die elk leesteken ontsnapt, is niet veiliger maar wel onleesbaar in de bron, en levert bij een latere bewerking eerder problemen op. De vraag is nooit «hoeveel kan ik ontsnappen» maar «wat verandert hier de betekenis».

De ampersand gaat eerst, of het gaat niet

Wie zelf ontsnapt met een reeks vervangingen, moet de ampersand als eerste doen. Doe je `<` eerst, dan staat er `&lt;`, en vervangt de volgende stap die ampersand alsnog — met `&amp;lt;` als resultaat, dat op de pagina als de letterlijke tekst `&lt;` verschijnt.

Het is de klassieker onder de zelfgebouwde ontsnappingsfuncties, en de reden dat dubbel ontsnapte tekst zo vaak op websites staat. Hier wordt in één doorgang per teken gewerkt, zodat de volgorde geen rol speelt — en dat is meteen het argument om zulke functies niet zelf te schrijven.

Inhoud en attribuut zijn niet hetzelfde

Tekst tussen twee tags heeft aan `&` en `<` genoeg. Tekst binnen een attribuutwaarde heeft daarnaast het aanhalingsteken nodig waarmee die waarde is afgebakend, want daarmee kun je de waarde verlaten en er een tweede attribuut achter zetten.

Nog gevaarlijker is een attribuut zonder aanhalingstekens, wat HTML toestaat. Dan volstaat een spatie om eruit te breken, en dan helpt ontsnappen van aanhalingstekens niets. De regel die overal werkt: zet attribuutwaarden altijd tussen aanhalingstekens, en ontsnap ook het teken waarmee je ze hebt afgebakend.

Wanneer je «alles boven ASCII» nodig hebt

De tweede stand zet ook letters met een trema, accenten, aanhalingstekens en emoji om in numerieke verwijzingen. Dat is voor HTML niet nodig — een pagina met `<meta charset="utf-8">` toont die tekens gewoon — en het maakt de bron aanmerkelijk langer.

Nuttig is het waar de keten onderweg niet te vertrouwen is: een oud CMS dat de tekenset verhaspelt, een mailsjabloon dat door een systeem gaat dat op Latin-1 blijft hangen, een export die door software van jaren geleden wordt ingelezen. Dan is `&#235;` een teken dat de reis overleeft en `ë` misschien niet.

Ontsnappen is uitvoerhygiëne, geen filter

Ontsnappen hoort thuis op het moment dat tekst in HTML terechtkomt, en niet bij het opslaan. Sla je ontsnapte tekst op in de database, dan weet je later niet meer of een waarde al bewerkt is, en beland je bij dubbele ontsnapping of bij een gat, afhankelijk van welke kant je vergist.

De regel die stand houdt: bewaar de tekst zoals de gebruiker hem heeft ingevoerd, en ontsnap bij het uitvoeren, telkens passend bij de context waarin hij belandt. Elke gangbare sjabloonmotor doet dat standaard, en het uitzetten daarvan is de bekendste manier om een cross-site scripting-gat te maken.

Drie manieren om hetzelfde teken te schrijven

De ampersand kan `&amp;`, `&#38;` of `&#x26;` zijn: een naam, een decimaal getal of een hexadecimaal getal. Alle drie zijn ze geldig en betekenen ze hetzelfde, en een browser leest ze zonder onderscheid.

Voor tekstvergelijkingen is dat lastig: drie schrijfwijzen van dezelfde inhoud vergelijken niet gelijk. Wie HTML-fragmenten met elkaar vergelijkt — in een test, in een diff, in een cachesleutel — moet ze eerst decoderen en dan pas vergelijken, anders meet hij de schrijfwijze en niet de inhoud.

Waarom hier `&#39;` staat en niet `&apos;`

De naam `&apos;` staat in XML en in XHTML, maar niet in HTML 4. Oude browsers die een pagina als HTML 4 lezen, tonen daarom letterlijk `&apos;` waar een apostrof hoorde te staan.

De numerieke vorm `&#39;` heeft dat probleem niet: numerieke verwijzingen werken in elke versie en in elke parser. Het is een klein detail met een lange staart, en het is de reden dat vrijwel elke serieuze ontsnappingsbibliotheek voor de apostrof de numerieke vorm kiest en voor de andere vier de namen.

HTML ontsnappen redt JavaScript niet

Belandt de tekst binnen een `<script>`-blok, dan helpt HTML-ontsnappen niet, want daarbinnen worden entiteiten niet gelezen. Hetzelfde geldt voor een `href` die met `javascript:` begint en voor een waarde die in CSS terechtkomt: elke context heeft eigen regels.

De praktische regel is dat er niet één «veilige» vorm bestaat maar één per bestemming. Voor JavaScript is dat JSON-codering, voor een URL procentcodering, voor CSS de eigen ontsnapping. Wie overal HTML-ontsnapping toepast, heeft drie van de vier gevallen niet afgedekt en denkt van wel.

Waar het juist niet hoort: URL’s en bestandsnamen

Een waarde die in een `href` of een `src` komt te staan, heeft eerst procentcodering nodig en daarna pas HTML-ontsnapping voor het attribuut. Alleen HTML-ontsnappen laat een `&` in de URL staan als parameterscheiding en breekt het adres precies daar.

Bij bestandsnamen geldt iets vergelijkbaars: HTML-entiteiten in een naam die op een schijf terechtkomt, worden geen tekens meer teruggelezen. Ontsnappen is altijd gekoppeld aan de bestemming, en de veelgemaakte fout is het ergens toe te passen waar het niet gelezen wordt.

Twee keer ontsnappen, en hoe dat eruitziet

Zie je op een pagina `&amp;lt;` staan of leest een bezoeker letterlijk `&nbsp;`, dan is er twee keer ontsnapt: eerst in de applicatie en daarna nog eens in het sjabloon, of andersom. Voor geen van beide stappen is dat een fout; samen leveren ze onzin op.

Het is de tegenhanger van dubbele procentcodering in URL’s en de diagnose is dezelfde. Zoek de twee plekken en haal er één weg, in plaats van aan het eind een keer te decoderen — want die reparatie werkt tot de dag dat er ergens maar één keer wordt ontsnapt.

De AVG en tekst van iemand anders

Wat hier wordt ontsnapt is vaak tekst die van gebruikers komt: een reactie, een supportbericht, een productomschrijving met een naam erin. Omdat het op de pagina gebeurt, wordt die tekst niet aan ons verstrekt en staat hij in geen enkel verzoeklogboek van ons.

Dat is precies het geval waarin een onlinehulpmiddel anders lastig te verantwoorden is. Een bericht van een klant in het formulier van een derde plakken om het te ontsnappen, is een verstrekking; hier gebeurt dat niet, en het netwerktabblad laat zien dat er niets vertrekt.

HTML-entiteiten coderen: veelgestelde vragen

Wordt mijn tekst naar een server gestuurd?

Nee. Het ontsnappen gebeurt op deze pagina, in je browser. Dat telt, want wat hier wordt ontsnapt is vaak tekst van gebruikers: reacties, supportberichten, omschrijvingen met namen erin.

Waarom worden mijn trema’s en accenten niet ontsnapt?

Omdat ze de parse niet veranderen. Een pagina met UTF-8 toont ze gewoon. Wil je ze toch als numerieke verwijzing, kies dan de tweede stand — nuttig als de keten onderweg niet met UTF-8 overweg kan.

Beschermt dit tegen XSS?

Voor tekst die in HTML belandt wel, mits het bij het uitvoeren gebeurt en per context. Binnen een script-blok, in een javascript:-URL of in CSS gelden andere regels, en daar helpt HTML-ontsnapping niet.

Waarom zie ik `&amp;lt;` in mijn resultaat?

Omdat de invoer al ontsnapt was en er nu een tweede ronde overheen is gegaan. Zoek de twee plekken in je keten en haal er één weg; aan het eind een keer decoderen is een reparatie die later stukgaat.

Zit er een grens aan de lengte?

Alleen wat je eigen browser in een veld aankan. Er wordt niets geteld of begrensd, want er is geen server die het werk doet.

Andere hulpmiddelen