Ga naar hoofdinhoud

HTML van een bezoeker: in de browser met innerHTML

Je server escapet nu elke plek waar tekst van een bezoeker in HTML komt. Maar HTML maken kan ook JavaScript, in de browser, nadat de server zijn werk al heeft gedaan. In JavaScript erbij zette je tekst in de pagina met textContent. Er is nog een manier, en die leest de tekst als HTML.

Een voorbeeld van je bericht​

Onder het formulier komt te staan hoe je bericht eruit gaat zien, terwijl je typt. Het berichtveld krijgt een id, en onder het formulier komt een lege <p>. Zet dit in static/gastenboek.html, met een tweede script-tag in de <head>:

<!doctype html>
<html>
<head>
<script src="/static/js/htmx.min.js" defer></script>
<script src="/static/js/voorbeeld.js" defer></script>
</head>
<body>
<form hx-post="/gastenboek" hx-target="#antwoord">
<input type="text" name="naam" placeholder="Je naam" required>
<input type="text" id="bericht" name="bericht" placeholder="Je bericht" required>
<button type="submit">Verstuur</button>
</form>
<p id="voorbeeld"></p>
<p id="antwoord"></p>
<div id="lijst" hx-get="/berichten/lijst" hx-trigger="load, every 5s"></div>
</body>
</html>

Heb je in les 4 een zoekveld boven het formulier gezet, laat dat dan staan.

En maak static/js/voorbeeld.js:

const veld = document.querySelector("#bericht");
const voorbeeld = document.querySelector("#voorbeeld");

function toon() {
voorbeeld.innerHTML = veld.value;
}

veld.addEventListener("input", toon);

const vooraf = new URLSearchParams(location.search).get("bericht");
if (vooraf) {
veld.value = vooraf;
toon();
}

innerHTML zet tekst in een element, net als textContent, maar leest hem als HTML. Het tweede stuk vult het formulier alvast in met een link, bijvoorbeeld om iemand een bericht voor te stellen. location.search is het stuk van de URL vanaf het vraagteken. URLSearchParams haalt daar de waarde van bericht uit, en maakt van codes als %3C weer een <. Staat er geen ?bericht=, dan is vooraf leeg en gebeurt er niets.

Doe het na, op je eigen server​

Open http://127.0.0.1:8000/static/gastenboek.html en typ als bericht <b>vet</b>. In het voorbeeld onder het formulier staat vet, vetgedrukt. Dat is je eigen tekst, in je eigen browser: daar kan nog niemand anders bij. Open nu deze link:

http://127.0.0.1:8000/static/gastenboek.html?bericht=<b>vet</b>

Dit heet DOM-XSS: de HTML van een bezoeker komt in de pagina via JavaScript, in plaats van via de server. De DOM is de pagina zoals JavaScript hem ziet. Een <script> dat zo binnenkomt, voert de browser niet uit, maar HTML heeft andere manieren om code te laten draaien.

Waarom escape op de server hier niet helpt​

Kijk in de terminal van je server. Daar staat één regel met "GET /static/gastenboek.html?bericht=%3Cb%3Evet%3C/b%3E HTTP/1.1" 200. Open je de link nog een keer, dan staat er 304 Not Modified: je browser had de pagina al, en de server zegt alleen dat hij niet veranderd is. Je server stuurde gastenboek.html zoals het in static/ staat. Het stuk na het vraagteken heeft hij niet gebruikt, en er is geen endpoint waar je een escape kunt zetten. De HTML ontstaat pas in de browser, in de functie toon in voorbeeld.js.

Controlevraag
Je opent de link met een vet woord achter ?bericht=, maar je klikt niet op Verstuur. Wat staat er daarna in gastenboek.db?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.

Er gaat iets mis​

Cannot read properties of null​

Het voorbeeld blijft leeg, hoe je ook typt. In de Console staat:

Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')

Oorzaak: document.querySelector("#bericht") vond geen element. Het berichtveld heeft geen id="bericht", of het id is anders gespeld.

Oplossing: geef het berichtveld id="bericht", precies zoals in querySelector, zonder het hekje.

Meer uitleg: Console en Elementen

Opdrachten​

Opdracht 1: Investigate - De server escapet toch?​

Iemand zegt: "Mijn server escapet alles, dus innerHTML in mijn JavaScript kan geen kwaad." Klopt dat?

Tip

Waar komt de tekst in voorbeeld.js vandaan? Is hij langs een endpoint van jou gekomen?

Antwoord

Nee. Het escapen op de server beschermt alleen wat door je endpoints gaat. De tekst uit het veld en uit location.search komt nooit langs een endpoint: de browser leest hem zelf. Die tekst is alleen veilig als je JavaScript hem op een veilige manier in de pagina zet. Hoe dat gaat, zie je in les 8.

Door naar les 8: tekst blijft tekst met textContent.