Sécuriser WordPress : les headers HTTP oubliés que personne ne configure
Il y a un secret bien gardé dans la communauté WordPress : la majorité des sites sont déployés sans la moindre protection au niveau HTTP. Pas de HSTS, pas de CSP, des X-Frame-Options absents. Et le pire ? Ce n’est même pas par négligence, c’est par ignorance systémique.
Le mythe du plugin de sécurité tout-en-un
Parlons franchement. Vous installez Wordfence, iThemes/Solid Security ou n’importe quel plugin à 50 balles par an qui vous promet « une sécurité militaire » et vous pensez dormir tranquille. Sauf que ces outils traitent les symptômes, jamais les causes. Ils scannent les malwares après l’infection, bloquent les IPs après la tentative d’intrusion.
Mais qui s’occupe des headers HTTP ? Personne.
Ces headers, c’est la première ligne de défense. C’est ce qui dit au navigateur : « Non, tu n’exécutes pas ce script inline suspect. Non, tu ne charges pas cette ressource depuis un domaine louche. Non, tu n’embarques pas mon site dans une iframe pour du phishing. »
Et pourtant, la documentation WordPress officielle n’en souffle mot. Les tutoriels vous parlent de permissions de fichiers et de .htaccess magiques, mais jamais de Strict-Transport-Security ou de Content-Security-Policy.
Mesurez l’état actuel de votre site
Avant d’aller plus loin, faites un test rapide : allez sur securityheaders.com et entrez l’URL de votre site WordPress.
Vous avez probablement un F ou un D. Normal, c’est le cas de 90% des sites WordPress.
Notez votre score. Vous allez le transformer en A dans les 5 prochaines minutes.
Comment savoir quelle solution utiliser ?
Test simple : Créez un fichier info.php avec <?php phpinfo(); ?> et cherchez « Server API » :
- Si vous voyez
Apache 2.0 Handler→ Solution 1 (Apache avec mod_php) - Si vous voyez
FPM/FastCGI→ Solution 2 (Apache avec PHP-FPM) - Si vous voyez
nginxdans « Server Software » → Solution 3 (nginx)
Supprimez info.php immédiatement après.
Pas d’accès à phpinfo() ? Demandez à votre hébergeur ou essayez la Solution 1 d’abord (ça fonctionne dans la majorité des cas).
Trois solutions selon votre configuration
Arrêtez de chercher midi à quatorze heures. Voici trois scripts issus de centaines de déploiements en production. Un seul vous concerne. Copiez-collez, testez, passez à autre chose.
Solution 1 : Apache avec mod_php
Créez ou modifiez le fichier .htaccess à la racine de WordPress. Si vous utilisez un serveur dédié/VPS, vous pouvez intégrer directement ces directives dans votre vhost Apache (/etc/apache2/sites-available/votre-site.conf) à l’intérieur du bloc <VirtualHost>. C’est plus performant.
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set Content-Security-Policy "report-uri https://%{HTTP_HOST}"
Header always set Cross-Origin-Embedder-Policy "unsafe-none; report-to=\"default\""
Header always set Cross-Origin-Embedder-Policy-Report-Only "unsafe-none; report-to=\"default\""
Header always set Cross-Origin-Opener-Policy "same-origin-allow-popups; report-to=\"default\""
Header always set Cross-Origin-Opener-Policy-Report-Only "same-origin; report-to=\"default\""
Header always set Cross-Origin-Resource-Policy "cross-origin"
Header always set Permissions-Policy "accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), document-domain=(), encrypted-media=(), fullscreen=*, geolocation=(self), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=*, picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), gamepad=(), serial=(), window-placement=()"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set X-Content-Security-Policy "default-src \"self\"; img-src *; media-src * data:;"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Permitted-Cross-Domain-Policies "none"
Header always set X-XSS-Protection "1; mode=block"
</IfModule>
# Bloquer l'accès aux fichiers cachés (sauf .well-known)
<FilesMatch "^\.(?!well-known)">
Require all denied
</FilesMatch>Langage du code : PHP (php)
Solution 2 : Apache avec PHP-FPM
Avec PHP-FPM, la méthode .htaccess fonctionne pour les headers HTTP de base, mais pour un contrôle total et une exécution avant WordPress, utilisez cette méthode.
1. Créez un fichier .user.ini à la racine de WordPress
auto_prepend_file="/home/votre-compte/public_html/headers.php"Langage du code : JavaScript (javascript)
Note : Utilisez le chemin absolu complet.
2. Créez un fichier headers.php à la racine de WordPress
<?php
header('Strict-Transport-Security: max-age=63072000; includeSubDomains; preload');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Methods: GET, PUT, POST, DELETE');
header('Access-Control-Allow-Origin: null');
header('Content-Security-Policy: report-uri https://' . $_SERVER['SERVER_NAME']);
header('Cross-Origin-Embedder-Policy: unsafe-none; report-to="default"');
header('Cross-Origin-Embedder-Policy-Report-Only: unsafe-none; report-to="default"');
header('Cross-Origin-Opener-Policy: same-origin-allow-popups; report-to="default"');
header('Cross-Origin-Opener-Policy-Report-Only: same-origin; report-to="default"');
header('Cross-Origin-Resource-Policy: cross-origin');
header('Permissions-Policy: accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), document-domain=(), encrypted-media=(), fullscreen=*, geolocation=(self), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=*, picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), gamepad=(), serial=(), window-placement=()');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('X-Content-Security-Policy: default-src "self"; img-src *; media-src * data:;');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('X-Permitted-Cross-Domain-Policies: none');
header('X-XSS-Protection: 1; mode=block');
if (preg_match('/^\./', basename($_SERVER['REQUEST_URI'])) && strpos($_SERVER['REQUEST_URI'], '.well-known') === false) {
header('HTTP/1.0 403 Forbidden');
exit;
}
?>Langage du code : HTML, XML (xml)
Ce fichier sera exécuté avant tout code WordPress, garantissant que les headers sont toujours envoyés.
Solution 3 : nginx (serveur dédié/VPS uniquement)
Éditez votre bloc server nginx (généralement dans /etc/nginx/sites-available/votre-site) :
server {
listen 443 ssl http2;
server_name votre-domaine.com;
root /var/www/votre-site;
# Configuration SSL existante...
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Allow-Methods "GET, PUT, POST, DELETE" always;
add_header Access-Control-Allow-Origin "null" always;
add_header Content-Security-Policy "report-uri https://$host" always;
add_header Cross-Origin-Embedder-Policy "unsafe-none; report-to=\"default\"" always;
add_header Cross-Origin-Embedder-Policy-Report-Only "unsafe-none; report-to=\"default\"" always;
add_header Cross-Origin-Opener-Policy "same-origin-allow-popups; report-to=\"default\"" always;
add_header Cross-Origin-Opener-Policy-Report-Only "same-origin; report-to=\"default\"" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
add_header Permissions-Policy "accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), document-domain=(), encrypted-media=(), fullscreen=*, geolocation=(self), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=*, picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), gamepad=(), serial=(), window-placement=()" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Content-Security-Policy "default-src \"self\"; img-src *; media-src * data:;" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-XSS-Protection "1; mode=block" always;
location ~ /\.(?!well-known) {
deny all;
}
# Reste de votre configuration WordPress...
}Langage du code : PHP (php)
Puis rechargez : sudo systemctl reload nginx
Vérifiez que ça fonctionne
Étape critique : Retournez sur securityheaders.com et testez à nouveau votre site.
Vous devriez maintenant avoir un A ou un A+. Si ce n’est pas le cas :
- Videz le cache de votre navigateur (Ctrl+F5)
- Videz le cache WordPress si vous utilisez un plugin de cache
- Attendez quelques minutes (certains CDN mettent à jour leurs caches)
- Vérifiez que vous avez bien rechargé Apache/nginx si nécessaire
Testez votre site : Naviguez sur vos pages, l’admin WordPress, vérifiez que tout fonctionne normalement. Dans 99% des cas, tout marchera parfaitement.
En cas de problème
Quelque chose ne fonctionne plus après l’ajout des headers ? Voici comment identifier le coupable :
- Ouvrez les DevTools de votre navigateur (F12)
- Allez dans l’onglet Console
- Rechargez votre page (F5)
- Cherchez des erreurs en rouge mentionnant « blocked by », « CORS », ou « Content-Security-Policy »
Si vous voyez des erreurs, vous avez deux options :
- Option rapide : Commentez temporairement la ligne problématique dans votre configuration (ajoutez
#devant sur Apache/nginx, ou commentez avec//dans le fichier PHP) - Option propre : Ajustez la directive concernée pour autoriser la ressource légitime
Cas particuliers connus :
- Fonts externes (Google Fonts, etc.) : Si vos polices ne chargent plus, c’est probablement le CSP. Vous pouvez l’ajuster ou le retirer temporairement.
- Intégrations tierces (YouTube, Maps, etc.) : Même chose, le CSP peut bloquer ces embeddings.
- HSTS et le preload : Une fois activé avec
preload, votre navigateur se souviendra de forcer HTTPS pendant 2 ans. Ne retirez jamais HTTPS après avoir activé HSTS avec preload, vous bloquerez vos visiteurs.
Les excuses que j’entends (et pourquoi elles ne tiennent pas)
- « Mon hébergeur ne me donne pas accès à ces fichiers »
Changez d’hébergeur. Sérieusement. Si vous ne pouvez pas créer un.htaccess, vous êtes sur un hébergement obsolète qui ne mérite pas votre argent. - « C’est trop technique pour moi »
Vous êtes capable de copier-coller du CSS dans l’inspecteur. Copier trois lignes dans un fichier, c’est dans vos cordes. - « Mes plugins ne vont plus fonctionner »
Testez. Dans 99% des cas, tout fonctionnera. Si un plugin casse avec ces headers, c’est qu’il est mal codé. Et vous ne voulez pas de code mal codé sur votre site. - « J’ai un WAF, ça suffit »
Un WAF, c’est génial. Mais ça ne remplace pas les headers de sécurité côté client. Defense in depth, ça vous parle ?
Pour aller plus loin
Cette configuration représente un équilibre entre sécurité et compatibilité, testée sur des centaines de sites WordPress en production. Elle n’est pas parfaite, elle n’est pas exhaustive, mais elle vous met à l’abri de 90% des attaques courantes.
Vous voulez comprendre en détail chaque header ? La documentation MDN est votre amie : HTTP Headers – MDN Web Docs
Vous voulez durcir encore plus ? Une fois ces bases en place, vous pouvez progressivement renforcer votre Content-Security-Policy, ajouter des headers plus spécifiques, monitorer les violations avec des outils comme Report URI.
Mais franchement, commencez par appliquer ce qui est dans cet article. La perfection est l’ennemie du bien, et 90% des sites WordPress n’ont même pas ça.
Conclusion : arrêtez de chercher des solutions miracles
Il n’y a pas de plugin magique qui va sécuriser votre WordPress pendant que vous dormez. La sécurité, c’est du travail, de la rigueur, et des choix assumés.
Les headers de sécurité HTTP, c’est le strict minimum. C’est la base de la base. Si vous ne les avez pas en place, vous êtes vulnérable. Point final.
Alors, qu’est-ce que vous attendez ?
- Testez votre site sur securityheaders.com
- Identifiez votre configuration serveur
- Copiez-collez le bon script
- Retestez et constatez votre note A
Cinq minutes chrono. Aucune excuse.
Parce qu’un site WordPress sans headers de sécurité, c’est comme une maison avec une porte d’entrée grande ouverte : tôt ou tard, quelqu’un finira par entrer.
Cette configuration a été éprouvée par mes soins sur des centaines de déploiements WordPress en production depuis plusieurs années. Elle représente un équilibre pragmatique entre sécurité, compatibilité et facilité de mise en œuvre.