Tæm din Content Security Policy uden at smadre dit site

Tæm din Content Security Policy uden at smadre dit site

Har du nogensinde slået CSP til, fået 117 fejl i konsollen og slået det fra igen efter 5 minutter?

Hvorfor du overhovedet gider Content Security Policy

Content Security Policy (CSP) er i høj grad et værn mod XSS (Cross Site Scripting) og lignende angreb, hvor en angriber får din side til at køre deres JavaScript i stedet for (eller sammen med) dit eget.

Den helt korte version: CSP er en HTTP-header, der fortæller browseren hvor den må hente scripts, styles, billeder osv. fra, og hvad den må udføre.

Eksempel på en CSP-header helt basic:

Content-Security-Policy: default-src 'self'; script-src 'self'

Det betyder i praksis:

  • default-src 'self' – alt indhold skal komme fra samme origin (samme domæne + protokol + port).
  • script-src 'self' – JavaScript må kun komme fra dit eget domæne.

CSP beskytter især mod:

  • Inline scripts der bliver injiceret via brugerinput
  • Eksterne scripts fra domæner, du ikke har godkendt
  • Nogle typer af data-URLs og farlige indlejrede ting

Men CSP er ikke en erstatning for:

  • At escape brugerinput rigtigt
  • At bruge fornuftig output encoding på serveren
  • At fikse sårbare endpoints med SQL injection, CSRF osv.

Jeg tænker på CSP som det ekstra sikkerhedslag, der fanger de XSS-ting, du ikke lige så komme. Ikke som en undskyldning for at skrive rodet HTML rendering.

Hvis du vil grave i detaljerne, så er MDNs side om CSP rigtig god at have liggende i en fane, mens du bygger.

1. Start med Report-Only, så du ikke brænder alt ned

Fejl nummer ét med Content Security Policy: man starter for hårdt. Sætter en stram policy, deployer, og halvdelen af sitet holder op med at virke.

Løsningen er at starte med Content-Security-Policy-Report-Only. Samme syntaks som CSP, men browseren håndhæver den ikke. Den rapporterer bare overtrædelser.

Sådan ser en simpel Report-Only header ud

Content-Security-Policy-Report-Only: 
  default-src 'self';
  script-src 'self';
  report-to csp-endpoint;
  report-uri https://example.com/csp-report;

report-uri er den gamle måde at få rapporter på, report-to er den nyere model, som kræver en Report-To header. Du behøver ikke starte med rapport-endpoint. Du kan nøjes med at kigge i konsollen.

Hvor du ser CSP-violations i Chrome DevTools

Åbn DevTools, gå på din side, og kig her:

  • Console – fejl a la: Refused to load the script ‘https://example.com’ because it violates the following Content Security Policy directive…
  • Network → Filter på “csp” eller “report” hvis du har rapport-endpoint

Jeg plejer at gøre sådan her, når jeg tester en ny CSP:

  1. Sæt en rimelig stram Report-Only policy
  2. Brug sitet som en normal bruger: log ind, klik lidt rundt, åbn de vigtigste sider
  3. Samle alle CSP-fejl, sortere dem i bunker (egen kode vs tredjeparts scripts)

Pointen: Du vil gerne se, hvad der vil bryde, før du rent faktisk bryder det.

2. Byg en simpel baseline-policy til et almindeligt site

Lad os sige, du har et klassisk server-renderet site eller en let SPA, hvor du:

  • Serverer HTML, CSS og JS fra dit eget domæne
  • Har Google Analytics
  • Loader et par billeder fra et CDN

En minimalistisk, men allerede nyttig CSP kunne ligne noget i stil med:

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
  img-src 'self' https://cdn.example.com https://www.google-analytics.com data:;
  style-src 'self';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'self';

Hvad de vigtigste linjer gør:

  • default-src 'self' – alt går som udgangspunkt kun til dit eget domæne.
  • script-src – tillad scripts fra dig selv + de 2 domæner Google bruger.
  • img-src – tillad billeder fra dig selv, dit CDN, Googles tracking-pixel, og data:-URLs (bruges af nogle libs).
  • style-src 'self' – kun styles fra dig selv, ingen inline styles.
  • object-src 'none' – bloker Flash, plugins osv. Helt fint i 2026.
  • base-uri 'self' – forhindrer at nogen sætter et <base>-tag og ændrer hvordan relative links virker.
  • frame-ancestors 'self' – forhindrer at andre sider iframer dit site (clickjacking-ting).

Du kan køre den her som Report-Only først og se, hvad der brokker sig.

