HTML van een bezoeker: in de praktijk
Met escape en templates uit les 3 tot en met 8 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.
Wat jij zelf doet
Escapen bij de uitvoer, niet filteren bij de invoer
Veel mensen willen bij het opslaan alle <script> uit de tekst halen. Dat
werkt niet goed. HTML kent veel meer manieren om code te laten draaien dan
<script>, en een filter mist er altijd een. En een bezoeker kan dan 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.
Wat grote sites nog meer doen
Dit hoef je voor je eigen project niet te bouwen. Klap open wat je wilt lezen.
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.
Dat geldt ook voor de hx-on-attributen van htmx. Van
hx-on::after-request="this.reset()" uit het
htmx-overzicht maakt
htmx een stukje JavaScript, en dat houdt script-src 'self' tegen. De velden
blijven na het versturen gevuld; de enige aanwijzing is een rode EvalError in
de Console. Haal het attribuut dan weg, en zet de reset in een eigen bestand
static/js/leegmaken.js:
document.addEventListener("htmx:afterRequest", (event) => {
if (event.target.tagName === "FORM") {
event.target.reset();
}
});
Laad het in de <head>, onder htmx, met
<script src="/static/js/leegmaken.js" defer></script>. Je kunt de reset ook
weglaten: het formulier werkt zonder.
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 doe je later, 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. Hoe dat gaat, lees je later in Wie mag wat: in de praktijk.
Opdracht: waar moet de bescherming?
Hieronder staan zes 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.")HTMLResponse(f"<p>Je zocht naar: {term}</p>"), mettermuit?term=lijst.innerHTML = veld.valuein een.js-bestand
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.
- Niet veilig: ook de URL komt van de bezoeker. Maak er
{escape(term)}van. - Niet veilig: maak er
lijst.textContent = veld.valuevan.
Door naar de volgende reeks: Te veel verzoeken.