# CSRF

## Hva det er

CSRF, kort for cross-site request forgery, er angrepet der en side offeret tilfeldigvis åpner, får
offerets egen nettleser til å sende en forespørsel til et annet nettsted, et der offeret er logget
inn. Nettleseren legger ved nettstedets informasjonskapsler slik den alltid gjør, så forespørselen
kommer fram med offerets økt og ser ut som noe offeret selv valgte å gjøre. Common Weakness
Enumeration fører det som CWE-352 og beskriver feilen fra serverens side: applikasjonen kontrollerer
ikke, eller kan ikke kontrollere, godt nok om en forespørsel bevisst ble sendt av den brukeren den
ser ut til å komme fra.

OWASPs jukselapp om forebygging av CSRF (cheat sheet) lister opp hvor angrepet kan starte, fra et
ondsinnet nettsted til en e-post, et blogginnlegg, en direktemelding eller et program, og hva det
kan oppnå: en pengeoverføring, et endret passord, et kjøp, tildelte privilegier. Hver av dem er en
handling på nettstedet, en endring av tilstand, og det er der jukselappen setter forsvaret: et token
på hver forespørsel som endrer tilstand. CWE legger til at omfanget bare begrenses av offerets
privilegier, fordi angriperen låner offerets identitet. Rettet mot en administrator kan resultatet
bli full kontroll over webapplikasjonen.

CWE behandler CSRF som en sammensatt svakhet, en svakhet som bare finnes der flere andre møtes.
Blant dem er en feil i validering av opprinnelse, CWE-346, og den utilsiktede proxyen eller
«confused deputy», CWE-441: nettleseren har offerets påloggingsinformasjon og bruker den på vegne av
en side som ikke har noen. Det skiller CSRF fra krysskripting, som CWE fører opp som en likestilt
svakhet. Ved XSS kjører angriperens kode inne i målnettstedet; ved CSRF blir den værende på
angriperens side og sender bare forespørsler. Det skiller det også fra CORS, som styrer hva en side
kan lese fra en annen opprinnelse. OWASP påpeker at et vanlig HTML-skjema sender en enkel
forespørsel, en som ikke trenger noen forhåndsforespørsel (preflight), og at nettlesere tillater
slike forespørsler til enhver opprinnelse. CWE staver selv termen Cross-Site Request Forgery i
tittelen, og oppføringen nevner andre navn på det samme, blant dem XSRF og Session Riding.

## Hvorfor det er viktig

CSRF har holdt seg på begge de mest kjente listene over svakheter. CWE-352 var med på CWE Top 25 i
2009, 2010 og 2011, og i hver utgave fra 2019 til 2025. OWASP ga det en egen kategori på Top 10 i
2007 og 2010, som A5, og i 2013, som A8. Siden 2021 har det vært en del av A01, Broken Access
Control, som står øverst i 2025-utgaven. OWASP melder at hver eneste av de testede applikasjonene
hadde en eller annen form for brutt tilgangskontroll, og nevner CWE-352 blant de mest merkbare
svakhetene i kategorien.

Angrepet er vanskelig å utrydde fordi det er nettleseren som gjør jobben. W3C-utkastet om Fetch
Metadata tar utgangspunkt i nettopp det: nettlesere lar seg lett overtale til å sende forespørsler
til et nettsteds endepunkter og ta med påloggingsinformasjonen brukeren allerede har, for eksempel
informasjonskapsler eller en privilegert plass på et intranett, så applikasjoner må være svært nøye
med hvordan disse endepunktene oppfører seg, og det viser seg å være vanskelig. CWE påpeker at
angrepet kan settes i gang fra så å si hvilken som helst tagg eller HTML-konstruksjon, også
bildetagger og lenker. De observerte eksemplene i oppføringen leses som en liste over administrative
handlinger utført fra en annens side: brukerkontoer lagt til gjennom en URL i en `img`-tagg
(CVE-2004-1703), administratorrettigheter skaffet på samme måte (CVE-2004-1842), og et
webgrensesnitt der en forfalsket forespørsel kunne endre passord eller stoppe en virtuell maskin
(CVE-2009-3759).

Skannere hjelper lite. CWE skriver at CSRF i dag er vanskelig å oppdage pålitelig med automatiske
teknikker, fordi hver applikasjon har sin egen underforståtte policy for hvilke forespørsler en
utenforstående kan utløse, og hvilke som krever sterk visshet om at brukeren mente dem. Et verktøy
kan ikke vite at ett skjema er harmløst og det neste flytter penger. Det gjør CSRF til et
designspørsmål: avgjør, forespørsel for forespørsel, hva som beviser at brukeren mente det.