Hvis du bruger et moderne frontend build-setup (Vite, Next, Nuxt osv.), ligger det meste af din JS allerede i statiske filer, og så er det faktisk overraskende let at få en ret stram CSP uden inline scripts.

3. Nonce, hash og ‘unsafe-inline’ – hvad du egentlig bør vælge

Stort spørgsmål: hvordan håndterer du inline scripts og styles?

CSP vil som udgangspunkt blokere inline scripts. Det er godt for sikkerheden, men skidt hvis dit template-system elsker ting som:

<button onclick="doSomething()">Klik mig</button>

<script>
  window.user = {{ user_json | safe }};
</script>

‘unsafe-inline’ – den nemme, men dårlige løsning

Hvis du skriver:

script-src 'self' 'unsafe-inline'

… så siger du i praksis “kør bare alle inline scripts”. Det gør CSP langt mindre effektiv mod XSS. Jeg vil kun anbefale det i to situationer:

  • Legacy-projekt hvor du ikke har tid til at rydde op endnu
  • Midlertidigt, mens du migrerer væk fra inline scripts

Nonce-baseret CSP – god til server-renderede sider

En nonce er en tilfældig streng, du genererer på serveren per request. Den sættes både i headeren og på de inline scripts, du vil tillade.

Header:

Content-Security-Policy: 
  script-src 'self' 'nonce-abc123';

HTML:

<script nonce="abc123">
  window.user = { name: "Lasse" };
</script>

Reglen: nonce-værdien skal være kryptografisk stærk, unik per response, og du må ikke genbruge den på tværs af requests.

Det er ret oplagt, hvis du har en server der alligevel genererer HTML. Du genererer en nonce i middleware, lægger den i request-context, bruger den både til header og til dine script-tags.

Hash-baseret CSP – godt til få, faste inline-klumper

En anden mulighed er at hashe selve indholdet af dine inline scripts.

Inline script:

<script>console.log('hej');</script>

Du laver f.eks. en SHA-256 hash af teksten console.log('hej');, base64-encoder den og smider den i CSP:

Content-Security-Policy:
  script-src 'self' 'sha256-Xfk6l9...';

Fordel: du behøver ikke generere noget per request. Ulempe: hvis du ændrer et enkelt tegn i scriptet, ændrer hash-værdien, og du skal opdatere CSP.

Jeg bruger typisk hash, når jeg har én lille inline-klump, som jeg ved ikke ændrer sig tit. Resten ryger i eksterne .js-filer.

Hvad jeg normalt anbefaler i praksis

  • Nyt projekt: undgå inline scripts helt, brug eksterne filer, så slipper du for nonce/hash.
  • Eksisterende server-renderet projekt: brug nonce og flyt langsomt inline scripts ud.
  • Legacy som ingen tør røre: brug midlertidigt 'unsafe-inline' + Report-Only for næste, mere stramme iteration.

4. Sådan læser du typiske CSP-fejl i DevTools

CSP-fejl lyder tit mere dramatiske end de er. De følger nogenlunde det her mønster:

Refused to load the script 'https://example-cdn.com/script.js' 
because it violates the following Content Security Policy directive: 
"script-src 'self'". Note that 'script-src-elem' was not explicitly set...

Jeg plejer at kigge på tre ting:

  1. Hvilken directive? script-src, style-src, img-src osv.
  2. Hvilket domæne? https://example-cdn.com
  3. Hvor i koden? Stack trace eller request initiator i Network fanen.

Eksempel: du bruger et CDN til JS

Fejl:

Refused to load the script 'https://cdn.example.com/app.js' 
because it violates the following Content Security Policy directive: 
"script-src 'self'".

Løsning: tilføj cdn-domænet til script-src:

script-src 'self' https://cdn.example.com;

Eksempel: inline event handlers

Fejl:

Refused to execute inline event handler because it violates the following 
Content Security Policy directive: "script-src 'self'".

Det er ting som onclick="..." og onload="..." i HTML.

Mulige løsninger:

  • Flyt logikken over i en separat JS-fil og brug addEventListener
  • Hvis du ikke kan lige nu: tilføj nonce og sæt den på <script>-klumpen der definerer funktionen

Eksempel: tredjeparts analytics pixel

Fejl:

Refused to load the image 'https://www.google-analytics.com/r/collect' 
because it violates the following Content Security Policy directive: 
"img-src 'self'".

Løsning: tilføj domænet til img-src:

img-src 'self' https://www.google-analytics.com;

