Aller au contenu principal

CSP et HSTS : les en-têtes qui protègent ce que HTTPS ne protège pas

Le site est en ligne, le cadenas est vert, et un outil d'audit renvoie une note F. Cet article explique ce que chaque en-tête de sécurité demande au navigateur, comment déployer HSTS sans bloquer un sous-domaine pendant un an, comment écrire une politique CSP qui protège au lieu de décorer, et pourquoi la liste copiée-collée d'un blog ne suffit pas.

Guides & tutoriels ·
Adel LATIBI
Adel LATIBI

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

En vous inscrivant, vous acceptez de recevoir notre newsletter. Désinscription possible à tout moment.

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.

1. Avant l'envoi 2. Reponse recue 3. Page executee HSTS force le https en-tetes lus CSP filtre les scripts transport contenu Un aller-retour HTTP
HSTS s'applique à la requête suivante, CSP s'applique à la page en cours d'affichage.

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-origin est 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.

300 = 5 min 604800 = 1 sem. 2592000 = 1 mois 31536000 = 1 an Valeur de max-age, avec includeSubDomains a chaque palier
Chaque palier reste en place au moins le temps qu'il annonce, pour laisser remonter les pages cassées.

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 ?

Non. Ils limitent l'exploitation d'une faille, ils ne la ferment pas. L'échappement des données affichées, les requêtes préparées et le contrôle des droits restent le premier rempart. CSP arrive derrière, pour le jour où l'un de ces remparts cède.

Par quel en-tête commencer sur un site déjà en ligne ?

Par X-Content-Type-Options, Referrer-Policy et Permissions-Policy : trois lignes de configuration, aucun risque de casse sur une application classique. HSTS ensuite, avec un max-age court le temps de vérifier que chaque sous-domaine répond en HTTPS. CSP en dernier, parce que c'est le seul qui demande de toucher au code.

Faut-il encore envoyer X-Frame-Options ?

La directive frame-ancestors de CSP couvre le même besoin avec plus de finesse, et les navigateurs actuels lui donnent la priorité quand les deux sont présents. Garder l'ancien en-tête coûte une ligne et sert encore les navigateurs très anciens. Envoyer les deux avec des règles contradictoires produit surtout de la confusion au moment du diagnostic.

Comment annuler une politique HSTS posée par erreur ?

En servant l'en-tête avec max-age=0 sur les réponses HTTPS du domaine concerné. Chaque navigateur oubliera la politique à sa prochaine visite, ce qui peut prendre des semaines selon la fréquentation. Pour un poste de développement, les navigateurs proposent un écran de gestion des politiques enregistrées, accessible dans Chrome à l'adresse chrome://net-internals/#hsts. Un domaine inscrit sur la liste de préchargement demande une démarche séparée et plusieurs mois.

Une politique CSP peut-elle se déclarer dans une balise meta ?

En partie. La balise meta http-equiv accepte la plupart des directives, ce qui dépanne quand les en-têtes ne sont pas modifiables, par exemple derrière un hébergement statique fermé. Plusieurs directives y sont ignorées, dont frame-ancestors, sandbox et celles qui pilotent l'envoi des rapports, et la politique ne s'applique qu'à partir du moment où le navigateur lit la balise. L'en-tête HTTP reste la forme complète.

CSP casse mes outils de mesure d'audience, que faire ?

C'est le cas d'usage qui a motivé strict-dynamic. Le script de mesure reçoit le nonce, et les fichiers qu'il charge ensuite héritent de cette confiance sans qu'il faille autoriser des dizaines de domaines. Les rapports de violation collectés pendant la phase d'observation disent précisément quelles ressources ont été bloquées et depuis quelle page.

Faut-il connaître ces en-têtes pour un premier poste de développeur ?

Savoir qu'ils existent, savoir les lire dans l'onglet Réseau et pouvoir expliquer en deux phrases ce que fait CSP suffit largement à un entretien junior. La configuration fine relève souvent d'une personne dédiée à l'infrastructure. Un candidat qui aborde le sujet spontanément se distingue, parce que la question tombe rarement dans les parcours d'apprentissage classiques.

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.

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse - publiez un article invité et gagnez en visibilité.

Écrire pour nous