WordPress security headers configureren is een klus die er op papier simpel uitziet, maar in de praktijk verraderlijk is. Een klant uit Middelburg mailde me vorige maand: securityheaders.com gaf een F, en in Google Search Console verscheen een waarschuwing over ontbrekende Content-Security-Policy.
Zijn vorige “WordPress hardening”-script had zomaar een strenge CSP toegevoegd. Gutenberg-blocks en Google Tag Manager laadden niet meer, en de admin was voor de helft wit. We hebben het teruggedraaid en opnieuw opgebouwd, in Report-Only, met precisie.
Reken voor een volledige, veilige implementatie op 2 tot 4 uur werk. Spreid dat over 1 tot 2 weken, zodat je genoeg CSP-violation-logs verzamelt. In dit stuk lees je hoe ik dat systematisch doe, met exacte syntax per hostingpaneel en concrete workarounds voor de plekken waar het meestal misgaat.

WordPress security headers configureren: de vijf must-haves
Voordat je iets configureert, wil je begrijpen wat elke header oplost. Security headers zijn korte HTTP-response-instructies die de browser vertellen wat er wel en niet mag op jouw pagina. Ze compenseren voor kwetsbaarheden in themes, plugins en third-party scripts. Het OWASP Secure Headers Project publiceert de canonieke baseline, en die is in 2026 nog altijd de referentie voor productiesites.
De vijf headers die ik in elke WordPress-implementatie neerzet:
- Content-Security-Policy (CSP). Blokkeert cross-site scripting (XSS) en data-injection door precies te definiëren welke scripts, stijlen, images en frames de browser mag laden. Dit is verreweg de krachtigste header en tegelijk degene die het vaakst dingen breekt.
- X-Frame-Options. Voorkomt dat andere sites jouw pagina in een iframe embedden voor clickjacking-aanvallen. In moderne setups vervangt CSP’s
frame-ancestors-directive deze header, maar veel oudere browsers (en scanning tools) verwachten hem nog. Ik zet beide. - X-Content-Type-Options: nosniff. Blokkeert MIME-sniffing, waarbij de browser probeert te “raden” wat een bestand is. Zonder deze header kan een geüpload .txt-bestand met JavaScript-inhoud alsnog als script uitgevoerd worden. Er is maar één juiste waarde:
nosniff. - Referrer-Policy. Bepaalt hoeveel URL-informatie meegaat wanneer een bezoeker via een link jouw site verlaat. Zonder policy lekt de volledige URL (inclusief sessie-tokens in queryparameters) naar externe partijen.
- Permissions-Policy. Deze vervangt de oude Feature-Policy. Hij regelt welke browser-APIs (camera, microfoon, geolocatie, USB, payment) een pagina mag aanspreken. Voor een reguliere WordPress-site zet je vrijwel alles uit. Zie de MDN Permissions-Policy referentie voor de actuele directive-lijst.
HSTS (Strict-Transport-Security) hoort er ook bij, maar dat is een apart hoofdstuk met eigen preload-lijst en voorwaarden. Daar heb ik een aparte gids voor HSTS Preload op WordPress.
Vergelijkingstabel: syntax per header
| Header | Functie | Simpel syntax | Advanced syntax |
|---|---|---|---|
| Content-Security-Policy | Blokkeert XSS en injectie | default-src 'self' | default-src 'self'; script-src 'self' 'nonce-{random}' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; frame-ancestors 'self'; report-to csp-endpoint |
| X-Frame-Options | Clickjacking-preventie | SAMEORIGIN | DENY (als je site nooit in eigen iframes hoeft) |
| X-Content-Type-Options | MIME-sniffing blokkeren | nosniff | n/a (één geldige waarde) |
| Referrer-Policy | Privacylek beperken | strict-origin-when-cross-origin | no-referrer-when-downgrade (marketing-vriendelijker) of no-referrer (strengst) |
| Permissions-Policy | Camera/mic/geo/USB gate | camera=(), microphone=(), geolocation=() | camera=(), microphone=(), geolocation=(self), payment=(self "https://checkout.mollie.com"), interest-cohort=() |
De keuze tussen “simpel” en “advanced” hangt af van je stack. Een brochuresite zonder formulieren komt weg met de simpele varianten. Een WooCommerce-site met Mollie, GTM en Elementor Pro heeft de advanced kolom nodig om A+ te halen zonder brekage.
Bouw je WordPress security headers-config
Kies je headers en platform. Je krijgt direct een kopieerbaar startconfig met waarschuwingen voor HSTS en CSP.
De vijf basisheaders staan alvast aan. Permissions-Policy is optioneel.
Controleer bestaande headers eerst. Dubbele regels kunnen onverwachte uitkomsten geven.
Een startpunt, geen kant-en-klare productieconfig. Test elke wijziging en controleer de site voordat je CSP enforcement aanzet.
De 7-stap roadmap: van F naar A+ zonder brokstukken
Dit is de volgorde die ik altijd aanhoud. Doe geen enkele stap over of door elkaar.
- Baseline meten (5 minuten). Ga naar securityheaders.com, voer je domein in, screenshot de F- of D-score. Doe direct daarna een tweede scan op Mozilla Observatory. Twee referentiepunten, want ze wegen headers net anders.
- Report-Only mode inschakelen (15 minuten). Voeg alle vijf headers toe, maar de CSP als
Content-Security-Policy-Report-Only. In deze mode blokkeert de browser niks; hij stuurt alleen rapportages over wat er geblokkeerd zou zijn. Zo zie je de brekage aankomen zonder impact op bezoekers. - Logs verzamelen 1 tot 2 weken. Laat de site draaien. Verzamel de violation-reports via een report-endpoint (report-uri.com heeft een gratis tier) of via een simpel eigen PHP-endpoint dat naar een logbestand schrijft.
- Analyseer de violations. Filter op unieke
blocked-uri. Meestal zie je 5 tot 20 unieke domains die je onbedoeld blokkeert: Google Fonts, GTM, Facebook Pixel, YouTube-embed, Stripe, Mollie, je CDN. Elk hoort in de policy of moet weg. - Custom CSP schrijven met specifieke domains. Bouw je policy op basis van de logs. Toets hem tegen Google's CSP Evaluator. Die tool markeert onveilige patronen zoals
'unsafe-inline'zonder nonce, of te brede wildcards. - Report-Only naar Enforce switchen. Verander de header-naam van
Content-Security-Policy-Report-OnlynaarContent-Security-Policy. Vanaf dat moment blokkeert de browser echt. Doe dit buiten kantooruren, en houd de eerste 24 uur violation-logs strak in de gaten. - Post-implementation verify (10 minuten). Nieuwe scan op securityheaders.com. Als je alle vijf headers goed hebt en HSTS eronder, staat er A of A+. Screenshot als bewijsstuk voor je klant of voor je jaarrapport.
Rush geen enkele stap. Ik heb sites gezien die op stap 6 in vier uur alle inline JavaScript van Gutenberg blokkeerden, waardoor de editor voor redacteuren onbruikbaar werd. De Report-Only-fase van 1 tot 2 weken bestaat juist om dat te voorkomen.
Per hostingpaneel: waar zet je de headers neer
De headers moeten op server- of platformniveau. In principe kan het ook via een WordPress plugin (zie verderop), maar server-level is stabieler omdat je headers dan ook stuurt bij statische bestanden en 404's.
Overzicht per paneel
| Paneel | Configuratielocatie | Bestand of UI | Reload nodig |
|---|---|---|---|
| cPanel (Apache/LiteSpeed) | Document root van de site | .htaccess | Nee (direct actief) |
| Plesk | Domain settings | Apache & nginx Settings → Additional nginx directives | Auto-reload nginx |
| DirectAdmin | Per-domein config template | custom_httpd.conf of .htaccess | Apache/LiteSpeed restart |
| Hostinger hPanel | File Manager | .htaccess in public_html | Nee |
| Cloudflare | Dashboard | Rules → Transform Rules → Response Header | Nee (paar seconden) |
| RunCloud/GridPane | Web application settings | Nginx custom config | Auto-reload nginx |
| WordPress fallback | Site zelf | mu-plugins/security-headers.php | Nee |
cPanel .htaccess (Antagonist, Vimexx, Hostnet, Argeweb)
cPanel-hostings draaien meestal Apache of LiteSpeed. Beide lezen .htaccess.
- Log in op cPanel → File Manager.
- Navigeer naar
public_html/(of de subfolder van je site). - Open
.htaccess. Als hij verborgen is: rechtsboven Settings → "Show Hidden Files" aanvinken. - Voeg dit blok toe boven de
# BEGIN WordPress-regel:
<IfModule mod_headers.c>
# De vier veilige headers direct enforce
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(self)"
# CSP eerst als Report-Only voor 2 weken
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; frame-ancestors 'self'; report-uri /csp-endpoint.php"
</IfModule>- Bewaar en refresh de site.
- Test met
curl -I https://jouwsite.nlin een terminal, of gebruik https://securityheaders.com. Je moet alle vijf headers in de respons zien.
Plesk Additional nginx directives (TransIP zakelijk, Combell)
Plesk zet nginx voor Apache als reverse-proxy. Headers zet je in het Plesk-panel zelf, dan wordt nginx opnieuw geladen.
- Domains → jouwsite.nl → Apache & nginx Settings.
- Scroll naar Additional nginx directives.
- Plak in:
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(self)" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; report-uri /csp-endpoint.php" always;- Apply onderaan de pagina. Plesk herlaadt nginx automatisch.
- Test opnieuw via securityheaders.com.
Let op: de always-parameter is belangrijk. Zonder hem stuurt nginx de header niet mee bij 4xx/5xx-responses, wat op securityheaders.com tot puntenverlies leidt bij deep scans.
DirectAdmin custom_httpd (Neostrada, Argeweb-oud)
DirectAdmin gebruikt template-bestanden voor Apache/OpenLiteSpeed. Voor per-domein overrides zet je een custom_httpd config aan.
- SSH-toegang aanvragen bij je hoster, of via Advanced Features → File Manager in DirectAdmin.
- Navigeer naar
/usr/local/directadmin/data/users/{gebruikersnaam}/domains/{domein}.conf/. - Maak (of open)
custom_httpd.confvoor de VirtualHost en voeg toe:
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(self)"
Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-endpoint.php"
</IfModule>- Rebuild de config:
directadmin --build all(root-only). Of via DirectAdmin: Server Manager → Custom HTTPD Configurations → Save. - Herstart Apache/LiteSpeed via het paneel.
Als je géén SSH hebt op DirectAdmin, gebruik dan de .htaccess-route zoals bij cPanel. DirectAdmin ondersteunt dat ook.
Cloudflare Transform Rules → Rewrite Response Headers
Als je site achter Cloudflare zit, is dit de simpelste plek. Je hoeft niks op je server aan te passen, en je kunt regels per URL-patroon maken (bijvoorbeeld strengere CSP voor /wp-admin/* uit).
- Log in op Cloudflare dashboard → kies je zone.
- Rules → Overview → Create rule → Response Header Transform Rule.
- Rule name: "Security headers baseline".
- When incoming requests match: kies "All incoming requests" (of filter op hostname als je meerdere zones hebt).
- Then: klik Modify response header → Operation: Set static → Header name en Value invullen. Voeg voor elk van de vijf headers een aparte "Modify" toe.
- Set:
X-Content-Type-Options=nosniff. - Set:
X-Frame-Options=SAMEORIGIN. - Set:
Referrer-Policy=strict-origin-when-cross-origin. - Set:
Permissions-Policy=camera=(), microphone=(), geolocation=(self). - Set:
Content-Security-Policy-Report-Only= je policy-string.
- Deploy. Werkt binnen enkele seconden globaal.
Cloudflare heeft daarnaast Managed Transforms onder Rules → Managed Transforms met een "Add security headers"-preset. Handig voor snel starten, maar je hebt geen controle over de CSP. Zelf schrijven blijft aan te raden. Zie de Cloudflare docs voor Response Header Transform Rules voor de meest actuele UI.
WP-CLI + functions.php als noodoplossing
Als je géén server-toegang hebt en geen Cloudflare, kun je headers via WordPress zelf sturen. Nadeel: alleen bij PHP-responses, dus niet bij statische assets of 404's van niet-WordPress-bestanden. Toch werkt het voor de meeste scanners.
Voeg toe aan wp-content/themes/{child-theme}/functions.php, of nog beter, in een mu-plugins/security-headers.php:
<?php
add_action('send_headers', function() {
if (!headers_sent()) {
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: camera=(), microphone=(), geolocation=(self)');
header("Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; report-uri /csp-endpoint.php");
}
});Deployen via WP-CLI vanaf je lokale machine (of via SSH):
# Bevestig dat de mu-plugin actief is
wp plugin list --status=must-use
# Test dat de header binnenkomt
wp eval 'wp_remote_get(home_url()); print_r(wp_remote_retrieve_headers(wp_remote_get(home_url())));'Voor productie is dit de minst nette oplossing. Server-level of Cloudflare heeft mijn voorkeur. De WordPress-route is prima voor development of als tijdelijke fix terwijl je op je hoster wacht.

Wat breekt CSP het vaakst (en hoe je het fixt)
CSP breekt vaak omdat WordPress zwaar leunt op inline styles en inline scripts. Daar komen de third-party CDN's van het ecosysteem nog bij. Onderstaande tabel toont de patronen die ik 90% van de tijd tegenkom.
| Element | Waarom breekt CSP | Fix |
|---|---|---|
Inline in header/footer |
Zonder 'unsafe-inline' of 'nonce-' blokkeert de browser elk -blok zonder externe src | Voeg 'unsafe-inline' toe (zwak) of implementeer nonces via wp_get_script_tag()-filter (sterk) |
| Google Fonts | fonts.googleapis.com (CSS) en fonts.gstatic.com (WOFF) zijn twee aparte domains | style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com |
| Google Tag Manager | GTM injecteert extra scripts runtime van variabele hostnames | script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com; connect-src 'self' https://www.google-analytics.com https://analytics.google.com |
| Google AdSense | Ads laden via ep2.adtrafficquality.google en andere subdomains | script-src 'self' https://pagead2.googlesyndication.com https://*.googlesyndication.com; frame-src https://googleads.g.doubleclick.net |
| Gutenberg blocks met inline styles | Block-editor injecteert style="..." attributes op elementen | style-src 'self' 'unsafe-inline' blijft nodig totdat WP core nonces uitrolt (Core issue #57381) |
| YouTube embed | youtube.com/embed/* iframes | frame-src https://www.youtube.com https://www.youtube-nocookie.com |
| Mollie/Stripe checkout | Payment iframes en JS | script-src ... https://js.stripe.com https://www.mollie.com; frame-src https://js.stripe.com https://www.mollie.com |
Elke keer dat je een nieuwe integratie toevoegt (nieuwsbrief-widget, chatbot, webinar-tool), scan je opnieuw en verleng je de policy. Dat is de reden dat CSP een levend document is en geen one-shot config.
Drie concrete scenario's uit de praktijk
Scenario A: A+ score halen zonder Cloudflare
Klant in Terneuzen met een simpele brochuresite op Antagonist. Geen CDN, geen GTM, alleen Google Fonts en een Contact Form 7-formulier.
Aanpak:
- cPanel →
.htaccess→ vijf headers toevoegen. CSP eerst Report-Only. - Report-Only twee weken laten draaien. Enige violation was Google Fonts (verwacht).
- CSP naar enforce, met
default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; frame-ancestors 'self'. - HSTS al aan sinds de SSL-uitrol vorig jaar.
- Eindresultaat: A+ op securityheaders.com. Doorlooptijd 2,5 uur verspreid over 15 dagen.
Scenario B: WordPress admin-toolbar breekt na CSP enable
Bij een webshop in Hulst na de Report-Only-naar-Enforce switch: de zwarte toolbar bovenaan wp-admin renderde niet meer, plus de Gutenberg-editor toonde alleen een spinner.
Diagnose: In browserconsole: Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". WordPress core injecteert honderden inline scripts in de admin.
Fix: Uitzondering voor /wp-admin/*. In .htaccess:
<IfModule mod_headers.c>
# Standaard strenge CSP
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'"
# Ruimere CSP alleen voor wp-admin
<If "%{REQUEST_URI} =~ m#^/wp-admin/#">
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'"
</If>
</IfModule>Voor Cloudflare-users: maak een tweede Transform Rule met filter URI Path starts with /wp-admin/ en een lossere CSP-waarde. Frontend blijft streng, admin blijft werken.
Scenario C: Elementor of WPBakery inline styles versus CSP
Elementor Pro-site met een strenge policy: style-src 'self' (zonder 'unsafe-inline'). Resultaat: alle Elementor-widgets kwijt hun styling. Pagina's zagen eruit als 1998.
Waarom: Elementor genereert per-widget inline styles op Workaround: Ik ben in de praktijk pragmatisch: op een e-commerce site levert Elementor omzet op, dus Draai deze niet één keer maar bouw ze in in je maandelijkse audit. Ik heb een simpele cron met Merk op dat A+ op Mozilla Observatory strenger scoort dan securityheaders.com. Als je op beide A+ wilt, komt Cross-Origin-Opener-Policy en Cross-Origin-Embedder-Policy erbij. Dat is werk voor een advanced sessie, buiten deze baseline. Voor MKB'ers zonder server-toegang bestaan er WordPress-plugins die dit werk overnemen. In mijn praktijk gebruik ik ze zelden omdat server-level altijd stabieler is, maar de goede zijn: Voor kleinere sites (bijvoorbeeld een lokale ondernemer in Zeeuws-Vlaanderen die zelf onderhoudt) adviseer ik Really Simple Security Pro. Voor sites die ik zelf beheer: server-level, altijd. Ik heb sites gezien waar na een WordPress core-update ineens de headers weg waren. Oorzaak: een plugin-update herschreef Zet deze in Doe dit zelf als je een klein tot middelgrote site hebt met max 10 third-party integraties en toegang tot .htaccess of Cloudflare. De roadmap kost je 2 tot 4 uur werk verspreid over 2 weken en je leert ontzettend veel over je eigen stack. In deze gevallen bel je beter iemand in. In mijn praktijk kost een volledige security-header-implementatie voor een MKB-site 2 tot 4 uur (€140 tot €280). Daarin zit 1 tot 2 weken monitoring en een lever-document met de policy-tekst en verantwoording per directive. Voor Elementor- of WooCommerce-sites met veel plugins reken 4 tot 6 uur. Zit je in een Report-Only-log te staren zonder te weten welke violation legitiem is, of is je site na een enforce-switch half stuk? Stuur me een mail via /contact/. Ik kijk binnen 4 uur mee tijdens kantoortijden, factuur alleen de daadwerkelijk bestede tijd (€70/uur), en lever een gedocumenteerde CSP-policy die je aan je hoster of compliance-auditor kunt overhandigen. Zeeuws-Vlaanderen of remote: voor headers maakt de locatie niks uit. Stuur me een mail via /contact/. Binnen 24 uur reactie tijdens kantooruren.-tags. In 2026 is er nog geen structurele fix in Elementor core.
style-src 'self' 'unsafe-inline' accepteren als tijdelijk realiteit. Je verliest 5 punten op securityheaders.com maar Elementor werkt.'unsafe-inline' voor styles blijft. Voor scripts blijf ik streng.Testing tools: welke gebruik je waarvoor
Tool Doel URL securityheaders.com Snelle grade (F tot A+), gewogen scoring per header securityheaders.com Google CSP Evaluator Diepte-analyse van je CSP-string, spot bypasses csp-evaluator.withgoogle.com Mozilla Observatory Grondige check inclusief cookie-attributes en SRI observatory.mozilla.org Hardenize TLS + headers + DNS-security in één rapport hardenize.com Browser DevTools → Console Live violation-messages tijdens ontwikkeling F12 → Console-tab curl -I Ruwe respons-headers uit een terminal curl -I https://jouwsite.nlcurl -I die de headers naar een logbestand schrijft en me mailt als er iets is weggevallen.Voor en na: wat het scheelt in scores
Header ingesteld securityheaders.com Mozilla Observatory Geen (default WordPress) F (0/130) F (0/100) Alleen X-Content-Type-Options + X-Frame-Options D (35) D (40) Vier headers zonder CSP C (65) C (55) Vier headers + CSP Report-Only B (80) B (70) Vijf headers + CSP enforce + HSTS A (105) A (85) Vijf headers + CSP enforce + HSTS Preload + COOP/COEP A+ (120+) A+ (100+) Alternatief: security headers via een plugin

Preventie: headers behouden na updates
.htaccess en gooide het -blok eruit. Dit voorkom je met een monitoring-workflow.
/etc/apache2/conf.d/userdata/ (root only, via je hoster). Op Plesk: in de nginx additional directives (die overleeft plugin-updates). Op Cloudflare: sowieso safe, plugin kan er niet bij.#!/bin/bash
# /usr/local/bin/check-headers.sh
HEADERS=$(curl -sI https://jouwsite.nl)
for H in "X-Content-Type-Options" "X-Frame-Options" "Referrer-Policy" "Permissions-Policy" "Content-Security-Policy"; do
if ! echo "$HEADERS" | grep -qi "$H"; then
echo "MISSING: $H on jouwsite.nl" | mail -s "Security header alert" jij@jouwsite.nl
fi
donecrontab -e als 0 9 1 * * (elke eerste van de maand 9:00).
csp-history.md bij met de policy per datum en de reden van elke wijziging. Bij een violation-onderzoek weet ik precies wanneer we https://cdn.mailchimp.com hebben toegevoegd.Wanneer een webmaster inschakelen
Vast door je headers?
Loop je vast met beveiliging?



