Ga naar hoofdinhoud

HTML van een bezoeker: escapen in Python

escape ken je al uit het htmx-recept Een formulier versturen. Daar stond hij om de naam, zonder dat je zag wat hij doet. In les 2 zag je wat er zonder gebeurt: de browser las <b>vet</b> als HTML, omdat de f-string het letterlijk in de pagina zette.

Waarom escape werkt​

De browser leest een < als het begin van een tag. Dat is de hele zwakheid: hij kan niet zien of die < van jou komt of van een bezoeker. Escapen vervangt de tekens die in HTML iets betekenen door een code die de browser alleen als teken toont: < wordt &lt;, > wordt &gt;, & wordt &amp; en " wordt &quot;. Op het scherm staat dan weer <, maar de browser begint er geen tag mee.

Python heeft er een functie voor, escape uit de module html:

from html import escape

print(escape("<b>vet</b>"))

Uitvoer:

&lt;b&gt;vet&lt;/b&gt;

In de browser staat daar weer <b>vet</b>, met de haakjes erbij. Niets wordt vet.

Op beide plekken in je server​

Je gastenboek zet op twee plekken tekst van een bezoeker in HTML: in het antwoord na het versturen, en in de lijst. Op beide komt escape om de tekst heen:

@app.post("/gastenboek")
async def gastenboek_opslaan(naam: str = Form(...), bericht: str = Form(...)):
with SqliteDict("gastenboek.db") as db:
db[f"bericht_{time.time_ns()}"] = {"naam": naam, "bericht": bericht}
db.commit()
return HTMLResponse(f"Bedankt, {escape(naam)}. Je bericht staat in het gastenboek.")

@app.get("/berichten/lijst")
async def berichten_lijst():
regels = ""
with SqliteDict("gastenboek.db") as db:
for bericht in db.values():
regels += f"<li>{escape(bericht['naam'])}: {escape(bericht['bericht'])}</li>"
return HTMLResponse(f"<ul>{regels}</ul>")
  1. Regel 4:

    Het bericht gaat de database in zoals het getypt is. Escapen doe je pas bij het tonen.

  2. Regel 6:

    De naam in het antwoord na het versturen.

  3. Regel 13:

    De naam en het bericht in de lijst, elk apart.

Zet from html import escape bovenaan main.py, bij de andere imports.

Waar de controle zit​

Het bericht mag de database in zoals het getypt is. De controle zit op de plek waar het de HTML in gaat.

Waar de controle zit: HTML van een bezoeker
  1. Browser: Een bezoeker typt HTML. Bijvoorbeeld <b>vet</b> in zijn bericht. · Tekst of HTML?

  2. Server: Het bericht gaat de database in. Zoals het getypt is. Dat is prima: het gevaar zit pas bij het tonen.

  3. Browser: Een ander vraagt de lijst op. Met hx-get of door de pagina te openen.

  4. Hier zit de controleServer: De lijst wordt HTML. Een .html-template of escape() maakt van < een &lt;. · Escapen in Python

  5. Browser: De browser toont tekst. Je ziet <b>vet</b> met de haakjes erbij; niets van de bezoeker wordt HTML.

Doe de test opnieuw​

Gooi gastenboek.db weg, start de server opnieuw, en vul als Sara weer <b>vet</b> in.

Controlevraag
Je server escapet nu bij het tonen. Wat staat er in gastenboek.db voor het bericht van Sara?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.

Er gaat iets mis​

De naam is in de lijst nog schuin​

Noor vult als naam <i>Noor</i> in en als bericht hoi. Onder het formulier staat haar naam als tekst, maar in de lijst is hij schuin. lijst.py geeft, met het bericht van Sara er nog in:

<ul><li>Sara: &lt;b&gt;vet&lt;/b&gt;</li><li><i>Noor</i>: hoi</li></ul>

Oorzaak: in de lijst staat escape om het bericht, maar niet om de naam. Eén vergeten plek is genoeg.

Oplossing: elke waarde van een bezoeker krijgt zijn eigen escape, ook de naam.

# FOUT
regels += f"<li>{bericht['naam']}: {escape(bericht['bericht'])}</li>"

# GOED
regels += f"<li>{escape(bericht['naam'])}: {escape(bericht['bericht'])}</li>"

Omdat je zo makkelijk een plek vergeet, doet een template het in les 5 voor je.

Opdrachten​

Opdracht 1: Run - Beide plekken​

Zet escape op beide plekken. Vul als naam <i>Noor</i> in en als bericht <b>vet</b>. Wat zie je onder het formulier, en wat in de lijst?

Antwoord

Onder het formulier: Bedankt, <i>Noor</i>. Je bericht staat in het gastenboek. In de lijst: <i>Noor</i>: <b>vet</b>. Alles staat er als tekst, niets is schuin of vet.

Opdracht 2: Predict - escape twee keer​

Wat print dit?

from html import escape

print(escape(escape("<b>")))
Tip

De eerste escape maakt van < een &lt;. Welk teken in &lt; escapet de tweede?

Antwoord
&amp;lt;b&amp;gt;

De tweede keer wordt de & van &lt; zelf ge-escapet, tot &amp;. In de browser zie je dan &lt;b&gt; staan in plaats van <b>. Escape daarom één keer, op de plek waar de tekst de HTML in gaat, en niet ook al bij het opslaan.

Zo ziet je main.py er nu uit
import time
from html import escape

from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from sqlitedict import SqliteDict

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")

@app.post("/gastenboek")
async def gastenboek_opslaan(naam: str = Form(...), bericht: str = Form(...)):
with SqliteDict("gastenboek.db") as db:
db[f"bericht_{time.time_ns()}"] = {"naam": naam, "bericht": bericht}
db.commit()
return HTMLResponse(f"Bedankt, {escape(naam)}. Je bericht staat in het gastenboek.")

@app.get("/berichten/lijst")
async def berichten_lijst():
regels = ""
with SqliteDict("gastenboek.db") as db:
for bericht in db.values():
regels += f"<li>{escape(bericht['naam'])}: {escape(bericht['bericht'])}</li>"
return HTMLResponse(f"<ul>{regels}</ul>")

Beide plekken escapen nu. In les 5 gaat de lijst naar een template.

Door naar les 4: tekst uit de URL.