De meeste CSP-headers die ik in het wild zie, staan vol met unsafe-inline en unsafe-eval. Dan heb je een sticker die “beveiligd” schreeuwt en een deur die openstaat. Een Content Security Policy WordPress-opzet die écht beschermt begint niet met een header uit een blogpost kopiëren, maar met report-only. En met accepteren dat een 100% strikte CSP op een gemiddelde WordPress-site praktisch bijna niet haalbaar is. Wat wél haalbaar is: een nonce-based CSP met strict-dynamic, waarbij je in vier iteraties van “geen bescherming” naar “serieuze XSS-vangrail” gaat.
Dit artikel loopt de zeven stappen af die ik standaard doorloop op een WordPress-, WooCommerce-, Laravel- of Shopify-project. Header-niveau werkt op elke stack identiek; de details verschillen per plugin en per cache-laag.

Wat een Content Security Policy WordPress doet
Een CSP is een response-header die de browser vertelt uit welke bronnen scripts, styles, afbeeldingen, fonts en verbindingen mogen komen. Alles wat je niet whitelistet, wordt geweigerd. Dat is een aparte beschermingslaag naast CORS (die controleert wie een resource mag lezen) en HSTS (die HTTPS afdwingt).
De praktische winst: als een aanvaller via een plugin-lek een </> uit te voeren en heeft je CSP feitelijk nul waarde tegen script-injectie.
Dat WordPress-core zelf nog steeds inline-scripts spuit is geen geheim; zie de follow-up tickets #51407 (inline event handlers) en #63710 (CSP violations vanuit core), die de discussie na de sluiting van #39941 in WP 5.7 levend houden. De oplossing is niet 'unsafe-inline' accepteren, maar overstappen op nonces of hashes.
Report-only eerst: meet vóór je iets blokkeert
Direct enforcement aanzetten op een productie-site is de snelste manier om je eigen checkout, admin of gutenberg-editor te breken. De juiste volgorde: eerst een report-only-fase van 1 tot 2 weken, dan pas iteratief enforcement opbouwen.

Content-Security-Policy-Report-Only header instellen
De report-only-variant laat de browser doen alsof de policy geldt, zonder daadwerkelijk iets te blokkeren. Overtredingen worden gerapporteerd naar een endpoint dat jij aanwijst. Een minimale start voor een WordPress-site:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://www.googletagmanager.com;
style-src 'self' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self';
report-uri /wp-json/lypii/v1/csp-report;report-uri is officieel deprecated ten faveure van de nieuwere Reporting API (report-to + Reporting-Endpoints-header), maar wordt nog altijd door alle browsers ondersteund en scheelt je een extra header. Wil je toch beide sturen, voeg dan expliciet toe:
Reporting-Endpoints: csp-endpoint="/wp-json/lypii/v1/csp-report"
Content-Security-Policy-Report-Only: ...; report-to csp-endpoint; report-uri /wp-json/lypii/v1/csp-report;Zet de header via een mu-plugin (send_headers action) of via je webserver (nginx/apache). Op Cloudflare kan het ook via een Transform Rule, met de kanttekening dat CF-transformaties na de origin komen, wat soms dubbele headers oplevert.
Rapporten verzamelen zonder betaalde tool
Voor een klein volume is een custom REST-endpoint voldoende: één route die JSON accepteert, valideert en in een dagelijkse rotatielog schrijft. In mijn ervaring krijg je zo in drie dagen 80% van de overtreders in beeld zonder één bezoeker te storen. Wil je liever een dashboard: report-uri.com (naam, geen aanbeveling) heeft een gratis tier.
Van report-only naar enforcement in 4 iteraties
De report-only-logs vertellen je welke directives je nog moet oprekken. Werk in vier ronden, elk 3 tot 5 dagen, en switch pas naar enforcement als het rapportvolume tot bijna nul is gedaald.
- Iteratie 1 (script-src): voeg known-good CDN’s toe (Google Tag Manager, Stripe.js, je eigen theme-assets). Verwijder alle onbekende bronnen na verificatie.
- Iteratie 2 (style-src): page builders zoals Elementor en Divi injecteren veel inline-styles. Kies: hashes toevoegen voor de vaste blocks, of tijdelijk
'unsafe-inline'op style-src (minder erg dan op script-src). - Iteratie 3 (img-src / media-src): meestal ruim
https: data:omdat WooCommerce-producten en gutenberg-embeds vaak externe media laden. - Iteratie 4 (aanscherpen): switch script-src naar
'strict-dynamic' 'nonce-…'en verwijder eventuele host-whitelists.
Bewaar de policy in versiebeheer en test na elke plugin-update of thema-wijziging opnieuw. Elke pluginupdate kan de policy breken.
Nonces vs. hashes vs. strict-dynamic in WordPress
Er zijn drie manieren om inline-scripts alsnog toe te staan zonder 'unsafe-inline'. Ze werken naast elkaar, niet in plaats van elkaar.
- Nonces (
'nonce-') zijn per request unieke tokens die je aan elke inline<script>tag hangt. Perfect voor dynamische output. Nadeel: elke request moet een unieke nonce hebben. - Hashes (
'sha256-…') zijn de SHA-256 van een exacte inline-script-inhoud. Perfect voor statische snippets (analytics, tracker init). Verandert het script één karakter, dan verandert de hash. 'strict-dynamic'vertrouwt scripts die door een vertrouwde loader (met nonce of hash) worden geladen, zonder dat je elke child-script apart hoeft te whitelisten. Redder voor sites met dynamisch geladen scripts.
De officiële referentie voor alle CSP-directives staat op de MDN-referentie voor CSP-directives. Kort houden en dat als bron gebruiken werkt beter dan Stack Overflow-antwoorden knippen en plakken.
Waarom nonces per request lastig zijn met page cache
WP Rocket, LiteSpeed Cache, en Cloudflare Full Page Cache serveren dezelfde HTML aan meerdere bezoekers. Als je nonce in de HTML zit maar de header per request nieuw is, matcht niets meer en gaat alles kapot. Drie oplossingen:
- Cache-fragment (ESI): cache alle blokken behalve
<script nonce="...">. Werkt in LiteSpeed en Nginx-fastcgi met wat pijn. - Server-side injectie na cache: een filter dat vlak vóór output nonces in HTML én header schrijft.
- Hashes +
strict-dynamic: hash alle inline-scripts, zet strict-dynamic op script-src, en accepteer dat de policy niet 100% strikt is voor gebruiker-input.
Optie 3 is voor WooCommerce-shops met caching de enige die zonder maandelijkse regressie werkt.