Det samme mønster går igen for fonts (font-src), media (media-src) osv.

5. En 3-ugers plan: fra nul CSP til stram baseline

Hvis du gerne vil have noget, der kan passes ind i en normal hverdag uden at blive et kæmpe projekt, kan du køre sådan her:

Uge 1 – mød din nuværende virkelighed (Report-Only)

  1. Sæt en rimelig stram Content-Security-Policy-Report-Only på staging eller en lille andel af trafikken.
  2. Log CSP-violations til et endpoint (eller brug browserkonsollen manuelt).
  3. Gruppér violations: egne scripts vs tredjepart.

Eksempel på en start-Report-Only header:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data:;
  object-src 'none';

Uge 2 – tilpas din baseline og ryd op i de værste inline-ting

  1. Tilføj de tredjepartsdomæner du faktisk bruger, til relevante directives.
  2. Flyt de mest trivielle inline scripts over i eksterne filer.
  3. Implementer nonce, hvis du har få, men nødvendige inline scripts.

Her kan du med fordel skrive en lille “CSP config” i kode, f.eks. i Node:

const cspDirectives = {
  "default-src": ["'self'"],
  "script-src": ["'self'", "https://www.googletagmanager.com"],
  "style-src": ["'self'"],
  "img-src": ["'self'", "data:", "https://www.google-analytics.com"],
  "object-src": ["'none'"],
};

function buildCspHeader(directives) {
  return Object.entries(directives)
    .map(([key, value]) => `${key} ${value.join(' ')}`)
    .join('; ');
}

Så kan du tilføje/ændre kilder ét sted, uden at håndtere en kæmpe streng manuelt.

Uge 3 – slå delvist over på rigtig enforcement

  1. Flyt din nuværende Report-Only policy over til en rigtig Content-Security-Policy header.
  2. Bevar en lidt strammere version i Report-Only for næste iteration.
  3. Overvåg logning og fejl de første par dage.

En teknik, jeg godt kan lide, er at køre:

  • En “konservativ” policy i enforcement (den du ved virker).
  • En “ambitiøs” policy i Report-Only, der tester næste skridt.

Det føles lidt som feature flags, bare for sikkerhed. Apropos, hvis du ikke arbejder med feature flags endnu, har jeg skrevet om det i artiklen om at skifte til feature flags.

6. Tredjeparts scripts uden at give dem hele butikken

De fleste sites har noget tredjepart: analytics, chat widgets, video embeds, sociale knapper. Det er også der, CSP kan blive irriterende.

Analytics (Google Analytics, Plausible osv.)

Typisk skal du åbne for:

  • script-src – hvor selve trackingscriptet kommer fra
  • connect-src – hvor XHR/fetch til trackingendpoints går hen
  • img-src – hvis der bruges pixel-billeder til tracking

Eksempel for Google Analytics via gtag/gtm (forenklet):

script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
connect-src 'self' https://www.google-analytics.com;
img-src 'self' https://www.google-analytics.com;

Chat widgets

Chat widgets er ofte lidt sultne: de vil have scripts, styles, fonts, images og websockets.

Min erfaring: prøv altid først at se, om leverandøren har en side med CSP-anbefalinger. Nogle har en liste over nødvendige domæner. Du kan også bruge Report-Only til at se, hvad de faktisk loader.

Trin:

  1. Aktiver widgetten i et testmiljø.
  2. Kør med stram CSP i Report-Only.
  3. Notér alle domæner, der rammes i script-src, style-src, font-src, connect-src, img-src.
  4. Overvej, om du virkelig har brug for den widget, hvis den kræver 10 forskellige tredjepartsdomæner.

Embeds (YouTube, maps osv.)

Her er frame-src vigtig.

Eksempel for YouTube embeds:

frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com;

Og så selvfølgelig de tilhørende script-src og img-src domæner, hvis du loader scripts direkte fra dem.

Hvis du bruger meget embeds, kan det også være værd at kigge på hvordan du saniterer indlejret HTML. CSP hjælper, men du vil stadig ikke slippe brugere tæt på rå <iframe>-HTML uden kontrol.

7. En lille tjekliste inden du slår CSP til på produktion

Inden du går fra “Report-Only hygge” til rigtig enforcement, er der et par ting, jeg altid lige kører igennem.

1. Har du kørt en realistisk tur gennem sitet?

  • Log ind / ud (hvis relevant)
  • De vigtigste flows (køb, booking, formularer osv.)
  • Edge-sider: 404-side, fejl-side, profiler osv.

