Ga naar hoofdinhoud

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?

  1. <p>{{ bericht.bericht }}</p> in berichten.html
  2. HTMLResponse(f"<p>{bericht}</p>")
  3. <p>{{ bericht.bericht|safe }}</p> in berichten.html
  4. HTMLResponse(f"Er staan {len(berichten)} berichten in het gastenboek.")
  5. HTMLResponse(f"<p>Je zocht naar: {term}</p>"), met term uit ?term=
  6. lijst.innerHTML = veld.value in een .js-bestand
Antwoord
  1. Veilig: Jinja escapet in een .html-template.
  2. Niet veilig: maak er {escape(bericht)} van, of gebruik een template.
  3. Niet veilig: haal |safe weg.
  4. Veilig: het getal komt van je eigen code, niet van de bezoeker.
  5. Niet veilig: ook de URL komt van de bezoeker. Maak er {escape(term)} van.
  6. Niet veilig: maak er lijst.textContent = veld.value van.

Door naar de volgende reeks: Te veel verzoeken.