Un site part en production. Certificat gratuit, redirection du HTTP vers le HTTPS, cadenas affiché dans la barre d'adresse. Le travail semble terminé de ce côté-là.
Puis arrive un audit, un client sensible au sujet, ou une simple curiosité qui pousse à coller l'URL dans un scanner d'en-têtes. La note tombe, elle est mauvaise, et la liste de ce qui manque contient des noms jamais croisés ailleurs : Content-Security-Policy, Strict-Transport-Security, Permissions-Policy.
Ces en-têtes occupent une zone bizarre de l'apprentissage du web. Ils ne sont ni du code applicatif ni de l'infrastructure au sens classique, ils n'apparaissent dans aucun tutoriel de déploiement, et la documentation officielle qui les décrit s'adresse à des gens qui connaissent déjà le modèle de menace.
L'article qui suit prend la suite directe de comprendre HTTP en profondeur, qui décortiquait la requête et la réponse. Même onglet Réseau, même colonne d'en-têtes de réponse, mais cette fois sur les lignes qui changent le comportement du navigateur au lieu de décrire le contenu. À la fin, poser ces en-têtes devient une décision argumentée plutôt qu'un copier-coller.
Le problème : une note qui monte, une protection qui ne bouge pas
La séquence habituelle ressemble à ceci. Le scanner reproche l'absence de Content-Security-Policy. Une recherche remonte un article de blog avec une valeur toute faite. La valeur part dans la configuration du serveur, le scanner repasse au vert, l'affaire est classée.
La politique collée ce jour-là contient presque toujours 'unsafe-inline' dans la directive qui gouverne les scripts, parce que sans ça la page cesse de fonctionner. Cette valeur autorise l'exécution de n'importe quel script écrit directement dans le HTML, ce qui correspond exactement à ce qu'un attaquant injecte lors d'une faille de type XSS. L'en-tête est présent, l'outil le compte, la protection contre le scénario visé est nulle.
L'ampleur du phénomène a été mesurée. Une équipe de chercheurs de Google a analysé les politiques CSP déployées sur plus de 1,6 million d'hôtes et publié ses résultats à la conférence ACM CCS en 2016 sous le titre CSP Is Dead, Long Live CSP!. Conclusion de l'étude : 94,68 % des politiques qui tentent de restreindre l'exécution des scripts sont inefficaces. Le chiffre a dix ans, la cause qu'il pointe reste la même, et c'est ce travail qui a donné naissance au mot-clé 'strict-dynamic' décrit plus bas.
Mon parti pris de formateur sur ce sujet : la note d'un scanner mesure la présence d'en-têtes, pas la sécurité d'une application. Un site qui affiche A+ avec une politique permissive est moins bien protégé qu'un site noté B avec une politique stricte et deux en-têtes secondaires manquants. Viser la note produit du théâtre de sécurité.
Le principe : une consigne donnée au navigateur, pas un correctif
Un en-tête de sécurité est une ligne que le serveur ajoute à sa réponse pour demander au navigateur de s'imposer une restriction. Le serveur n'a aucun moyen d'agir sur la machine du visiteur, il peut seulement formuler une politique que le navigateur accepte d'appliquer.
D'où une conséquence que les listes de configuration passent sous silence : aucun de ces en-têtes ne corrige une faille. Une injection SQL reste une injection SQL, une variable affichée sans échappement reste une variable affichée sans échappement. Ces en-têtes réduisent ce qu'un attaquant peut tirer d'une faille déjà présente, ce qui les place dans la catégorie des défenses en profondeur, au même titre que le principe de moindre privilège.
Les deux en-têtes principaux agissent à des moments différents de l'échange, et cette différence de calendrier explique la plupart des confusions.
HSTS verrouille le transport
Une redirection du HTTP vers le HTTPS laisse subsister une requête en clair : celle qui déclenche la redirection. Un attaquant placé sur le réseau, dans un train ou derrière un point d'accès public, peut intercepter cette première requête et renvoyer sa propre version du site sans jamais laisser le navigateur atteindre le vrai serveur. La technique porte un nom, sslstrip, et elle survit à une redirection correctement configurée.
L'en-tête Strict-Transport-Security, défini par le RFC 6797, ferme cette fenêtre. Quand le navigateur le reçoit sur une réponse HTTPS, il mémorise le domaine et refuse ensuite toute connexion en clair vers lui pendant la durée annoncée, sans même envoyer la requête. Trois directives suffisent à le décrire :
- max-age : la durée de mémorisation, en secondes. La valeur de référence pour un site en régime stable est 31536000, soit un an.
- includeSubDomains : étend la règle à tous les sous-domaines, y compris ceux qui ne sont pas publics et ceux que personne n'a pensé à recenser.
- preload : demande l'inscription du domaine dans une liste embarquée directement dans le code des navigateurs, ce qui protège même la toute première visite.
Cette troisième directive mérite un arrêt, parce que l'avis officiel a changé sans que les articles de blog suivent. Le site hstspreload.org, maintenu par l'équipe Chrome, indique aujourd'hui noir sur blanc que le préchargement n'est pas recommandé. Sa justification : Chrome et Safari basculent déjà automatiquement les navigations HTTP vers HTTPS, ce qui rend le gain marginal face à un désagrément définitif, puisque sortir de la liste prend des mois. La recommandation vise HSTS, elle ne vise plus son préchargement.
Cette bascule automatique franchit d'ailleurs une étape en ce moment même. Google a annoncé sur son blog sécurité que le réglage « Always Use Secure Connections » deviendrait actif par défaut pour tout le monde avec Chrome 154, prévu en octobre 2026 : le navigateur demandera l'autorisation de l'utilisateur avant le premier accès à un site public sans HTTPS. Au moment où ces lignes sont écrites, l'étape précédente est déjà passée, activée en avril 2026 pour les utilisateurs de la navigation sécurisée renforcée.
CSP limite ce que la page a le droit de faire
Le scénario visé par Content-Security-Policy porte le nom de XSS, pour cross-site scripting. Un attaquant parvient à glisser du code JavaScript dans une page, via un champ de commentaire, un paramètre d'URL mal traité ou un nom d'utilisateur affiché sans précaution. Ce code s'exécute ensuite dans le navigateur des visiteurs avec les mêmes droits que le code légitime du site : lecture des cookies de session, envoi de requêtes authentifiées, modification de l'affichage.
CSP n'empêche pas l'injection. Il empêche l'exécution, en donnant au navigateur une liste de ce qu'il a le droit de lancer. Deux façons de rédiger cette liste coexistent, et elles n'ont pas la même valeur.
La première consiste à autoriser des domaines : les scripts de mon site, ceux de tel prestataire, ceux de tel CDN. C'est l'approche intuitive, c'est aussi celle que l'étude citée plus haut démolit. Il suffit qu'un seul domaine autorisé héberge un fichier détourné pour que la politique tombe, et l'étude a trouvé des points faibles dans 14 des 15 domaines les plus fréquemment autorisés.
La seconde approche, appelée politique stricte, oublie les domaines et marque les scripts autorisés. Le serveur génère à chaque réponse une valeur aléatoire, le nonce, qu'il place à la fois dans l'en-tête et dans l'attribut de chaque balise script légitime. Un script injecté ne peut pas deviner cette valeur, donc il ne s'exécute pas. Pour les fichiers statiques, une empreinte cryptographique remplace le nonce. Le mot-clé 'strict-dynamic' étend ensuite la confiance accordée à un script aux fichiers que ce script charge lui-même, ce qui rend l'approche compatible avec les briques tierces sans rouvrir la porte aux injections.
La documentation MDN recommande cette forme, et elle recommande aussi de commencer par l'en-tête Content-Security-Policy-Report-Only, qui signale les violations sans rien bloquer. Cette phase d'observation évite de découvrir en production qu'un script de facturation ne se charge plus.
Les quatre autres lignes utiles
- X-Content-Type-Options: nosniff interdit au navigateur de deviner le type d'un fichier à partir de son contenu. Sans ça, un fichier téléversé par un visiteur peut finir interprété comme du JavaScript.
- Referrer-Policy décide de la quantité d'informations transmises au site suivant quand on clique sur un lien sortant. La valeur
strict-origin-when-cross-originest le défaut des navigateurs actuels, l'écrire explicitement rend le comportement indépendant de leurs choix futurs. - Permissions-Policy coupe l'accès aux fonctions du navigateur dont le site n'a pas l'usage : caméra, micro, géolocalisation, paiement.
- frame-ancestors, directive de CSP, décide qui a le droit d'afficher la page dans un cadre, ce qui bloque le détournement de clic. Elle remplace l'ancien en-tête X-Frame-Options, que les navigateurs ignorent quand les deux sont présents.
Une ligne à supprimer plutôt qu'à ajouter : X-XSS-Protection. MDN classe cet en-tête comme non standard et signale qu'il a introduit des failles dans des sites par ailleurs sains. Chrome a retiré le filtre qu'il pilotait, Firefox ne l'a jamais implémenté. Il traîne encore dans beaucoup de configurations recopiées.
Le déploiement, deux configurations réelles
Les exemples ci-dessous ciblent nginx 1.30, branche stable au moment de la rédaction, et Express 5 côté application. Les directives utilisées sont celles que la documentation de ces versions recommande.
Les en-têtes fixes, côté serveur web
Tout ce qui ne dépend pas de la réponse se pose au niveau du serveur web ou du reverse proxy. Le mot-clé always garantit que l'en-tête part aussi sur les réponses d'erreur, celles où l'oubli se paie le plus cher.
# nginx 1.30, branche stable
server {
listen 443 ssl;
server_name exemple.fr;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
}
Un piège de nginx mérite d'être connu avant la première mise en ligne : les directives add_header d'un niveau supérieur sont héritées uniquement s'il n'y en a aucune au niveau courant. Ajouter un seul add_header dans un bloc location annule silencieusement tous ceux définis au-dessus. La vérification prend une commande, celle du précédent article sur HTTP :
curl -sI https://exemple.fr/une-page-interne | grep -i "strict-transport\|content-security"
Cette ligne demande uniquement les en-têtes de la réponse et filtre les deux qui nous intéressent. Elle mérite d'être lancée sur une page interne autant que sur la page d'accueil. Le maniement de ces outils fait partie du socle travaillé en formation Linux et ligne de commande.
Le nonce, côté application
CSP en version stricte ne peut pas vivre dans la configuration du serveur web, puisque la valeur change à chaque réponse. Elle se pose dans l'application, avant le rendu du gabarit.
// Express 5
import crypto from "node:crypto";
import express from "express";
const app = express();
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString("base64");
res.locals.nonce = nonce;
res.setHeader("Content-Security-Policy", [
"default-src 'self'",
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
"object-src 'none'",
"base-uri 'none'",
"frame-ancestors 'none'"
].join("; "));
next();
});
Chaque balise script légitime reprend ensuite la valeur, injectée par le moteur de gabarit :
<script nonce="{{ nonce }}" src="/js/app.js"></script>
Les quatre directives autres que script-src ferment des voies de contournement documentées : object-src bloque les greffons, base-uri empêche un attaquant de réécrire la base des URL relatives pour détourner le chargement des fichiers.
Avant d'activer le blocage, la phase d'observation. Deux en-têtes travaillent ensemble : le premier donne un nom à l'adresse de collecte, le second désigne ce nom.
Reporting-Endpoints: csp="https://exemple.fr/rapports-csp"
Content-Security-Policy-Report-Only: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; report-to csp
La directive report-to a rejoint le socle interopérable des navigateurs en mars 2026 selon les données de compatibilité de MDN. Garder l'ancienne report-uri à côté reste utile tant que du trafic arrive de navigateurs plus anciens. Compter une à deux semaines de collecte sur un site à trafic modeste avant de basculer en mode bloquant, et brancher cette vérification dans le pipeline plutôt que sur une relecture manuelle, comme le décrit l'article sur les projets qui ne marchent que sur une machine. Une chaîne CI/CD avec GitHub Actions encaisse ce type de contrôle sans effort particulier.
Les pièges qui coûtent cher
Quatre erreurs reviennent plus souvent que les autres. La première coûte une soirée, la deuxième peut coûter un an.
Poser HSTS à un an dès le premier jour
Un navigateur qui a mémorisé une politique HSTS ne l'oublie pas parce que le serveur a changé d'avis. Retirer l'en-tête ne fait rien : les visiteurs qui l'ont reçu restent verrouillés pendant toute la durée annoncée. Avec includeSubDomains et un sous-domaine oublié qui tourne en HTTP, ce sous-domaine devient injoignable pour eux. Le seul retour en arrière consiste à publier un max-age=0 et à attendre que chaque visiteur repasse sur le site.
La montée par paliers recommandée par hstspreload.org évite ce piège, chaque palier durant au moins le temps qu'il annonce.
Servir HSTS sur les mauvaises réponses
L'en-tête est ignoré quand il arrive sur une réponse en HTTP simple, ce qui est logique : un attaquant capable de modifier cette réponse pourrait aussi bien supprimer l'en-tête. Il doit donc partir sur les réponses HTTPS, y compris sur celles qui redirigent. Un site qui redirige exemple.fr vers www.exemple.fr en HTTPS doit poser l'en-tête sur la redirection elle-même, pas seulement sur la page d'arrivée.
Croire qu'un nonce statique fait le travail
Une valeur codée en dur dans le gabarit, ou générée une fois au démarrage de l'application, est lisible par n'importe qui affiche la source de la page. Elle sera reprise par le script injecté. Le nonce doit être aléatoire, imprévisible, et regénéré à chaque réponse. Une page mise en cache avec son nonce pose exactement le même problème, ce qui rend la combinaison CSP stricte et cache HTML partagé nettement moins simple qu'elle en a l'air.
Ajouter 'unsafe-inline' pour faire taire la console
La console signale un script bloqué, la tentation est d'ajouter cette valeur et de passer à autre chose. Détail contre-intuitif : quand un nonce ou une empreinte est présent dans script-src, les navigateurs récents ignorent 'unsafe-inline'. Cette règle sert justement à écrire une politique stricte qui reste tolérable pour les vieux navigateurs. Dans une politique sans nonce, la même valeur annule la protection contre les XSS. Le bon geste consiste à sortir le JavaScript du HTML et à le charger depuis un fichier, ce qui rejoint les habitudes travaillées en formation JavaScript fondamentaux.
Ces sujets ne relèvent plus de la seule bonne pratique pour qui livre des produits logiciels en Europe. Le règlement sur la cyberrésilience impose des obligations de sécurité par défaut, terrain couvert par la formation Git, GitHub Actions et Cyber Resilience Act.
Questions fréquentes
Ces en-têtes remplacent-ils la validation des données côté serveur ?
Par quel en-tête commencer sur un site déjà en ligne ?
Faut-il encore envoyer X-Frame-Options ?
Comment annuler une politique HSTS posée par erreur ?
Une politique CSP peut-elle se déclarer dans une balise meta ?
CSP casse mes outils de mesure d'audience, que faire ?
Faut-il connaître ces en-têtes pour un premier poste de développeur ?
Un exercice pour finir, sur un projet personnel déjà en ligne : lancer curl -sI sur trois pages différentes du site, dont une page d'erreur 404, et comparer les en-têtes reçus. Les écarts entre les trois racontent en général une histoire sur la configuration du serveur.
Reste la question qui décide de tout le reste : est-ce que ce site a un endroit où du contenu écrit par un visiteur ressort à l'écran. Si oui, CSP passe devant tout le reste de la liste.