Table des matières :
- Clickjacking : ce que
frame-ancestorsbloque réellement (et ce qu’il ne bloque pas) X-Frame-Optionsvs CSPframe-ancestors: compatibilité navigateur et stratégie pragmatique- Écrire une policy
frame-ancestorspropre : patterns, anti-patterns et multi‑domaines - Implémentation serveur (recommandée) : Apache, Nginx, LiteSpeed, reverse proxy
- Implémentation applicative : quand vous devez varier la policy (BO Symfony, routes spécifiques)
- Vérification, reporting CSP et roll-back : passer de “ça marche chez moi” à “c’est prouvé en prod”
Clickjacking : ce que frame-ancestors bloque réellement (et ce qu’il ne bloque pas)
Le clickjacking (UI redressing) consiste à charger votre page dans un iframe sur un site tiers, puis à superposer des éléments visuels pour pousser l’utilisateur à cliquer “au mauvais endroit”. Dans un contexte e‑commerce, la cible n’est pas uniquement le front (ex. clic sur un bouton “Commander”), mais surtout les actions sensibles côté back‑office : activation d’un module, changement d’email admin, création d’un webservice, ou validation d’un virement/chargeback selon vos workflows. Sur PrestaShop, la surface d’attaque la plus réaliste reste l’admin : un utilisateur déjà authentifié peut être piégé par un site tiers, et ses cookies de session seront utilisés tant que le navigateur envoie ces cookies au chargement du iframe.
Un scénario typique (et très réaliste en entreprise) :
- un employé (ou prestataire) est connecté au BO PrestaShop dans un onglet ;
- il ouvre un autre site (mail, documentation, outil de ticketing, site partenaire) ;
- ce site intègre en arrière-plan une page BO dans un
iframe(ou tente de le faire) ; - un bouton visible (“Télécharger un PDF”, “Valider”) est en fait positionné au-dessus d’un bouton BO (“Activer”, “Sauvegarder”, “Créer une clé API”), avec opacité et z-index.
Même si votre back‑office dispose de tokens/jetons anti-CSRF, le clickjacking ne “casse” pas la crypto : il cherche à faire exécuter une action légitime par un utilisateur légitime, en détournant son intention. Certaines actions restent protégées par un token “one shot” ou une confirmation, mais ce n’est pas systématique (et l’expérience montre que des modules ajoutent des écrans d’action avec une protection incomplète).
Ne comptez pas sur des “protections implicites” comme SameSite=Lax ou les blocages de cookies tiers : elles varient par navigateur et par configuration, et elles ne vous protègent pas si vous avez explicitement des cookies SameSite=None; Secure (cas fréquent quand vous intégrez des services cross-site) ou si l’attaque passe par une séquence où l’utilisateur clique d’abord sur un lien top‑level. De plus, dans beaucoup d’architectures, l’admin est utilisé en conditions réelles sur des environnements hétérogènes (postes gérés, webviews, navigateurs non mis à jour, etc.). Le cœur de PrestaShop ne fournit pas un mécanisme central et stable pour imposer des en‑têtes HTTP de sécurité sur toutes les réponses (front legacy + pages Symfony) : si vous laissez le sujet “au hasard des modules”, vous aurez des trous.
La directive CSP frame-ancestors est précisément faite pour empêcher ce chargement dans un conteneur tiers. MDN résume correctement le périmètre :
« The HTTP Content-Security-Policy frame-ancestors directive specifies valid parents that may embed a page using
<frame>,<iframe>,<object>,<embed>, or<applet>. » — MDN Web Docs, CSP: frame-ancestors (MDN — CSP: frame-ancestors)
Deux clarifications utiles (parce que c’est une confusion fréquente) :
frame-ancestorscontrôle qui peut intégrer vos pages (vos pages “dans un iframe”). Il ne contrôle pas les iframes que vous mettez dans votre site (pour ça, on parle plutôt deframe-src/child-src).frame-ancestorsest évalué par le navigateur au moment du chargement de la réponse : si un tiers essaie de vous iframer, le navigateur bloque l’affichage (et selon les navigateurs, affiche une erreur en console).
Point important : frame-ancestors ne “sécurise” pas votre site au sens large. Ça ne bloque ni le scraping, ni l’accès direct par URL, ni une exploitation XSS. Pour une approche plus globale sur PrestaShop auto‑hébergé (droits fichiers, mises à jour, exposition admin, TLS), la base est ailleurs (cf. Expertise PrestaShop — PrestaShop auto‑hébergé (prérequis)). frame-ancestors répond à un problème concret : “qui a le droit de me mettre dans un iframe ?”
X-Frame-Options vs CSP frame-ancestors : compatibilité navigateur et stratégie pragmatique
Historiquement, la défense anti-clickjacking s’appuyait sur X-Frame-Options (XFO). MDN donne la définition sans ambiguïté :
« The X-Frame-Options HTTP response header can be used to indicate whether a browser should be allowed to render a page in a
<frame>,<iframe>,<embed>or<object>. » — MDN Web Docs, X-Frame-Options (MDN — X-Frame-Options)
Le problème : XFO est limité à DENY ou SAMEORIGIN. La variante ALLOW-FROM existe mais est largement obsolète et mal supportée. Dès que vous avez un cas métier qui impose une liste d’origines autorisées (ex. portail B2B, intranet, domaine “checkout” séparé), XFO ne suffit plus. frame-ancestors, lui, gère explicitement une allowlist multi-origines.
Comparaison rapide (utile pour décider “quoi poser où”) :
| Besoin | X-Frame-Options |
CSP frame-ancestors |
|---|---|---|
| Bloquer tout framing | ✅ DENY |
✅ 'none' |
| Autoriser uniquement même origine | ✅ SAMEORIGIN |
✅ 'self' |
| Autoriser plusieurs origines (allowlist) | ❌ (quasi impossible de façon portable) | ✅ https://a.example https://b.example |
| Granularité / cohérence avec autres directives CSP | ❌ | ✅ |
| Standardisation / spécification dédiée | RFC (XFO) | W3C CSP |
Pour la référence normative côté XFO : RFC 7034.
Dans les navigateurs modernes, frame-ancestors est la référence. Dans la pratique, vous déployez les deux : CSP pour la granularité, XFO en fallback pour des clients legacy et des environnements embarqués (webviews, vieux navigateurs industriels). Exemple de combinaison saine :
Content-Security-Policy: frame-ancestors 'self';
X-Frame-Options: SAMEORIGIN
Autre point pragmatique : si vous avez déjà une CSP “large” (avec default-src, script-src, etc.), vous n’êtes pas obligé de tout refaire. Vous pouvez simplement ajouter frame-ancestors à votre policy existante. Évitez en revanche de servir plusieurs en-têtes CSP divergents via des couches différentes (appli + proxy + CDN) : le navigateur applique toutes les policies reçues, ce qui revient souvent à durcir “par intersection”… et peut créer des effets de bord difficiles à diagnostiquer.
Dernier point opérationnel : frame-ancestors n’est pas fiable via une balise <meta http-equiv="Content-Security-Policy"> (la directive frame-ancestors n’est pas prise en charge depuis les meta), donc pour garantir le contrôle, posez-la en en‑tête HTTP, donc au niveau serveur, reverse proxy, ou à défaut au niveau applicatif (Symfony) quand vous maîtrisez toute la chaîne.
Écrire une policy frame-ancestors propre : patterns, anti-patterns et multi‑domaines
Une policy minimale, cohérente avec la majorité des boutiques PrestaShop, c’est :
Content-Security-Policy: frame-ancestors 'self';
'self' signifie “même origine” (schéma + host + port). Sur une architecture multiboutique PrestaShop avec domaines distincts, ça reste correct par domaine : chaque host répond avec sa propre policy, et le self est évalué par rapport à l’URL courante. En revanche, si vous voulez autoriser un parent différent (ex. https://portal.example.com qui embarque https://shop.example.com), il faut l’ajouter explicitement.
Quelques patterns concrets (et sûrs) :
- Back-office : blocage total
Idéal dès que possible :
frame-ancestors 'none' - Front-office : même origine + 1 portail
frame-ancestors 'self' https://portal.example.com - Environnements distincts (prod + préprod)
frame-ancestors 'self' https://portal.example.com https://portal-preprod.example.com - Sous-domaines contrôlés (à utiliser avec parcimonie)
frame-ancestors 'self' https://*.example.com
Syntaxe à connaître : frame-ancestors accepte des source expressions (origines), pas des chemins. https://partner.example.com/iframe/ est invalide : vous devez autoriser https://partner.example.com (éventuellement avec wildcard de sous-domaines https://*.partner.example.com). Autre subtilité : http:// et https:// sont deux origines différentes. Si vous avez encore des usages internes en HTTP (VPN, LAN, recette), listez-les explicitement — puis planifiez leur extinction.
Anti‑pattern courant : autoriser trop large (* ou https:) “pour éviter les problèmes”. C’est exactement l’erreur qui annule la protection.
Pour des cas où l’intégration “en iframe” est une exigence (typiquement un extranet, une webapp interne, ou un kiosque), documentez votre contrat : quelles origines, quels environnements (prod, préprod), quels ports, et quels sous‑domaines. Un exemple raisonnable :
Content-Security-Policy: frame-ancestors 'self' https://portal.example.com https://intranet.example.net;
Conseil pratique : avant de figer l’allowlist, faites l’inventaire de ce qui doit réellement être embeddé. Dans beaucoup de SI, des demandes “on veut un iframe” cachent en réalité un besoin de SSO, de deep links ou de composant réutilisable — et frame-ancestors vous oblige à rendre ce besoin explicite (ce qui est plutôt sain côté gouvernance).
Si votre besoin réel est d’exposer du catalogue à des tiers (comparateurs, marketplaces, apps), l’iframe n’est pas une API. Il vaut mieux sortir du HTML et passer par des flux, webhooks, ou API (voir Expertise PrestaShop — Integration API et, côté PrestaShop, Expertise PrestaShop — API Webservice PrestaShop).
Implémentation serveur (recommandée) : Apache, Nginx, LiteSpeed, reverse proxy
Pré-requis : un environnement de staging, accès aux vhosts (ou .htaccess), et la capacité de vérifier les en‑têtes en prod (via curl/logs). Risque principal : vous pouvez casser des intégrations existantes qui “embed” votre boutique (souvent sans que ce soit documenté). Déployez d’abord en Report-Only si vous suspectez des usages non maîtrisés (voir section tests).
Avant de déployer, une checklist simple évite 80% des surprises :
- l’URL du BO est-elle stable (ou randomisée) ? (
/adminXXXX/) ; - avez-vous un CDN / reverse proxy qui injecte déjà des en-têtes ?
- avez-vous des pages “intermédiaires” (SSO, WAF, page de maintenance) servies par une autre couche ?
- quelles URLs doivent rester embeddables (si c’est réellement un besoin) ?
Apache (mod_headers) — souvent le cas en hébergement mutualisé
Dans un vhost Apache (recommandé), ou à défaut dans .htaccess si vous n’avez pas la main, vous pouvez faire :
<IfModule mod_headers.c>
Header always set Content-Security-Policy "frame-ancestors 'self'"
Header always set X-Frame-Options "SAMEORIGIN"
</IfModule>
Notes pratiques :
alwaysgarantit l’en‑tête même sur des réponses d’erreur (4xx/5xx), utile pour éviter des comportements bizarres sur pages de login/admin.- Évitez de dupliquer l’en‑tête via plusieurs couches (Apache + CDN + application) : certaines stacks concatènent mal et vous obtenez une policy invalide.
- En multi‑store avec domaines multiples, la règle reste valable telle quelle ; la variabilité est naturellement gérée par l’origine.
Si vous avez accès au vhost (plutôt qu’à .htaccess), vous pouvez aussi appliquer une règle plus stricte sur l’admin via un bloc dédié (l’idée est la même qu’avec Nginx) : BO en DENY / 'none', front en 'self'. Le gain est important : vous réduisez drastiquement le risque “admin déjà loggé” sans impacter le catalogue public.
Nginx — cas fréquent avec PHP‑FPM / stacks optimisées
Exemple minimal (à placer au bon niveau : server ou location), en tenant compte de la subtilité Nginx : add_header n’est appliqué que sur certaines réponses, sauf si vous ajoutez always.
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
Si vous voulez différencier back‑office et front (par exemple pour autoriser un portail interne à embarquer uniquement un sous-ensemble), vous pouvez cibler le répertoire admin (ex. /admin123/)
location ^~ /admin123/ {
add_header Content-Security-Policy "frame-ancestors 'none'" always;
add_header X-Frame-Options "DENY" always;
# ... config PHP habituelle
}
Là on assume un choix strict : le BO ne doit jamais être iframé. En pratique, ça vous évite des scénarios de clickjacking “admin déjà loggé”.
Deux détails qui font gagner du temps en prod :
- vérifiez que vos règles s’appliquent aussi aux redirections (
301/302) autour du BO (certaines stacks redirigent/admin123→/admin123/) ; - si vous avez un
locationtrès générique qui réécrit/écrase des en-têtes, assurez-vous de ne pas annuler le bloc admin plus spécifique.
Si vous êtes en phase de modernisation/déploiements automatisés, c’est typiquement un en‑tête à gérer comme du code infra (cf. Expertise PrestaShop — Modernisation et reprise).
LiteSpeed / OpenLiteSpeed / reverse proxy et CDN
LiteSpeed supporte aussi l’injection d’en‑têtes au niveau vhost. Le point d’attention, c’est la multiplication des couches : LiteSpeed + plugin de cache + CDN (Cloudflare, Fastly, etc.). Une seule couche doit être source de vérité, sinon vous allez passer du temps à comprendre “qui écrase quoi”.
Deux points d’implémentation concrets sur des architectures cache/CDN :
- assurez-vous que les réponses “hit cache” incluent aussi les en-têtes de sécurité (certains caches stockent sans certains en-têtes si mal configurés) ;
- si vous variez les policies entre front et BO, vérifiez que le CDN ne met pas en cache des réponses admin (idéalement, il ne devrait pas).
Sur des architectures avec Varnish/CDN, la stratégie est la même que pour le cache : centraliser et tracer (voir Expertise PrestaShop — Cache PrestaShop).
Implémentation applicative : quand vous devez varier la policy (BO Symfony, routes spécifiques)
Quand le serveur web ne peut pas exprimer facilement des règles par route (ou quand vous êtes dans un PaaS verrouillé), vous pouvez poser la policy au niveau applicatif. En PrestaShop 8.x/9.x, c’est surtout pertinent sur les pages Symfony (une large partie du back‑office, et potentiellement certains écrans modernes). Sur ces routes, un EventSubscriber Symfony sur KernelEvents::RESPONSE est propre, testable, et versionnable.
Exemple (PrestaShop 9.1/9.2, PHP 8.2+ ; code Symfony standard) :
<?php
// src/EventSubscriber/SecurityHeadersSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
final class SecurityHeadersSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [KernelEvents::RESPONSE => 'onResponse'];
}
public function onResponse(ResponseEvent $event): void
{
$response = $event->getResponse();
// Policy stricte : pas d’iframe pour le BO.
$response->headers->set('Content-Security-Policy', "frame-ancestors 'none'");
$response->headers->set('X-Frame-Options', 'DENY');
}
}
Deux améliorations simples, souvent nécessaires en projet réel :
- ne pas écraser une CSP existante (si vous en avez déjà une pour
script-src, etc.) : dans ce cas, l’objectif est d’ajouterframe-ancestorsproprement ; - éviter les effets de bord sur des sous-requêtes (selon votre stack) : appliquer l’en-tête uniquement à la requête principale peut être préférable.
Limite à connaître (et c’est là que le cœur PrestaShop vous met dans le dur) : le front-office historique n’est pas intégralement dans Symfony. Vous pouvez techniquement envoyer des headers en PHP (avant output) dans certains hooks, mais la couverture n’est pas garantie et vous jouez avec l’ordre d’exécution des modules (donc non déterministe). Si votre objectif est “pas de clickjacking partout”, ne tentez pas de le résoudre uniquement en module : encadrez, testez, livrez (cf. Expertise PrestaShop — Module PrestaShop).
Dernier détail : si vous avez un besoin transactionnel d’embed ponctuel (ex. une page d’auth SSO interne, ou un tunnel spécifique), faites-le à l’envers : policy stricte globale côté serveur, puis exception ciblée par route uniquement si vous maîtrisez la réponse (Nginx location, Apache LocationMatch, ou code Symfony). Garder une exception “large” en permanence est généralement un endettement sécurité qui finit exploité.
Vérification, reporting CSP et roll-back : passer de “ça marche chez moi” à “c’est prouvé en prod”
Valider frame-ancestors ne demande pas d’outillage exotique. Commencez par la preuve brute :
curl -I https://votre-boutique.example/
curl -I https://votre-boutique.example/admin123/
Vous devez voir Content-Security-Policy (et idéalement XFO) sur toutes les réponses attendues. Ensuite, test fonctionnel : tentez un embed dans une page HTML locale (ou un petit gist interne) et vérifiez que le navigateur refuse le framing (console DevTools). Pour industrialiser : Mozilla Observatory et securityheaders.com donnent une lecture rapide, mais ne remplacent pas vos tests e2e.
Un test “terrain” utile (notamment pour le BO) : demandez à un profil non-tech (chef de produit, ADV…) de suivre un lien de test vers une page interne qui tente d’embarquer l’admin dans un iframe. S’il voit l’admin s’afficher, vous avez un trou. S’il voit une page vide / bloquée, vous êtes sur la bonne voie. L’objectif est de valider le comportement côté navigateur final, pas seulement côté serveur.
Si vous avez un doute sur des usages “cachés” (affiliés, intranet, intégrations legacy), déployez temporairement en Report-Only :
Content-Security-Policy-Report-Only: frame-ancestors 'self';
Puis collectez les rapports. La recommandation actuelle est d’utiliser l’API Reporting (en-tête Reporting-Endpoints et directive CSP report-to) plutôt que report-uri (déprécié). Ça implique d’avoir un endpoint serveur qui ingère du JSON et de le corréler à vos logs. Ce n’est pas du confort : c’est la seule manière d’objectiver “qui tente de m’embed” et “qui va casser au go-live”.
Côté exploitation, prévoyez un plan de roll-back “simple et rapide” :
- centralisez la configuration (un seul endroit “source de vérité”) ;
- gardez un commit / une sauvegarde de vhost ;
- documentez quel en-tête est posé où (proxy, webserver, appli) pour éviter les retours arrière incomplets.
Les mêmes réflexes d’observabilité s’appliquent que pour les incidents HTTP (cf. Expertise PrestaShop — Erreur HTTP 503 & diagnostics).
Côté SEO : bloquer l’embed ne change rien à l’indexation classique (Googlebot charge vos pages en top-level, pas en iframe chez un tiers). Là où ça peut impacter votre acquisition, c’est si vous comptiez sur des intégrateurs qui affichent votre boutique dans une page externe via iframe. Techniquement, c’est fragile (cookies tiers, performances, tracking) et de plus en plus cassé par les navigateurs. Si vous voulez exposer du contenu à des tiers, passez par des intégrations API/flux (plus auditables) ou des deep links.
Pour les références normatives, gardez sous la main : la spec CSP (W3C, W3C CSP3) et la doc OWASP sur le clickjacking (OWASP — Clickjacking Defense Cheat Sheet). Et si vous êtes déjà en chantier “policies navigateur” (Permissions-Policy, etc.), évitez de gérer chaque header au cas par cas : centralisez votre gouvernance d’en‑têtes (cf. Expertise PrestaShop — Permissions-Policy guide).




