XSS: når fremmed kode får snakke
Reflektert og lagret XSS — og forskjellen på innerText og innerHTML.
XSS — cross-site scripting — er når fremmed kode får kjøre i brukerens nettleser, inne i *ditt* nettsted. Det er den vanligste sikkerhetsfeilen i webutvikling, og den starter med noe så uskyldig som et kommentarfelt.
Et kommentarfelt på Bakeriet i Bakkanten
Bakkanten — en liten fiktiv bakeributikk vi bruker gjennom kurset — har et kommentarfelt under hver vare. Besøkende kan skrive «Beste kanelbollene i byen!», og siden viser kommentaren under produktet. Slik håndterer en naiv versjon av siden det:
// Innholdet som kommer fra kommentarfeltet kan være hva som helst:const kommentar = "<script>alert('oi!')</script>"; // Naivt: innholdet settes inn som HTML ...element.innerHTML = kommentar; // ... og nettleseren bygger en ekte <script>-tag.// Koden kjører nå hos *alle* som åpner siden.Hva som skjedde der: teksten fra feltet krysser grensen fra «innhold» til «HTML» uten å bli stoppet. Vinkelparentesene ble ikke tegn lenger — de ble syntaks. Og siden alle som åpner produktet får samme kommentar fra databasen, kjører koden hos dem alle.
Obs
Hvorfor kjører vi ikke dette eksempelet?
Et alert-vindu i en sandkasse beviser ingenting — vi lærer ingenting av å se et bråk, og vi viser aldri fremmed kode som noe annet enn dødt materiale. I kursets øvelse (leksjon 3) gjør vi i stedet det som faktisk gir utbytte: vi skriver koden som gjør skriptet *ufarlig*.
Reflektert eller lagret
- Reflektert XSS — den farlige teksten kommer tilbake fra selve forespørselen, for eksempel fra et søkefelt eller en feilmelding som gjentar det du skrev. Offeret må lokkes til å klikke en lenke — hver gang.
- Lagret XSS — den farlige teksten lagres i databasen først, som Bakkantens kommentar. Så kjører den hos *alle* som laster siden, helt til noen fjerner den. Dette er den farligste varianten.
textContent setter tekst, innerHTML setter HTML
Forskjellen er altså ikke angrepet — det er samme grenseoverskridelse begge steder, men hvor teksten bor bestemmer hvor mange som rammes. Og i nettleseren ligger hele forsvaret bokstavelig talt i én egenskap. Statisk, som de andre figurene her — les de to linjene som to ulike avtaler med nettleseren:
element.innerHTML = "<b>hei</b>";// Løftet: «tolk dette som HTML» — og <script> er da HTML også. element.textContent = "<b>hei</b>";// Løftet: «vis dette som tegn» — parenteser blir parenteser. Punktum.Det er hele grepet: når innholdet skal være tekst — navn, kommentarer, søkestrenger — bruk textContent (eller innerText). Da kan innholdet uansett hva det inneholder bare *vises*. innerHTML er til HTML *du* har skrevet eller generert fra kilder du stoler fullstendig på — aldri til data som kom utenfra.
Samme idé på serveren
Grensen finnes overalt der data blir skrevet inn i et annet språk. På serveren er det escape ved utskrift: & blir &, < blir <, > blir > — tegnene beholder sin betydning *for deg*, men mister evnen til å lage tagger. Moderne malspråk og rammeverk gjør dette automatisk: React escaper som standard, mustache-stilen {{ }} i mange maler er escaped output, og i Next.js-rendering er det du skriver i JSX aldri rå HTML. Men «automatisk» er ikke «overalt»: så fort du skriver innerHTML for hånd, bygger HTML fra data, eller bruker en luke som dangerouslySetInnerHTML (navnet sier det selv), er du tilbake ved grensen — og da er det du som holder utstyret.
Tips
Regelen å huske
Data inn, tekst ut. Hvert sted du setter innhold fra brukere inn i en side — med textContent, med templates, med malspråk — er et sted du enten rens eller lekker. Leksjon 3 er denne leksjonens ryggrad: du skriver selve rensefunksjonen.
Du kan nå
- forklare hva XSS er: fremmed kode som kjører i brukerens nettleser
- si forskjellen på reflektert og lagret XSS — og hvorfor lagret er verst
- velge textContent når innholdet er tekst, og vite hvorfor innerHTML er farlig