Table des matières :
- Modèle de menace XSS sur PrestaShop : où ça casse réellement
- Checklist de durcissement côté code (PrestaShop 8.1–9.x / PHP 8.1–8.3)
- Encodage par contexte : le seul remède fiable contre XSS
- CSP sur PrestaShop : stratégie réaliste (pas un poster OWASP)
- Checklist “durcissement PrestaShop” orientée XSS : thème, modules, BO, infra
- Déploiement progressif : tests, métriques CSP et contrôle qualité
Le XSS (Cross‑Site Scripting) reste une des classes de vulnérabilités les plus “rentables” contre un e‑commerce : exfiltration de sessions BO, injection de webskimmers dans le checkout, détournement de formulaires et prise de contrôle via un compte employé compromis. Sur PrestaShop, le risque est structurel : historique Smarty, dette front (scripts inline, modules tiers), et mélange de couches (FO/BO) qui complexifie l’“encodage par contexte”.
Au-delà du vol de panier ou du defacement, un XSS sérieux devient vite un incident “métier” : fraude au paiement (webskimming), compromission d’emails transactionnels, fuite de données clients, et parfois obligation de notification (RGPD) si des données personnelles sont exfiltrées. L’objectif n’est donc pas uniquement “empêcher un alert(1)”, mais réduire l’exploitabilité (encodage/sanitization) et réduire l’impact (CSP, séparation des privilèges, monitoring).
“Cross‑Site Scripting (XSS) flaws occur whenever an application includes untrusted data in a web page without proper validation or escaping.” — OWASP, XSS Prevention Cheat Sheet : XSS Prevention Cheat Sheet (OWASP)
Modèle de menace XSS sur PrestaShop : où ça casse réellement
Le premier piège, c’est de croire que XSS = “un champ de formulaire mal filtré”. Sur PrestaShop, le plus dangereux est souvent le stored XSS : une donnée persistée (BDD, traductions, CMS, attributs produit) réaffichée plus tard dans un autre contexte. Exemple classique : un champ “nom de produit” ou “référence” rendu dans un template FO, puis réutilisé dans le BO (listing, autocomplete, export) où la surface d’attaque inclut des admins, donc un impact maximal.
Un autre scénario très “PrestaShop réel” : un module ajoute un champ de configuration (texte riche) dans le BO (“bandeau promo”, “message de réassurance”, “snippet tracking”), puis réinjecte ce champ dans le FO avec |raw pour préserver la mise en forme. Si ce champ est modifiable par un rôle trop large (ou par un compte employé compromis), vous obtenez un pivot BO → FO.
Deuxième point : l’écosystème module/thème. Un module peut injecter du HTML/JS via un hook (displayHeader, displayFooter, actionFrontControllerSetMedia, etc.). Si le module concatène des chaînes pour générer un <script> ou un attribut HTML, le XSS peut devenir DOM‑based (injection dans innerHTML, document.write, templates JS). Les webskimmers modernes exploitent exactement cette zone grise : un “petit” XSS dans le checkout devient un siphon de carte bancaire. Pour le contexte e‑commerce, ça se recoupe directement avec les scénarios de compromission décrits dans Sécurité PrestaShop 2026 : webskimmers et infostealers côté serveur.
Troisième point : le BO n’est pas un sanctuaire. Un back‑office PrestaShop (8.1/8.2/9.x) reste un front web avec beaucoup de JS, des pages “riches” et des composants qui manipulent du HTML (grilles Symfony, champs traduits, éditeurs WYSIWYG). Si un employé colle un contenu externe “propre” en apparence mais piégé (copier‑coller de HTML depuis un email/Google Doc), ou si un rôle a plus de permissions qu’il ne devrait, le XSS devient un accélérateur d’élévation de privilèges (lecture de tokens, déclenchement d’actions CSRF‑like via API internes). Sur ce point, XSS et contrôle d’accès sont corrélés : voir Broken access control : prévenir IDOR et élévation de privilèges.
Pour rendre ce modèle de menace opérationnel (audit + remédiation), une façon simple est de cartographier les points d’entrée et leur “sink” typique :
| Entrée (souvent “non fiable”) | Où c’est réaffiché | Sink dangereux fréquent | Type de XSS probable |
|---|---|---|---|
| Champs produits (nom, ref, attributs) | FO + BO listings/export | attribut HTML, template JS de recherche | Stored |
| CMS / blocs HTML | FO (home, catégories) | |raw / HTML non sanitizé |
Stored |
| Config module (message, tracking) | displayHeader, displayFooter |
<script> inline, innerHTML |
Stored / DOM |
Paramètres URL (q, order, filtres) |
FO (recherche, tri) | HTML/JS (si concaténation) | Reflected |
| Contenu importé (CSV, ERP) | BO (preview, grid) | cellules HTML rendues | Stored |
L’idée n’est pas de “tout sécuriser pareil”, mais d’identifier les zones où un input apparemment administratif (ex. import) se retrouve rendu dans un contexte plus sensible (JS, attribut, BO).
Checklist de durcissement côté code (PrestaShop 8.1–9.x / PHP 8.1–8.3)
La règle pragmatique : valider à l’entrée, encoder à la sortie. La validation sert à réduire l’espace d’état (types, tailles, formats), l’encodage sert à garantir que la donnée ne “casse” pas le contexte de rendu. Dans PrestaShop, il existe des aides (ex. Validate::*, Tools::getValue(), parfois Tools::safeOutput()), mais aucune de ces fonctions n’est une baguette magique. Tools::safeOutput() est typiquement un encodage HTML générique ; si vous l’utilisez dans un attribut, une URL ou une chaîne JS, vous créez une illusion de sécurité.
Quelques points de vigilance fréquents en code PrestaShop (modules et overrides) :
Validate::isCleanHtml(): utile comme garde‑fou, mais ce n’est pas une “politique HTML” complète (les cas limites HTML/JS sont trop nombreux). À réserver à des champs où vous acceptez un sous‑ensemble très restreint, et à combiner avec une sanitization robuste pour du WYSIWYG.- Champs multilingues : les champs
*_langet traductions peuvent se retrouver affichés dans plusieurs templates. Une donnée “safe” en FO (texte) peut être réutilisée en attribut/JS en BO (autocomplete). C’est typiquement là que l’encodage par contexte devient non négociable. - Helpers/Forms : certains composants affichent des valeurs en réinjectant dans des attributs (
value="..."). Si vous passez des valeurs non encodées au bon filtre, vous ouvrez une injection d’attribut.
Le point qui fait gagner du temps : classifier vos champs en 3 catégories dès la conception du module/thème :
- Texte simple (nom, code, libellé) : interdire
<>et contrôle strict de longueur ; encodage HTML au rendu. - Texte riche (WYSIWYG CMS, description) : sanitization allowlist (pas “strip_tags”), puis encodage adapté selon le contexte de réinjection.
- Données structurées (JSON, arrays) : sérialisation/encodage via les primitives du langage (
json_encode+ options), jamais via concaténation.
Une checklist “input” concrète (facile à relire en revue de code) :
- Longueurs maximales réalistes (ex. nom produit, meta title, champs module).
- Encodage attendu (UTF‑8) et rejet des contrôles invisibles si votre contexte l’impose.
- Validation de format (email, URL) sans réutiliser la valeur “validée” directement en HTML/JS (validation ≠ encodage).
- Pour les champs riches : permettre un ensemble clair de tags/attributs (ex.
p,a[href],strong,ul/li) et interdire explicitementon*,style,srcdoc,javascript:danshref.
Sur la sanitization de texte riche, évitez strip_tags() et les regex “maison”. Utilisez une bibliothèque éprouvée (HTMLPurifier) ou, si votre stack Symfony le permet, le composant HTML Sanitizer (symfony/html-sanitizer) avec une politique allowlist explicite. En 2026, c’est un choix rationnel : vous préférez un parseur HTML robuste plutôt que de courir après les contournements (<svg onload>, math, srcdoc, encodages mixtes). Le coût de maintenance est inférieur à une compromission. Et si vous devez répondre à incident, gardez un playbook : Sécurité PrestaShop : plan de réponse à incident et containment immédiat.
Encodage par contexte : le seul remède fiable contre XSS
“Échapper” n’est pas un geste unique, c’est un choix dépendant du contexte : HTML texte, attribut HTML, JavaScript, URL, CSS, JSON, etc. L’OWASP le rappelle explicitement : l’encodage doit correspondre à la “sink” (le point d’injection). Dès que vous mélangez, vous créez des payloads qui passent (par exemple via des guillemets, backticks, ou encodage entités).
Un aide‑mémoire utile (et souvent absent des checklists) : le contexte réel est celui du parseur, pas celui que vous “croyez” écrire. Exemple : une valeur injectée dans un attribut data-* est du HTML attribut au moment du rendu, mais peut devenir du JS au moment où votre code fait element.dataset.x puis construit une chaîne ou du DOM.
| Contexte de sortie | Risque typique | Défense principale |
|---|---|---|
Texte HTML (<div>…</div>) |
fermeture de tag + injection | encodage HTML texte |
Attribut HTML (title="…") |
rupture de guillemets + nouvel attribut (onmouseover=) |
encodage html_attr |
URL (href, src) |
javascript: / injection querystring |
génération d’URL + encodage URL + allowlist schémas |
JS dans <script> |
fermeture de chaîne + exécution | JSON sérialisé, pas de concat |
DOM (innerHTML) |
exécution via HTML interprété | textContent / sanitization |
Contexte HTML / attribut dans Smarty (FO historique)
Sur PrestaShop 8.1/8.2 (et encore dans beaucoup de thèmes), Smarty reste dominant côté FO. Exemple : afficher un nom de produit en texte HTML.
<h3>{$product.name|escape:'htmlall':'UTF-8'}</h3>
En attribut, il faut une stratégie qui évite de casser les guillemets. Exemple pour un title :
<a title="{$product.name|escape:'html_attr':'UTF-8'}">...</a>
Attention : beaucoup de code legacy utilise |escape:'htmlall' partout. C’est mieux que rien, mais pas une preuve de sûreté contextuelle (et certains filtres htmlall varient selon versions Smarty). Votre checklist doit inclure une recherche de patterns “dangereux” : insertion brute {$var} dans <script>, data-*, href, handlers on*, ou concaténations.
{literal}+ concaténation : pousse souvent à injecter des variables non encodées dans du JS.nofilter/ affichage non échappé : acceptable uniquement si la donnée a été sanitizée en amont avec une politique documentée (et testée).
Enfin, sur les liens (href), le problème n’est pas uniquement l’encodage : c’est aussi la validation du schéma. Si une variable peut influencer un href, imposez une allowlist (ex. https://, mailto:) et refusez javascript: et data: sauf cas très cadré.
Contexte Twig (BO moderne, PrestaShop 8+ ; adoption progressive en 9)
Dans Twig, l’auto‑escape HTML est activé par défaut dans la plupart des templates, mais dès que vous utilisez |raw, vous redevenez responsable. Exemple (attribut) :
<a title="{{ product.name|e('html_attr') }}">...</a>
Pour une URL (paramètre) : ne concaténez pas, encodez la valeur. Exemple simple et correct :
<a href="/search?q={{ q|url_encode }}">Rechercher</a>
Le piège récurrent en BO : rendre des labels venant de traductions ou de champs “config module” et les passer en |raw “parce que ça casse la mise en page”. C’est une dette de sécurité. Si vous voulez du texte riche, sanitisez en amont (allowlist), puis rendez sans raw dès que possible.
Un bon compromis “pratique” en BO : pour des contenus riches administrables (ex. petite zone CMS), stocker la version sanitizée (ou au minimum la re‑sanitizer à chaque save), tracer la politique (tags/attributs autorisés), et couvrir les cas d’usage au test fonctionnel (édition → affichage FO/BO).
Contexte JavaScript / JSON : bannir la concaténation
Là où PrestaShop et ses modules craquent le plus : l’injection dans <script> via concaténation.
Mauvais :
<script>
var customerName = '{$customer.firstname}';
</script>
Correct : sérialiser en JSON côté PHP ou template (selon vos outils) ; en PHP :
$payload = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
Puis dans le template :
<script>
const payload = {{ payload|raw }};
</script>
Le |raw est acceptable ici uniquement parce que vous injectez un JSON généré par json_encode avec options défensives (sinon vous réintroduisez < >).
Deux améliorations qui aident aussi CSP (réduction d’inline) :
- Mettre les données dans un bloc non exécutable, puis parser côté JS :
<script type="application/json" id="page-data">…</script>- Ou utiliser
data-*si et seulement si vous ne reconstruisez pas de HTML derrière (sinon vous déplacez le problème).
Pour du DOM‑injection, n’utilisez pas innerHTML si textContent fait le job ; si vous devez injecter du HTML, passez par une sanitization robuste. Sur un checkout, c’est particulièrement important : les webskimmers se greffent souvent sur des insertions DOM “innocentes” (message d’erreur, résumé panier, bloc promo) où un innerHTML = devient un point d’exécution.
CSP sur PrestaShop : stratégie réaliste (pas un poster OWASP)
La CSP (Content Security Policy) n’est pas un correctif à une base XSS fragile, mais c’est un filet de sécurité qui casse une partie des payloads et surtout vous donne de la visibilité via le reporting. MDN est clair sur la finalité :
“CSP is an added layer of security that helps to detect and mitigate certain types of attacks, including XSS and data injection attacks.” — MDN Web Docs, Content Security Policy (CSP) : Content Security Policy (MDN)
Sur PrestaShop, une CSP “pure” (script-src 'self' sans inline) est rarement déployable immédiatement : le FO et beaucoup de modules injectent des scripts inline, et certains PSP (paiement) ou scripts tiers (tags marketing) chargent des domaines externes. La bonne approche est itérative :
Content-Security-Policy-Report-Onlypendant 1 à 2 sprints.- Réduction des violations (suppression inline, migration vers fichiers, whitelists minimales).
- Passage en enforcement (
Content-Security-Policy) sur un périmètre limité (FO hors checkout), puis extension.
Au passage, CSP sert aussi à verrouiller des axes souvent oubliés en e‑commerce :
frame-ancestors: limite le clickjacking (utile si BO exposé).base-uri: empêche la réécriture de base URL via<base>.object-src 'none': bloque Flash/objets legacy (souvent inutile mais sain).form-action: utile pour empêcher qu’un XSS redirige des formulaires vers un domaine attaquant (à tester selon modules).
Pour implémenter, privilégiez la couche reverse proxy / serveur web (Nginx/Apache/HAProxy) quand la politique est statique. Pour les nonces/hashes, il faut un traitement applicatif (générer un nonce par requête et l’injecter dans les <script>), ce qui demande de toucher au thème et parfois aux modules : à planifier, pas à bricoler.
Exemple Nginx “départ” orienté reporting (à adapter, notamment img-src/connect-src) :
add_header Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:; report-to csp-endpoint" always;
add_header Report-To '{"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://votre-domaine.example/csp-report"}]}' always;
Oui, unsafe-inline au début : c’est volontaire. L’objectif est de mesurer d’abord. Ensuite, vous remplacez progressivement par des nonce-... ou des hashes (sha256-...) et vous sortez les scripts inline vers des assets versionnés. Pour aller plus loin sur les en-têtes, référez-vous à HTTP Security Headers en PHP : guide CSP, HSTS, X-Frame-Options (utile même si vous configurez via Nginx : la logique des directives reste la même).
Côté paiement, la CSP s’inscrit aussi dans une logique de conformité et de réduction du risque “script supply chain” sur les pages sensibles. Sans en faire un sujet “compliance”, gardez en tête que les standards cartes poussent clairement à mieux contrôler les scripts côté navigateur (référentiel PCI DSS v4.0 : PCI DSS v4.0 (PCI Security Standards)).
Checklist “durcissement PrestaShop” orientée XSS : thème, modules, BO, infra
Côté thème/modules (PrestaShop 8.1–9.x), la checklist utile n’est pas “audit global”, mais une série de contrôles mécaniques :
- Interdire les insertions directes de variables dans
<script>et attributs sensibles (on*,style,href,srcdoc). - Interdire
|rawpar défaut (Twig) ; l’autoriser seulement après sanitization (HTML) ou sérialisation (JSON). - Remplacer les concaténations JS par du JSON sérialisé (
json_encodecôté PHP). - Remplacer
innerHTMLpartextContentquand c’est possible. - Vérifier les hooks qui injectent du JS (ex.
displayHeader) : assets externes justifiés, versionnés, et si possible avec SRI (integrity+crossorigin).
Ajoutez un contrôle “pratico-pratique” très rentable : sur chaque module ajouté (ou mis à jour), lister :
- les hooks utilisés (surtout
displayHeader,displayFooter, checkout), - les domaines tiers contactés (pixels, CDN, PSP),
- et les templates qui utilisent
raw/nofilterou qui écrivent dans le DOM.
Côté BO, le durcissement utile est organisationnel et technique : limiter la surface d’attaque (droits employés, IP allowlist si possible), et réduire les données “non fiables” affichées à des admins. Un exemple concret : si vous exposez des champs “commentaires” ou “notes internes” venant du FO (support, retours, formulaires), ils doivent être traités comme input hostile. Un stored XSS dans une note consultée par le support est un scénario banal.
- Sessions : limiter la durée, surveiller les connexions anormales (heures, IP, user‑agent).
- Principe du moindre privilège : éviter les rôles “fourre‑tout” qui peuvent éditer CMS + config module + traductions (c’est exactement ce qu’un attaquant veut après un compte compromis).
Côté infra, ne confondez pas CSP et WAF. Le WAF peut stopper des payloads triviales mais ne remplace pas l’encodage. En revanche, un WAF bien réglé peut réduire le bruit et protéger le checkout le temps que vous corrigez la base de code. Pour un déploiement qui ne casse pas la conversion (faux positifs), voir WAF PrestaShop : réduire les faux positifs et sécuriser le checkout. Et côté hygiène globale (mises à jour, .htaccess, TLS), gardez aussi un socle : Sécurité PrestaShop : mises à jour, SSL et durcissement .htaccess.
Déploiement progressif : tests, métriques CSP et contrôle qualité
Pour éviter le classique “on active CSP et le paiement explose”, vous devez traiter ça comme une migration : environnements, tests, rollback. Sur une boutique avec checkout complexe (ex. one‑page checkout), commencez par isoler les routes critiques (panier, tunnel, callbacks PSP) et appliquez une CSP plus permissive au début sur ces pages. Si vous utilisez un checkout spécifique, prenez en compte ses contraintes d’assets et de scripts (voir PrestaShop ps_onepagecheckout : prérequis, installation et workflow de build).
Le reporting CSP devient un outil de pilotage, pas un gadget. Un cas réel typique en e‑commerce : après passage en Report-Only, vous observez par exemple un volume très élevé de violations au début (scripts inline, domaines tiers, eval() dans des bundles). Votre objectif est de faire baisser ce nombre de façon mesurable sprint après sprint, en taguant les violations par “source” (thème, module X, tag manager, PSP). À partir du moment où le volume est stable et expliqué, vous basculez une partie du site en enforcement et vous surveillez les erreurs JS/checkout.
Pour que ça marche sans “aveuglement”, définissez 3 métriques simples (et actionnables) :
- Taux de pages avec violations CSP (par type : script/style/connect).
- Top 10 des sources (fichiers / endpoints / modules) responsables des violations.
- Impact fonctionnel : erreurs JS (via votre monitoring), et indicateurs tunnel (ajout panier, début checkout, paiement).
Enfin, vous ne sécurisez pas durablement sans contrôle qualité sur les livraisons. Ajoutez au pipeline CI des garde‑fous : scan des templates pour |raw, détection d’usages innerHTML, règles custom (Semgrep), et revue de code ciblée sur les sinks XSS.
Un contrôle CI minimaliste mais efficace (sans “outillage lourd”) consiste déjà à bloquer en revue :
- toute nouvelle concaténation dans
<script>qui inclut une variable serveur, - tout nouveau
|rawen Twig sans justification (“HTML sanitizé” ou “JSON sérialisé”), - toute nouvelle insertion de variable dans un attribut
href/src/style/on*.
Sur la logique de supply chain et de validation automatique des modules, vous pouvez vous appuyer sur CI PrestaShop : provenance, SBOM et validation automatique des modules. Le résultat attendu est simple : moins de régressions XSS, et une capacité à activer des protections (CSP plus stricte, SRI là où c’est pertinent) sans transformer chaque release en incident.
