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 <, > wordt >, & wordt & en " wordt
". 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:
<b>vet</b>
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>")
- Regel 4:
Het bericht gaat de database in zoals het getypt is. Escapen doe je pas bij het tonen.
- Regel 6:
De naam in het antwoord na het versturen.
- 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.
Browser: Een bezoeker typt HTML. Bijvoorbeeld <b>vet</b> in zijn bericht. · Tekst of HTML?
Server: Het bericht gaat de database in. Zoals het getypt is. Dat is prima: het gevaar zit pas bij het tonen.
Browser: Een ander vraagt de lijst op. Met hx-get of door de pagina te openen.
Hier zit de controleServer: De lijst wordt HTML. Een .html-template of escape() maakt van < een <. · Escapen in Python
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.
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: <b>vet</b></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 <. Welk teken in < escapet de
tweede?
Antwoord
&lt;b&gt;
De tweede keer wordt de & van < zelf ge-escapet, tot &. In de
browser zie je dan <b> 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.