Lad gerne en kollega eller ven klikke lidt rundt, mens du holder øje med konsollen.

2. Har du styr på inline scripts?

  • Har du fjernet trivielle inline event handlers (onclick osv.)?
  • Hvis der er inline scripts tilbage: er de dækket af nonce eller hash?
  • Bruger du 'unsafe-inline', har du taget valget bevidst og noteret hvorfor?

Hvis du er på vej væk fra 'unsafe-inline', så skriv det som en lille tech debt-opgave, og del det op i bidder.

3. Har du dokumenteret dine tredjepartsdomæner?

Lav en lille tabel i dit repo, wiki eller README:

Domæne                      Bruges til                  Direktiver
--------------------------  --------------------------  ---------------------------
https://www.googletag...    Google Analytics script     script-src
https://www.google-an...    GA tracking endpoint        connect-src, img-src
https://cdn.example.com     Eget CDN                    script-src, img-src, style-src

Det gør det meget nemmere, når du tre måneder senere tænker “hvorfor har vi egentlig åbnet for det her domæne?”.

4. Har du besluttet en rollback-plan?

CSP er “bare” en header. Det vil sige:

  • Du kan altid rulle tilbage til en mindre stram version.
  • Du kan hurtigt skifte bagudkompatibelt til Report-Only igen.

Hvis du deployer automatisk, kan du eventuelt lave en feature flag-lignende toggle i din config, der styrer om politiken kører i enforcement eller Report-Only. Samme teknik som med f.eks. release toggles, som jeg skrev om i artiklen om feature flags.

5. Har du en baseline, der giver reel værdi?

Jeg vil hellere have en simpel CSP der fanger 80 % af de åbenlyse XSS-veje, end en teoretisk perfekt CSP som ingen tør slå til.

Et godt realistisk minimum for mange sider er noget i den her stil (tilpas domænerne):

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://www.googletagmanager.com;
  style-src 'self';
  img-src 'self' data: https://www.google-analytics.com;
  connect-src 'self' https://www.google-analytics.com;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'self';

Når den kører stabilt, kan du altid komme tilbage og stramme style-src, tilføje nonces, fjerne flere tredjepartsdomæner osv.

Hvor du kan læse mere og bygge videre

Hvis du vil lidt dybere ned i syntaksen og alle de små faldgruber, er der tre ressourcer, jeg vender tilbage til igen og igen:

Hvis du i forvejen arbejder med ting som fejlbudgetter, secrets og deployment-strategier, hænger CSP egentlig bare på samme knage: små, kontrollerede forbedringer. Du kan fx koble CSP-ændringer på den samme slags simple “incident note” som i artiklen om fejlbudgetter.

Og hvis du ender med at bruge en halv aften på at jagte en enkelt stædig CSP-violation fra en gammel embed, så er du i fint selskab. Jeg har engang brugt længere tid på at få en Content Security Policy til at spille, end jeg brugte på selve featuret, den skulle beskytte. Det føles åndssvagt mens man sidder i det, men det er sjældent spildt tid.

Lasse Falkenberg er typen, der begyndte at rode med HTML og CSS for at lave en simpel bandside – og opdagede, at det var langt sjovere at få knapperne til at virke end at stå på scenen. Siden har han kastet sig over alt fra små JavaScript-snippets til Python-scripts, der kan spare ham for kedeligt, manuelt arbejde i hverdagen.

Han har lært det meste ved at bygge ting, der lige præcis løser hans egne problemer: en lille webapp til at holde styr på brætspilsaftener, et script til at rydde op i rodede mapper, eller en enkel side til at dele noter med venner. Undervejs har han kæmpet sig gennem alle de klassiske fejl – semikolon, forkerte indrykninger og variabler, der hedder noget helt andet end man tror – og det er præcis den rejse, han deler på Coding Class.

På Coding Class skriver Lasse praktiske, jordnære guides, der tager udgangspunkt i små, konkrete opgaver: noget du kan se, teste og bygge videre på med det samme. Han elsker at bryde en opgave ned i små bidder, vise den fulde kode og forklare linje for linje, hvad der sker – inklusive de typiske bugs, du med stor sandsynlighed også støder på.

For Lasse handler kodning ikke om flotte titler eller store ord, men om følelsen af at få noget til at virke – og om at du som læser kan gå derfra med noget, du selv har bygget. Hvis du kan kende glæden ved at få en fejl til endelig at forsvinde, er du lige på bølgelængde med hans måde at lære fra sig på.

Send kommentar

You May Have Missed