Nettleserne har flyttet seg i forsvarerens favør. OWASPs jukselapp sier at dagens utbredte
nettlesere som standard håndhever SameSite=Lax på informasjonskapsler, noe som holder dem unna de
fleste `POST`-forespørsler på tvers av nettsteder, og at alle de store nettleserne har sendt Fetch
Metadata-headerne siden mars 2023. Begge deler hjelper, og ingen av dem avgjør saken. Utkastet om
informasjonskapsler som definerer SameSite, kaller Lax et rimelig ekstra forsvarslag mot angrep som
bygger på usikre metoder som `POST`, men ikke et robust forsvar mot CSRF som kategori, og råder til
at ikke engang Strict-modus skal være hele forsvaret til et nettsted. Brukere av eldre eller
innebygde nettlesere, legger OWASP til, kan få informasjonskapsler som oppfører seg som om det ikke
var satt noen SameSite-verdi i det hele tatt.

## Slik ser det ut i praksis

Ta en webapplikasjon på `app.example.com` der en innlogget bruker endrer e-postadressen på kontoen
sin med et skjema som sendes med `POST` til `/account/email`. Øktens informasjonskapsel er satt med
`SameSite=None; Secure`, fordi det samme dashbordet også er innebygd i en partners portal, og None
er verdien som lar en nettleser sende en informasjonskapsel i en kontekst på tvers av nettsteder. En
angriper publiserer en side på `prizes.example.net` og får offeret til å åpne den:

```html
<form action="https://app.example.com/account/email" method="post">
  <input type="hidden" name="email" value="mallory@example.net">
</form>
<script>document.forms[0].submit();</script>
```

Nettleseren sender skjemaet så snart siden er lastet, og siden informasjonskapselen tillater det,
følger økten med:

```http
POST /account/email HTTP/1.1
Host: app.example.com
Origin: https://prizes.example.net
Sec-Fetch-Site: cross-site
Content-Type: application/x-www-form-urlencoded
Cookie: session=...

email=mallory%40example.net
```

For serveren er dette en innlogget bruker som endrer adressen sin. Blir ingenting annet kontrollert,
endres adressen, og en tilbakestilling av passordet som sendes til den nye adressen, gir kontoen til
angriperen.

To av headerne i denne forespørselen tilhører nettleseren, ikke angriperen. Fetch-standarden regner
`Origin` blant de forbudte forespørselsheaderne, sammen med alle headere der navnet begynner med
`Sec-`, så et skript på siden kan verken sette eller endre dem. `Origin` oppgir opprinnelsen som
sendte forespørselen, og Fetch Metadata-utkastet definerer fire verdier for `Sec-Fetch-Site`:
`cross-site`, `same-site`, `same-origin` og `none` for en navigasjon brukeren selv startet ved å
skrive inn en adresse eller velge et bokmerke. En kontroll kan lene seg på begge og likevel falle
tilbake på et token for nettlesere som ikke sender noen av dem. En skisse i Python:

```python
import hmac
import secrets

SAFE_METHODS = {"GET", "HEAD", "OPTIONS", "TRACE"}
OUR_ORIGIN = "https://app.example.com"


def new_csrf_token(session: dict) -> str:
    session["csrf_token"] = secrets.token_urlsafe(32)
    return session["csrf_token"]


def csrf_ok(method: str, headers: dict, form: dict, session: dict) -> bool:
    if method in SAFE_METHODS:
        return True
    if headers.get("Sec-Fetch-Site") == "cross-site":
        return False
    origin = headers.get("Origin")
    if origin is not None and origin != OUR_ORIGIN:
        return False
    expected = session.get("csrf_token", "").encode()
    sent = form.get("csrf_token", "").encode()
    return bool(expected) and hmac.compare_digest(sent, expected)
```

Tokenet kommer fra `secrets.token_urlsafe(32)`. Python-dokumentasjonen sier at modulen `secrets`
henter fra den sikreste kilden til tilfeldighet operativsystemet tilbyr, og at 32 byte, altså 256
bit, regnes som nok. Serveren legger tokenet i et skjult felt i hvert skjema den lager, og tar vare
på en kopi i økten. Tokenet havner aldri i en informasjonskapsel, en URL eller en logg, som er
stedene OWASPs jukselapp utelukker for et synkroniseringstoken (synchronizer token).
`hmac.compare_digest` er laget for ikke å stoppe ved den første byten som er ulik, så tiden en
sammenligning tar, avslører ingenting om hvor mye av en gjetning som var riktig. Begge sider kodes
til bytes først, fordi funksjonen bare tar imot tekststrenger når de er rene ASCII. En forespørsel
uten `Origin`-header faller gjennom til tokenet, og `Origin: null` stoppes, siden bare den eksakte
opprinnelsen slipper gjennom.