CSP en veelgebruikte WordPress-plugins (Elementor, WooCommerce, Gravity Forms)
Waar het misgaat, gaat het meestal mis met deze drie:
- Elementor injecteert veel inline-styles voor responsive settings. Ga uit van een groeiende style-src-hash-lijst of geef style-src een
'unsafe-inline'-vrijstelling en accepteer die concessie. - WooCommerce checkout met Stripe of Mollie gebruikt inline-scripts die per sessie iets verschillen. Een nonce is hier de nette weg; hashes werken niet omdat de inhoud varieert.
- Gravity Forms met conditional logic produceert per formulier een inline-blok. Zelfde patroon: nonces per pageload.
Praktisch advies: gebruik in de transitiefase een tijdelijke 'unsafe-hashes' op script-src-attr voor onclick-handlers in oude thema’s. Niet permanent laten staan, wel handig om niet in één weekend elk oude theme uit te bouwen.
CSP op andere stacks: Laravel middleware, Shopify limieten
CSP is stack-onafhankelijk op header-niveau, maar de manier waarop je hem genereert verschilt.
- Laravel: middleware-pattern (
AddCsp) die per request een nonce genereert en die via@cspin Blade beschikbaar maakt. Packages zoalsspatie/laravel-cspdoen dit uit-de-doos. - Shopify:
content_for_headeris beperkt tot wat Shopify toestaat; je hebt geen volledige controle over de outgoing header op standaard-plannen. Alternatief: CSP als<meta http-equiv>tag (subset van directives), of Enterprise-Cloudflare-Workers voor de rest. - Custom PHP: een
header('Content-Security-Policy: …')call vóór eerste output. Simpel, maar zorg dat je framework of MVC-router geen output-buffering-caveats heeft.
Wanneer schakel je Lypii in
CSP inregelen zonder je site plat te leggen kost bij een reguliere WooCommerce-shop 4 tot 6 uur. Reken op €380 tot €570 aan €95 per uur (migratie-, server- en headerwerk tarief). Inclusief report-only fase, iteratie-cycli, en documentatie van de eind-policy.
Spoed na een pentest-rapport waarin CSP als bevinding staat: €105 per uur. Ik werk niet ’s nachts, wel dezelfde week. In een bundel met andere security headers voor je website is het per uur iets goedkoper. En vergeet niet dat CSP een vangnet is voor XSS, geen wondermiddel. Wie z’n plugins nooit update, heeft ook geen boodschap aan een strikte header.
Multi-stack opmerking: dit werkt zoals gezegd op elke stack. Voor WordPress focus je op nonces plus cache-fragment; voor Laravel op middleware; voor Shopify op meta-tags plus edge-workers. De filosofie is identiek: report-only meten, iteratief opbouwen, geen 'unsafe-inline' op script-src.
Verbeteren boven herbouwen: een bestaande site krijgt een strikte CSP zonder dat je hem opnieuw hoeft te bouwen. Meer over XSS voorkomen in PHP en WordPress leest hier vervolgens goed op door.



