X-Frame-Options : ce que l'en-tete autorise, et ce qu'il ne dit pas
X-Frame-Options est l'en-tête de réponse HTTP qui dit au navigateur si une page peut s'afficher dans un cadre. DENY refuse tout encadrement, SAMEORIGIN n'autorise que votre propre origine, et ALLOW-FROM est ignoré des navigateurs modernes : présent, il ne protège rien. La page donne la commande pour lire la réponse finale après redirection, le correctif exact par pile, et ce que ce contrôle ne dit pas.
1. Le fait
X-Frame-Options est un en-tête de réponse HTTP. Il dit au navigateur si la page reçue peut s'afficher dans un cadre : iframe, object, embed, une fenêtre découpée dans une autre page. Trois valeurs : DENY (aucun cadre, jamais), SAMEORIGIN (seulement votre propre origine), ALLOW-FROM (un site nommé, obsolète, section 3). Sans lui, et sans frame-ancestors, n'importe quel site peut afficher votre page dans un cadre.
2. Pourquoi ça existe
Un cas concret : votre back-office a un bouton "Supprimer le compte" à position fixe. Une page attaquante affiche un contenu anodin et charge votre back-office par-dessus, dans un iframe invisible : opacité quasi nulle, positionné pour que le bouton tombe sous le curseur. Le clic est un vrai clic, dans votre page, avec la session du visiteur ouverte.
Le jeton anti-CSRF ne protège pas : il est dans la page que l'attaquant encadre, et il part normalement. Le serveur voit une requête légitime. L'attaquant n'a besoin d'aucun accès à votre serveur : une page et un clic au bon endroit.
Un iframe de paiement rend le même service à l'attaquant : il fait confirmer un montant ou désigner un bénéficiaire. Votre panneau d'administration aussi : accorder un droit, changer une adresse e-mail, tout se fait au clic. L'en-tête est apparu avec Internet Explorer 8 pour rendre ce montage impossible : le navigateur refuse le cadre non autorisé avant même que le clic existe.
3. Ce que la valeur autorise vraiment
DENY refuse tout encadrement, y compris par votre propre site, et casse les iframes internes : volet d'aperçu, vue d'impression.
SAMEORIGIN autorise les pages de la même origine. Une origine n'est pas un domaine : c'est le schéma, l'hôte et le port, donc https://app.exemple.fr ne peut pas encadrer https://www.exemple.fr. Elle s'applique à tous les ancêtres d'un cadre imbriqué, pas seulement au parent direct.
ALLOW-FROM est le piège central. MDN le classe obsolète et écrit que les navigateurs modernes qui le rencontrent ignorent l'en-tête complètement. Le résultat n'est pas une protection affaiblie, c'est aucune protection.
Un ALLOW-FROM est plus dangereux qu'une absence, et la raison est humaine. Une absence se voit tout de suite. Un ALLOW-FROM ressemble à une protection configurée. Il rassure celui qui relit le fichier et il est compté "présent" par les outils qui vérifient le nom de l'en-tête. La page reste encadrable par n'importe qui.
Dernier cas : deux valeurs, quand un CDN l'ajoute en plus de l'origine. Mozilla a documenté en 2012 qu'une valeur dupliquée faisait ignorer l'en-tête.
4. Vérifier soi-même
curl -sI https://votre-site.fr | grep -i x-frame-optionsCe qu'on doit lire : DENY ou SAMEORIGIN. Rien : trois causes possibles. L'en-tête n'est pas configuré. Ou il est posé sur la 200 seulement, et la commande ci-dessus, qui ne suit pas la redirection, lit la 301. Ou le serveur répond mal à HEAD, et il faut refaire la mesure en GET. ALLOW-FROM, aucune protection quel que soit le site nommé. SAMEORIGIN, SAMEORIGIN ou deux lignes successives, valeur dupliquée à corriger.
Apex vers www : la commande ne suit pas les redirections, et une 301 ne porte souvent pas l'en-tête. C'est la réponse finale qui compte.
curl -sIL https://votre-site.fr | grep -iE "^(HTTP/|location:|x-frame-options)"La sortie montre chaque saut ; l'en-tête doit apparaître dans le dernier bloc, celui de la 200. S'il n'est que dans le premier, la page finale n'est pas couverte alors que la configuration semble l'être. Si le résultat est vide alors que l'en-tête est configuré, refaites la mesure en GET : certains serveurs traitent HEAD à part.
curl -s -D - -o /dev/null https://votre-site.fr | grep -i x-frame-optionsUne page ne dit rien des autres : vérifiez la page de connexion et une route authentifiée.
5. Corriger, selon la pile
Next.js (next.config.js) :
// next.config.js
module.exports = {
async headers() {
return [{ source: '/:path*', headers: [{ key: 'X-Frame-Options', value: 'SAMEORIGIN' }] }]
},
}Si deux règles posent la même clé, la dernière gagne, et cette configuration ne sait pas retirer un en-tête : une route à laisser encadrer doit sortir de la règle.
Vercel (vercel.json, à la racine) :
{ "headers": [{ "source": "/(.*)", "headers": [{ "key": "X-Frame-Options", "value": "SAMEORIGIN" }] }] }Choisissez un seul endroit. Si Vercel et le CDN posent tous les deux la valeur, la section 4 montre comment repérer le doublon.
Nginx :
add_header X-Frame-Options SAMEORIGIN always;always couvre aussi les codes d'erreur (sans always, les reponses 4xx et 5xx partent sans l'en-tete). Surtout, les add_header d'un niveau supérieur ne sont hérités que si le bloc courant n'en définit aucun : en ajouter un dans un location fait disparaître ceux du server, HSTS compris (add_header_inherit merge; depuis 1.29.3).
Apache (mod_headers) :
Header always set X-Frame-Options "SAMEORIGIN"Dans le <VirtualHost> ou un .htaccess avec AllowOverride FileInfo. always couvre aussi les réponses d'erreur.
Cloudflare : Rules > Transform Rules > Modify Response Header, opération Set static, Header name X-Frame-Options, Value SAMEORIGIN. Pas "Add static" : il n'écrase pas un en-tête du même nom et fabrique le doublon de la section 3.
WordPress : WordPress envoie déjà l'en-tête sur sa page de connexion, pas sur le site public. Préférez la règle CDN ou serveur ; en dernier recours, functions.php :
add_action( 'send_headers', function () { header( 'X-Frame-Options: SAMEORIGIN' ); } );Elle échoue si du contenu a déjà été envoyé, et un second plugin peut ajouter sa propre valeur. Une balise <meta http-equiv="X-Frame-Options"> n'a aucun effet (MDN) : si un thème en contient une, elle ne protège rien.
Risque de casse : DENY casse toute iframe légitime (paiement, lecteur vidéo, formulaire externe, page de votre propre site), SAMEORIGIN casse l'encadrement entre sous-domaines. Posez la valeur en recette, ouvrez chaque parcours qui charge un iframe, puis déployez.
6. Le lien avec frame-ancestors
Content-Security-Policy: frame-ancestors 'none';
Content-Security-Policy: frame-ancestors 'self' https://app.exemple.fr;'none' correspond à DENY, 'self' à SAMEORIGIN, et la directive accepte une liste d'origines.
frame-ancestors ne retombe pas sur default-src : une politique qui déclare default-src 'none' autorise toujours l'encadrement. Elle vérifie chaque ancêtre. Et elle n'existe pas dans Internet Explorer 11 (CSP2 non implémenté) : c'est la principale raison de garder X-Frame-Options.
En 2026, c'est frame-ancestors qui l'emporte. La specification CSP3 est explicite : si la politique contient la directive en disposition "enforce", l'en-tête est ignoré. Une CSP en Report-Only n'écrase donc rien. Et si vous relâchez frame-ancestors sur une route d'embed sans toucher au DENY global, les navigateurs modernes obéissent à la CSP, IE11 bloque : les deux doivent dire la même chose.
En pratique : envoyez les deux, avec la même intention ('none' avec DENY, 'self' avec SAMEORIGIN).
7. Ce que ce contrôle ne dit pas
X-Frame-Options ne protège pas du XSS : un script injecté s'exécute avec votre origine, que la page soit encadrable ou non. Deux contrôles distincts.
Un en-tête présent ne prouve pas que la page est saine : il peut valoir ALLOW-FROM, être dupliqué, n'exister que sur l'accueil, ou venir du CDN alors que l'origine ne l'envoie pas.
Un site sans formulaire, sans session et sans action qui change quelque chose n'a presque rien à protéger : l'attaque suppose une action qui vaut la peine d'être déclenchée à la place du visiteur. Un site vitrine en lecture seule, encadrable, n'est pas vulnérable pour autant ; il a une configuration à compléter, et c'est un autre sujet.
Un en-tête absent signifie une chose : l'encadrement est autorisé, sauf si une CSP frame-ancestors est en place. Ce n'est pas une attaque démontrée, et cette page ne démontre aucun exploit.
Une étude d'éditeur, non auditée par un tiers, compte l'en-tête déployé avec une valeur jugée acceptable sur 46,2 % des 3 494 accueils notés sur 5 000 domaines du top 5000 qu'elle a crawlés le 22 juillet 2026 (myssl.info). Son moteur juge la valeur acceptable, pas l'efficacité réelle de la configuration.
Enfin, il ne couvre que l'encadrement : rien sur window.opener, les popups, ni les fuites entre sites que le cadrage permet encore.
8. La seule action qui compte
Une seule action : posez X-Frame-Options: SAMEORIGIN sur la réponse finale de votre domaine, là où se terminent les en-têtes (CDN ou serveur, pas les deux), puis vérifiez avec curl -sIL que la 200 porte une seule valeur. Ensuite seulement, passez en DENY les routes authentifiées, après avoir listé les iframes que vous encadrez.