I en ekte applikasjon bør du ta i bruk rammeverkets beskyttelse før du skriver din egen; det er det
første punktet på OWASPs liste. Mellomvaren i Django krever for eksempel både sin
CSRF-informasjonskapsel og et `csrfmiddlewaretoken`-felt for alle metoder unntatt `GET`, `HEAD`,
`OPTIONS` og `TRACE`, svarer med 403 når et av dem mangler eller er feil, og kontrollerer `Origin`
mot verten og innstillingen `CSRF_TRUSTED_ORIGINS`. Der ingenting trenger å bygge inn applikasjonen,
kan øktens informasjonskapsel også sendes som `__Host-session=...; Path=/; Secure; SameSite=Lax`.
OWASP påpeker at en informasjonskapsel med prefikset `__Host-` ikke kan overskrives fra et annet
underdomene.

Innstillingene i nettleseren er et lag, ikke selve forsvaret. Beskrivelsen vår av hvordan vi
[herdet applikasjonen med nonce-basert CSP](/nb/portfolio/hardened-the-application-with-nonce-based-csp/)
forteller at proxy-mellomvaren i frontenden setter HSTS og SameSite-cookies i tillegg til
Content-Security-Policy, og at rettighetene kontrolleres på nytt på serveren ved hver forespørsel,
mens sperrene i frontenden bare behandles som UX.

## Hvor det går galt

**Tilstandsendringer bak GET.** SameSite=Lax sender fortsatt informasjonskapselen ved en navigasjon
på toppnivå på tvers av nettsteder med en sikker metode, så en lenke offeret klikker på, eller en
side som sender hele vinduet til `https://app.example.com/account/delete?confirm=yes`, kommer fram
innlogget. OWASP kaller dette den vanligste måten forsvar basert på SameSite svikter på i praksis,
og Django bygger beskyttelsen sin på samme premiss: `GET` og de andre sikre metodene skal ikke ha
bivirkninger. Hold `GET`, `HEAD`, `OPTIONS` og `TRACE` fri for bivirkninger, og legg hver endring
bak en metode kontrollene dine dekker.

**SameSite som hele forsvaret.** Utkastet om informasjonskapsler forklarer hvorfor Lax ikke er nok
alene: en angriper kan fortsatt åpne et nytt vindu eller utløse en navigasjon på toppnivå for å få
en forespørsel til å telle som same-site, og forhåndsrendering (prerendering) kan gjøre det samme
uten at brukeren merker det. OWASP legger til at SameSite gjelder det registrerbare domenet, ikke
opprinnelsen. En forespørsel fra en hvilken som helst vert under example.com regnes som same-site
for `app.example.com`, så et glemt underdomene som noen tar over, er allerede innenfor gjerdet.
Behandle SameSite som et ekstra lag, og avgjør på serveren med et token eller en kontroll av
opprinnelsen.

**Et token angriperen kan plante.** I det naive double-submit-mønsteret kontrollerer serveren bare
at en informasjonskapsel og et skjemafelt har den samme tilfeldige verdien. OWASP fraråder nå det
mønsteret, fordi alle som kan skrive informasjonskapsler på domenet, gjennom et sårbart underdomene
ved siden av, en DNS-overtakelse eller en informasjonskapsel injisert over vanlig HTTP, kan sette
begge halvdelene. CWE-oppføringen nevner fortsatt den enkle dobbelt innsendte informasjonskapselen
blant tiltakene sine, så de to kildene er uenige her, og det er OWASPs advarsel man bør følge. Hold
tokenet i økten på serversiden, eller bruk OWASPs signerte double-submit-informasjonskapsel, bundet
til økten med en HMAC.

