19 - Security headers : CSP, HSTS et compagnie
Ce que tu vas apprendre
- Content-Security-Policy et comment bloquer les XSS
- HSTS et pourquoi il faut forcer HTTPS partout
- X-Frame-Options, X-Content-Type-Options, Referrer-Policy
- Permissions-Policy pour limiter les APIs du navigateur
- helmet.js et les configs par défaut de Caddy
- Comment auditer tes headers
Prerequisites
- 18 - WebSocket
- Connaitre les bases des attaques XSS et clickjacking
J'ai un aveu a faire. Pendant mes premières annees de dev web, je ne mettais aucun header de sécurité. Aucun. Et ca marchait. Les pages s'affichaient, les formulaires fonctionnaient, les APIs repondaient. Sauf que "ca marche" et "c'est sécurisé" sont deux choses tres différentes. Les headers de sécurité sont ta première ligne de defense cote navigateur. Ils ne coutent rien a mettre en place et bloquent des categories entières d'attaques.
Content-Security-Policy (CSP)
CSP est le header le plus puissant et le plus complexe. Il dit au navigateur exactement d'ou les ressources peuvent etre chargees.
httpContent-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src *; connect-src 'self' https://api.example.com
Traduction : les scripts ne viennent que de mon domaine et de mon CDN. Les styles peuvent etre inline (pas ideal mais pragmatique). Les images de n'importe ou. Les requêtes AJAX uniquement vers mon API.
Si un attaquant injecte <script src="https://evil.com/steal.js"> dans ta page, le navigateur le bloque. Le script ne vient pas d'une source autorisee. C'est la mort de 80% des attaques XSS.
Les directives principales :
default-src: fallback pour tout ce qui n'est pas specifiescript-src: JavaScriptstyle-src: CSSimg-src: imagesconnect-src: XHR, fetch, WebSocketfont-src: policesframe-src: iframesmedia-src: audio et videoobject-src: plugins (Flash, Java). Mets'none', c'est 2026
Le piège de CSP : 'unsafe-inline' et 'unsafe-eval'. Ils desactivent la protection contre les scripts inline et eval(). Beaucoup de frameworks en ont besoin (malheureusement). La solution propre : utiliser des nonces ou des hashes.
httpContent-Security-Policy: script-src 'nonce-abc123'
html<script nonce="abc123">
// Ce script est autorise
</script>
Le nonce doit etre différent a chaque chargement de page. Next.js et d'autres frameworks generent ca automatiquement.
Mode report-only : si tu as peur de casser ton site, commence par :
httpContent-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Le navigateur te signale les violations sans rien bloquer. Tu ajustes ta policy, puis tu passes en mode enforcement.
Strict-Transport-Security (HSTS)
httpStrict-Transport-Security: max-age=63072000; includeSubDomains; preload
Ce header dit au navigateur : "Ne me contacte JAMAIS en HTTP. Toujours HTTPS. Pendant les 2 prochaines annees." Meme si l'utilisateur tape http:// dans la barre d'adresse, le navigateur convertit en https:// avant d'envoyer la requête.
Sans HSTS, la première requête peut etre en HTTP, ce qui ouvre une fenêtre pour une attaque man-in-the-middle. Avec HSTS, cette fenêtre est fermee (sauf la toute première visite).
Le preload va plus loin : tu peux soumettre ton domaine a la HSTS preload list. Les navigateurs incluent cette liste en dur. Meme la première visite sera en HTTPS. paltemps.fr est sur cette liste.
Attention : une fois preloade, c'est quasi irreversible. Si tu perds ton certificat SSL, ton site est inaccessible. Pas de fallback HTTP possible.
X-Frame-Options
httpX-Frame-Options: DENY
Empeche ta page d'etre chargee dans un iframe. Protection contre le clickjacking : un attaquant met ton site dans un iframe invisible et place des boutons par-dessus pour pieger l'utilisateur.
Trois valeurs :
DENY: jamais dans un iframeSAMEORIGIN: uniquement depuis le meme domaineALLOW-FROM https://trusted.com: depuis un domaine spécifique (déprécié, utilise CSPframe-ancestorsa la place)
La version moderne est la directive CSP :
httpContent-Security-Policy: frame-ancestors 'none'
Mais X-Frame-Options est encore utile comme fallback pour les vieux navigateurs.
X-Content-Type-Options
httpX-Content-Type-Options: nosniff
Un seul mot, un header simple, mais critique. Sans ce header, le navigateur peut faire du "MIME sniffing" : deviner le type d'un fichier à partir de son contenu. Un fichier uploade par un attaquant avec du JavaScript dedans pourrait etre interprète comme un script, meme si le serveur dit que c'est du texte.
Avec nosniff, le navigateur fait confiance au Content-Type du serveur. Point final.
Referrer-Policy
httpReferrer-Policy: strict-origin-when-cross-origin
Contrôle ce qui est envoye dans le header Referer quand l'utilisateur clique sur un lien sortant.
Les options utiles :
no-referrer: n'envoie rien. Maximum de confidentialitesame-origin: envoie le referrer uniquement pour les requêtes same-originstrict-origin-when-cross-origin: envoie l'origine (pas le chemin complet) pour les requêtes cross-origin en HTTPS. Bon compromis
Pourquoi c'est important : si ton URL contient des tokens ou des identifiants (/reset-password?token=abc123), le referrer peut les fuiter vers des sites tiers.
Permissions-Policy
Anciennement Feature-Policy. Contrôle quelles APIs du navigateur ton site peut utiliser :
httpPermissions-Policy: camera=(), microphone=(), geolocation=(self), payment=()
Traduction : pas de camera, pas de micro, geolocation uniquement pour mon domaine, pas de paiement. Si un script tiers tente d'acceder a la camera, le navigateur bloque.
C'est particulièrement utile si tu charges des scripts tiers (analytics, ads, widgets). Tu limites ce qu'ils peuvent faire.
helmet.js : la solution express
Si tu utilises Express.js, helmet.js ajoute la plupart de ces headers en une ligne :
javascriptconst helmet = require("helmet");
app.use(helmet());
Par défaut, helmet active :
X-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINStrict-Transport-Security(avec un max-age raisonnable)X-XSS-Protection: 0(désactivé le vieux filtre XSS des navigateurs, qui posait plus de problèmes qu'il n'en resolvait)- Retire le header
X-Powered-By(pas besoin de dire au monde que tu utilises Express)
Pour CSP, tu dois configurer manuellement :
javascriptapp.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.example.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
})
);
Caddy : sécurisé par défaut
Caddy ajoute automatiquement plusieurs headers de sécurité. C'est une des raisons pour lesquelles je le recommande comme reverse proxy. Mais pour CSP, il faut le configurer soi-meme :
example.com {
header {
Content-Security-Policy "default-src 'self'"
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=()"
}
reverse_proxy localhost:3000
}
Auditer tes headers
Va sur securityheaders.com et entre ton URL. Le site analyse les headers de ta réponse et donne une note de A+ a F. La première fois que j'ai teste un de mes sites, j'ai eu un D. Ca pique, mais ca motive.
En ligne de commande :
bashcurl -I https://example.com
Verifie que tu vois bien CSP, HSTS, X-Frame-Options et X-Content-Type-Options dans la réponse.
Résumé
- CSP bloque les XSS en controlant d'ou viennent les ressources
- HSTS force HTTPS et elimine le risque de downgrade
- X-Frame-Options protégé contre le clickjacking
- X-Content-Type-Options empeche le MIME sniffing
- Referrer-Policy évité de fuiter des URLs sensibles
- helmet.js pour Express, config manuelle pour Caddy/Nginx
- securityheaders.com pour auditer rapidement
Article précédent : 18 - WebSocket Article suivant : 20 - Debugger HTTP