Content Security Policy WordPress: stappenplan voor een werkbare, strikte CSP

Content Security Policy WordPress opzetten zonder je checkout of admin plat te leggen: report-only, nonces en een strikt stappenplan.

· 29 augustus, 2026 · 7 min lezen
In het kort
  • Begin nooit met enforcement. Meet eerst 1 tot 2 weken met Content-Security-Policy-Report-Only.
  • 'unsafe-inline' op script-src maakt je CSP feitelijk waardeloos tegen XSS.
  • Bouw in vier iteraties op: script-src, style-src, img-src, dan aanscherpen met strict-dynamic.
  • Nonces per request werken slecht met page cache; combineer hashes plus 'strict-dynamic' voor WooCommerce.
  • CSP is stack-agnostisch op headerniveau. Denk aan Laravel-middleware, een Shopify-meta-tag of custom PHP werken hetzelfde.
In dit artikel

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.

Content Security Policy WordPress: dashboard naast een nginx CSP-header

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.

CSP Report-Only naast enforcement in devtools

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.

Nonces, hashes en strict-dynamic in CSP-header

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 @csp in Blade beschikbaar maakt. Packages zoals spatie/laravel-csp doen dit uit-de-doos.
  • Shopify: content_for_header is 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.

Serhii Lypii
Gratis meedenken

CSP inregelen zonder je site te breken?

Wil je een werkbare CSP op je WordPress-, Laravel- of Shopify-site? Ik lever een report-only fase, iteratieve enforcement en een fallback voor plugins. Reken op 4-6 uur (€95 per uur) voor een gemiddelde WooCommerce-shop.

Vraag een CSP-uitrol aan
Vond je dit nuttig? Deel het:
FAQ

Veelgestelde vragen over Content Security Policy in WordPress

Kan ik CSP aanzetten op een bestaande WordPress-site zonder iets te breken?
Alleen als je met Content-Security-Policy-Report-Only begint. Zet je meteen enforcement aan, dan is de kans groot dat je checkout, admin of Gutenberg-editor stopt met werken. Reken op 1 tot 2 weken meten voordat je enforcement aanzet.
Welk WordPress-plugin voor CSP raad je aan?
"No unsafe-inline" is het serieuste plugin voor wie richting een strikte CSP wil, maar geen enkel plugin regelt het volledig automatisch. Handwerk blijft nodig, vooral rond je specifieke thema, page builder en checkout-flow.
Werkt CSP met page caching (WP Rocket, LiteSpeed)?
Ja, met hashes en 'strict-dynamic'. Nonce-based CSP werkt alleen als de cache per request de nonce vervangt. Voor de meeste WooCommerce-shops is de combinatie hashes plus strict-dynamic praktisch de enige die zonder regressie draait.
Moet mijn Laravel-site ook een CSP hebben?
Ja. Laravel maakt het zelfs makkelijker: middleware genereert nonces en Blade injecteert die in @csp. Zelfde principe als WordPress, minder gedoe met caching. Packages zoals spatie/laravel-csp scheelt je de meeste boilerplate.
Wat kost het als Lypii dit voor me inregelt?
Reguliere WooCommerce-shop: 4 tot 6 uur ontwikkelwerk à €95 = €380 tot €570. Ik lever tegelijk een report-endpoint, de eind-policy in versiebeheer en documentatie van welke bronnen wel of niet in de policy staan.
Serhii Lypii
Over de auteur

Serhii Lypii

Freelance webmaster · Terneuzen

Sinds 2012 werk ik met websites en leverde ik ruim 210 projecten op. Ik help mkb-ondernemers met onderhoud, snelheid, beveiliging en praktische automatisering. Nuchter, zonder hype.

Ik schreef dit artikel op en werkte het voor het laatst bij op .

Meer over Lypii
Even sparren over je site?

Loop je ergens tegenaan of twijfel je over je site? Ik denk graag mee. Kies hieronder hoe je contact opneemt.

Reactie binnen 4 uur op werkdagen
Navigatie
Home Over mij Kennisbank
Contact
+31 6 39 56 93 03 serhii@lypii.nl WhatsApp
Stuur een bericht