**Krysskripting på samme nettsted.** Et skript som kjører i dine egne sider, kan lese tokenet fra
skjemaet og sende en forespørsel som er same-origin etter alle mål. OWASPs jukselapp sier det rett
ut: XSS kan sette alle teknikker mot CSRF ut av spill. CWE og Django sier det samme, og CWEs mest
kjente eksempel viser de to i samspill. Samy-ormen brukte XSS til å plante seg selv i en
MySpace-profil og legge til angriperen som venn, og venner som så på profilen, fikk sine egne
profiler endret på samme måte; siden ingen av ofrene valgte å gjøre den endringen, regner CWE CSRF
som en av de grunnleggende årsakene. Behandle hvert XSS-funn som et CSRF-funn også, og rett det
først.

**En kontroll av opprinnelse som slipper gjennom for mye.** En test som
`origin.startswith("https://app.example.com")` godtar også
`https://app.example.com.attacker.example`, fellen OWASPs jukselapp advarer mot. Verdien `null` er
en annen. Etter Fetch-standarden kommer et skjema sendt fra en side med referrer-policyen
`no-referrer` fram med `Origin: null`, og OWASP legger til at omdirigeringer på tvers av
opprinnelser og rammer i sandkasse også kan gi den. Sammenlign hele opprinnelsen med en fast liste
over eksakte strenger, og behandle en manglende header eller `null` som ukjent, aldri som din egen
opprinnelse.

**For stor tillit til forhåndsforespørselen.** En egendefinert forespørselsheader er et solid
forsvar for et API, fordi nettlesere sender en forhåndsforespørsel (preflight) før enhver
forespørsel på tvers av opprinnelser som har en slik header. Det holder bare hvis API-et avviser alt
annet. OWASP advarer om at et JSON-API som også godtar `text/plain`, kan nås fra et skjema, siden
det er en av de tre innholdstypene en enkel forespørsel kan bruke, og at en CORS-policy som tillater
påloggingsinformasjon fra alle underdomener gjennom et regulært uttrykk, lar alle som tar over et
underdomene, sende den egendefinerte headeren. Avvis innholdstyper endepunktet ikke forventer, og
list opp de eksakte opprinnelsene som får sende forespørsler med påloggingsinformasjon.

**Det glemte innloggingsskjemaet.** Login-CSRF går motsatt vei: angriperen logger offeret inn på
angriperens egen konto. På en nettbutikk, slik OWASPs eksempel lyder, legger offeret deretter inn
kortopplysningene sine, og angriperen kan senere handle med dem. Django-dokumentasjonen nevner
login-CSRF blant angrepene beskyttelsen dekker. Beskytt innloggingsskjemaet med et token knyttet til
en forhåndsøkt (pre-session), og ødelegg den økten og start en ny når brukeren har logget inn, slik
OWASP anbefaler.

**Forespørsler bygd av angriperens input.** CSRF på klientsiden (client-side CSRF) oppstår når
applikasjonens egen JavaScript bygger en forespørsel av input angriperen kontrollerer, for eksempel
URL-en, vindusnavnet eller en `postMessage`. Skriptet legger selv ved tokenet, og forespørselen er
same-site, så verken tokener eller SameSite stopper den; OWASP beskriver det som at confused
deputy-problemet kommer tilbake. Hold mål og parametere for forespørsler utenfor rekkevidden til
input angriperen kontrollerer, eller velg dem fra en fast liste i koden.

## Les mer

- [CWE-352: Cross-Site Request Forgery (CSRF)](https://cwe.mitre.org/data/definitions/352.html)
  — svakhetsoppføringen: den sammensatte strukturen, konsekvensene, tiltakene og de observerte
  CVE-ene.
- [OWASP Cheat Sheet Series: Cross-Site Request Forgery Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)
  — tokener, Fetch Metadata, kontroll av opprinnelse, grensene for SameSite og login-CSRF.
- [W3C: Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/)
  — headeren `Sec-Fetch-Site` og de fire verdiene en nettleser kan sende.
- [IETF draft: Cookies: HTTP State Management Mechanism (draft-ietf-httpbis-rfc6265bis)](https://datatracker.ietf.org/doc/draft-ietf-httpbis-rfc6265bis/)
  — attributtet SameSite og hvorfor Lax er et ekstra forsvarslag og ikke en kur; fortsatt et
  Internet-Draft.
- [Django documentation: Cross Site Request Forgery protection](https://docs.djangoproject.com/en/6.1/ref/csrf/)
  — et rammeverks innebygde beskyttelse, hva den forutsetter, og hvor underdomener kommer rundt den.
- [OWASP Top 10:2025: A01 Broken Access Control](https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/)
  — hvor CWE-352 står i 2025-utgaven.

<https://engineer.company/nb/glossary/csrf/>
