HTML van een bezoeker: in de praktijk
Met templates en escape uit de oplossing heb je het
belangrijkste gedaan. Echte sites doen er nog wat bij, omdat één vergeten plek
genoeg is.
Waarom XSS zo gevaarlijk is
Een script dat via je gastenboek in de pagina komt, draait in de browser van elke bezoeker, alsof het van jouw site is. Het kan dus alles wat die bezoeker op jouw site kan: verzoeken sturen met zijn sessie, de pagina veranderen, of een nep-inlogformulier tonen. De bezoeker ziet het adres van jouw site en heeft geen reden om te twijfelen.
Daarom staat XSS al twintig jaar in elke lijst van de meest gevonden zwakheden in webapplicaties.
Escapen bij de uitvoer, niet filteren bij de invoer
Een idee dat vaak opkomt: haal bij het opslaan alle <script> uit de tekst. Dat
werkt niet goed. HTML kent veel meer manieren om code te laten draaien dan
<script>, en een filter mist er altijd een. Bovendien kan een bezoeker niet
meer schrijven over <b> zelf, terwijl dat in een gastenboek over HTML heel
normaal is.
Escapen werkt andersom. Je laat de tekst zoals hij is, en zorgt op de plek waar hij in de HTML komt dat hij tekst blijft. Dan maakt het niet uit wat erin staat.
Een tweede slot: Content-Security-Policy
Een site kan de browser vertellen welke scripts hij mag uitvoeren. Dat heet een Content-Security-Policy, en het is een regel in de kop van elk antwoord. Met een kleine functie die FastAPI bij elk antwoord aanroept, zet je hem op al je pagina's:
@app.middleware("http")
async def veiligheidskoppen(request, call_next):
antwoord = await call_next(request)
antwoord.headers["Content-Security-Policy"] = "script-src 'self'"
return antwoord
script-src 'self' betekent: alleen scripts uit bestanden van deze site, zoals
/static/js/htmx.min.js. Een script dat midden in de HTML staat, voert de
browser dan niet uit, ook niet als het door een vergeten f-string binnenkwam.
In de Console zie je een melding dat de Content-Security-Policy het
tegenhield.
Het is een tweede slot en geen vervanging, dus escapen blijft nodig. Eigen
JavaScript moet dan wel in een .js-bestand staan, niet in een <script> in je
HTML.
Cookies buiten bereik van scripts
Een script in de pagina kan met document.cookie de cookies van die pagina
lezen, ook het sessie-id. Met één instelling in set_cookie houd je de
sessie-cookie daarbuiten. Dat staat in de reeks
Cookies afschermen.
Op een site van een ander
Of een site van een ander escapet, test je niet door er HTML in te vullen. Dat is een aanval op die site en op de bezoekers ervan. Zie je per ongeluk dat tekst als HTML verschijnt, meld het dan bij de eigenaar, zoals staat in Wie mag wat: in de praktijk.
Opdracht: waar moet de bescherming?
Hieronder staan vier stukken code. Welke zijn veilig voor tekst van een bezoeker, en wat moet er anders bij de rest?
<p>{{ bericht.bericht }}</p>inberichten.htmlHTMLResponse(f"<p>{bericht}</p>")<p>{{ bericht.bericht|safe }}</p>inberichten.htmlHTMLResponse(f"Er staan {len(berichten)} berichten in het gastenboek.")
Antwoord
- Veilig: Jinja escapet in een
.html-template. - Niet veilig: maak er
{escape(bericht)}van, of gebruik een template. - Niet veilig: haal
|safeweg. - Veilig: het getal komt van je eigen code, niet van de bezoeker.