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.
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.