Table des matières :
- Fixer une baseline PHP et verrouiller l’environnement d’exécution
- Typage strict : réduire l’ambiguïté avant de « sécuriser »
- Entrées/sorties : la base de la sécurité applicative (et les erreurs PrestaShop classiques)
- CSRF, authentification, autorisations : ne pas confondre « être connecté » et « avoir le droit »
- Opérations sensibles : uploads, SSRF, désérialisation, secrets et erreurs
- Qualité de code : PSR, analyse statique, tests et refactoring mesurable
- Sécurité de la supply chain PHP : Composer, SBOM, audit et CI reproductible
- Checklist opérationnelle (module/stack PrestaShop) : ce qui doit passer en revue avant merge
Fixer une baseline PHP et verrouiller l’environnement d’exécution
En 2026, la « bonne pratique » n’est pas de viser la dernière version de PHP, mais de figer une baseline cohérente avec votre stack (PrestaShop 8.x / PrestaShop 9.x, extensions obligatoires, contraintes d’hébergement) et de la rendre vérifiable par le code. Concrètement : même version PHP en local/CI/staging/prod, mêmes extensions, même php.ini (au moins sur les options de sécurité/erreurs), et un composer.lock versionné. Si vous êtes en phase de montée de version, partez d’abord des exigences côté PrestaShop et infra (voir Exigences système : compatibilités PHP, MariaDB et Elasticsearch minimales).
Une façon pragmatique d’éviter les « ça marche sur ma machine » est de formaliser une matrice d’environnement minimale (même si vous n’êtes pas 100% conteneurisé) :
| Zone | À verrouiller | Exemple de vérification |
|---|---|---|
| Local | PHP + extensions + ini principaux | php -v, php -m, export de phpinfo() en CI |
| CI | Version identique + Composer identique | image Docker PHP taguée + composer --version |
| Staging | Identique à prod (ou très proche) | vérif post-déploiement (endpoint /health) |
| Prod | Drift contrôlé | alerte si version/ext diffèrent (runbook) |
Côté php.ini / FPM, sans entrer dans le « tuning perf », il y a quelques réglages qui ont un vrai impact sécurité/robustesse (à adapter à votre infra) :
display_errors=0en prod (sinon fuite de chemins/stack traces)log_errors=1+ un chemin de log exploitable (sinon vous « volez à l’aveugle »)expose_php=0(réduit l’empreinte d’info, même si ce n’est pas une barrière)- cookies de session durcis :
session.cookie_secure=1(si HTTPS),session.cookie_httponly=1, et unsession.cookie_samesiteadapté (souventLax, parfoisStrictselon les parcours) - limites cohérentes pour uploads :
upload_max_filesize,post_max_size,max_file_uploads(pour éviter les surprises lors d’imports/visuels)
Côté Composer, le point souvent négligé est le champ config.platform.php : sans lui, votre CI peut résoudre des dépendances compatibles avec PHP 8.4, alors que la prod tourne en 8.2 (ou l’inverse). Résultat : erreurs au déploiement, ou pire, contournements de sécurité via une version de lib inattendue. Exemple minimaliste :
{
"config": {
"platform": { "php": "8.2.0" },
"sort-packages": true
},
"require": {
"php": "^8.2",
"ext-json": "*"
}
}
Deux compléments « delivery » très concrets dans un contexte PrestaShop (où l’on voit encore beaucoup de déploiements artisanaux) :
- Ne pas construire en prod : faites un build en CI (dépendances, assets) et déployez un artefact immuable (zip ou image). C’est un prérequis pour diagnostiquer une régression.
- Verrouiller les extensions : un module peut dépendre de
ext-intl,ext-gd,ext-zip, etc. Les déclarer (au moins pour vos modules) limite les surprises lors d’un changement d’hébergement.
Enfin, gardez à l’esprit que PrestaShop reste un hybride : une partie legacy (ObjectModel, helpers, Tools::getValue(), Db::getInstance()) et une partie Symfony de plus en plus structurante, surtout en PrestaShop 9 (contrôleurs/services/Twig). Si vous migrez ou développez des modules compatibles PS9, vous avez intérêt à aligner votre baseline sur cette réalité (référence : PrestaShop 9 : adapter modules et thèmes aux contrôleurs services et Twig).
Typage strict : réduire l’ambiguïté avant de « sécuriser »
Le typage en PHP n’est pas un gadget de style, c’est un mécanisme de réduction d’ambiguïté qui enlève des surfaces d’attaque et des classes entières de bugs (valeurs nulles inattendues, conversion implicite, confusion string/int sur des IDs, etc.). La mesure la plus rentable est d’activer strict_types sur vos fichiers de domaine et de service :
<?php
declare(strict_types=1);
final class StockReservationService
{
public function reserve(int $productId, int $qty): void
{
// ...
}
}
Attention : declare(strict_types=1) ne « typpe » pas tout PHP magiquement. Le manuel PHP précise : « Strict typing applies to function calls made from within the file with strict_types declaration » (PHP Manual, Type Declarations : https://www.php.net/manual/en/language.types.declarations.php). Donc si votre code legacy appelle vos services depuis un fichier non-strict, vous gardez des conversions implicites. La stratégie réaliste sur PrestaShop : zones strictes (services, domaines, adaptateurs API) + anti-corruption layer (mapping) en entrée/sortie des couches legacy.
Dans les modules PrestaShop, le point de friction classique est l’ObjectModel et ses champs dynamiques : vous recevez des tableaux, des string numériques, des valeurs vides '' qui signifient parfois « null ». Évitez de propager ça. Introduisez des DTO typés et des Value Objects (ex. ProductId, Money, Email) et mappez explicitement.
Mini-scénario typique (qui se traduit ensuite en bugs « impossibles » côté stock/prix) : un champ qty arrive depuis un formulaire BO en chaîne "0". Selon les conversions implicites, vous pouvez confondre « 0 valide » et « champ vide », ou déclencher un comportement différent si une comparaison faible (==) traîne dans le code.
Un mapping explicite (même simple) vous force à décider :
<?php
declare(strict_types=1);
final readonly class Quantity
{
public function __construct(public int $value)
{
if ($value < 0) {
throw new InvalidArgumentException('La quantité ne peut pas être négative.');
}
}
}
// anti-corruption layer: depuis Tools::getValue() vers le domaine
$rawQty = Tools::getValue('qty'); // string|array|null en pratique
if (!is_string($rawQty) || $rawQty === '') {
throw new InvalidArgumentException('Quantité manquante.');
}
$qty = new Quantity((int) $rawQty);
PHP 8.1+ vous aide aussi avec :
- les propriétés typées +
readonlypour geler l’état (évite des mutations « fantômes ») - les
enumpour remplacer des strings magiques (statuts, modes, types) - et, en PHP 8.2, l’attribut
#[\SensitiveParameter](utile pour éviter de logguer un secret dans une stack trace lors d’une exception)
Ça ne rend pas votre code « plus beau » : ça rend votre contrat explicite, donc testable, et ça facilite l’analyse statique (qui détecte tôt les chemins morts, les retours incohérents, les null oubliés).
Entrées/sorties : la base de la sécurité applicative (et les erreurs PrestaShop classiques)
La majorité des vulnérabilités exploitables en e‑commerce viennent d’un pipeline entrée → traitement → sortie mal contrôlé : injection SQL, XSS stockée, contournement d’autorisations, CSRF sur actions back-office, etc. Un rappel utile, issu de l’OWASP XSS Prevention Cheat Sheet : « Rule #1 is to escape output » (OWASP : OWASP XSS Prevention Cheat Sheet). En clair : valider en entrée, normaliser en interne, encoder en sortie selon le contexte (HTML, attribut, URL, JS, JSON).
Un bon réflexe « review » est d’identifier :
- Sources :
RequestSymfony,Tools::getValue(), webhooks, imports CSV/XML, back-office config, paramètres GET/POST, cookies. - Sinks (zones dangereuses) : SQL, templates (Smarty/Twig), fichiers (uploads, logs), HTTP sortant (SSRF), shell (rare, mais critique), e-mails (injections d’en-têtes).
- Frontières : dès qu’on passe du legacy à Symfony, d’un module au core, ou d’une entrée utilisateur à une requête SQL/HTML.
Sur PrestaShop legacy, le piège est de « faire confiance » à Tools::getValue() et à des validations partielles. Validate::* aide, mais n’est pas un substitut à une politique d’encodage en sortie. En front, Smarty rend facile le mélange HTML/variables ; en back-office PS9, Twig auto‑escape HTML par défaut, mais vous pouvez casser ça avec |raw (et certains thèmes/modules le font).
Quelques repères simples (à adapter à votre stack) :
- HTML (contenu) : échapper côté template (Twig auto-escape, ou filtre d’escape Smarty si besoin)
- Attribut HTML (ex.
href,title,data-*) : encodage spécifique attribut - URL : encoder les paramètres (et éviter de construire une URL par concat)
- JS inline / JSON : ne pas injecter de string brute dans un script, préférer un
json_encodeet un parse côté JS
Si vous devez sortir du HTML riche stocké (WYSIWYG), vous devez désinfecter à l’enregistrement (allowlist avec HTML Purifier par exemple) et interdire de ré-injecter des fragments non maîtrisés dans des attributs/JS. Autrement dit : le « contenu riche » doit être une exception cadrée, pas un prétexte à propager |raw.
Pour l’injection SQL, la règle est simple : requêtes paramétrées. Si vous êtes côté Symfony/Doctrine DBAL, utilisez des paramètres nommés. Si vous êtes côté Db::getInstance(), au minimum passez par pSQL() pour les strings… en comprenant que ce n’est pas une préparation SQL, juste un échappement. La meilleure option en module PS9 est de pousser le code data-access vers des services Symfony/DBAL (quitte à adapter), plutôt que de multiplier le SQL concaténé dans des contrôleurs.
Petit signal d’alarme en revue : toute occurrence de construction SQL du type "... WHERE id=" . Tools::getValue('id') doit déclencher un stop immédiat (même si « c’est un int »). Le coût de correction est faible, le coût d’une injection en prod est maximal.
CSRF, authentification, autorisations : ne pas confondre « être connecté » et « avoir le droit »
CSRF : dès qu’une action modifie un état (prix, adresse, statut commande, webhooks, config module), vous devez exiger un token non devinable et lié à la session. Côté Symfony, utilisez les CSRF tokens natifs (forms ou CsrfTokenManagerInterface). Côté pages legacy/back-office PrestaShop, il existe des jetons historiques, mais l’implémentation varie selon les zones : vérifiez systématiquement les contrôleurs ciblés et ne réutilisez pas un token « global ». En pratique : si votre module expose une route Symfony en BO, ne contournez pas le mécanisme CSRF « parce que ça marche en AJAX ».
Exemple d’attaque réaliste en agence/TMA : une page BO propose une action sensible (ex. « régénérer une clé », « synchroniser un stock », « activer un transporteur ») déclenchée en GET, sans token. Un attaquant peut alors forcer le navigateur d’un employé connecté à appeler l’URL via une image <img src="..."> ou un lien dans un e‑mail. Même si les cookies SameSite réduisent certains cas, ce n’est pas une protection suffisante : traitez le CSRF comme un contrôle applicatif, pas comme une propriété du navigateur.
Authentification : évitez de réinventer. Pour les API internes/externes (webhooks, intégrations ERP, automatisations), partez sur des clés courtes + rotation + scopes + rate limiting + logs d’audit. L’article API : sécuriser apikey, limiter le débit et renforcer la conformité couvre les patterns d’implémentation : ne stockez pas d’API keys en clair dans la base si vous pouvez stocker un hash (même logique que password_hash) : vous limitez l’impact d’une fuite de base.
Autorisations : « admin = tout » est une hypothèse fausse en agence, en TMA et en multi-boutique. Si vous exposez des endpoints (même internes), posez une matrice de droits : rôle → action → ressource. En Symfony, utilisez des Voters (isGranted) ; en PrestaShop, appuyez-vous sur les mécanismes d’autorisations BO quand ils existent, sinon implémentez une couche explicite et testée.
Deux points souvent oubliés :
- Multi-boutique : un employé peut avoir accès à un module, mais pas à toutes les boutiques. Vos requêtes doivent porter le contexte boutique (et vos contrôles d’accès aussi).
- Actions destructrices : exigez une confirmation (UI) + journalisation (qui, quoi, quand) + idéalement un mécanisme de « dry-run » pour les opérations de masse.
Pour un exemple de pattern Symfony lié à l’impersonation et au contrôle fin : https://www.expertise-prestashop.fr/2026/07/07/symfony-switch_user-limiter-limpersonation-avec-voter-et-isgranted/.
Opérations sensibles : uploads, SSRF, désérialisation, secrets et erreurs
Uploads : c’est un concentré de risques (RCE, XSS via SVG/HTML, exécution de polyglots, dépassement de quota). La check‑list minimale : refuser par défaut, allowlist MIME et extension, re‑détecter le type via finfo, renommer (UUID), stocker hors webroot si possible, servir via un contrôleur qui force Content-Disposition, et générer des miniatures côté serveur avec une lib robuste.
Quelques précisions qui évitent des faux positifs (ou des fausses sécurités) :
- Ne faites pas confiance au
Content-Typeenvoyé par le client : il est trivial à falsifier. - Le « MIME » peut être ambigu (ex. certains SVG) : si vous n’avez pas un besoin fonctionnel clair, interdisez SVG (et plus largement tout format pouvant embarquer du script).
- Pensez quota/DoS : taille max par fichier et par requête et par utilisateur.
Sur PrestaShop, ne supposez jamais que le répertoire /img/ est correctement durci : auditez les .htaccess et testez l’exécution PHP dans les répertoires d’upload. Un simple « mauvais emplacement + mauvaise config serveur » peut transformer un upload en exécution de code.
SSRF : dès que vous laissez un utilisateur fournir une URL (import produit, webhook test, fetch d’images distantes), vous ouvrez une porte vers l’infra (metadata cloud, réseau interne). Mesures concrètes : bloquer les schémas non attendus, résoudre DNS et refuser les IP privées/loopback/link-local, appliquer un timeout bas, interdire les redirections, et journaliser. Si vous êtes derrière un reverse-proxy, ne faites pas confiance à X-Forwarded-For sans configuration stricte.
Désérialisation : si votre code consomme des données externes, évitez unserialize() (sauf cas très maîtrisé). Pour des payloads applicatifs, JSON (avec validation stricte) est généralement un meilleur choix. En e‑commerce, les vecteurs arrivent souvent via des webhooks, des caches applicatifs, des queues, ou des champs « config » de modules.
Secrets et erreurs : stocker des secrets dans Configuration ou dans le code est une dette de sécurité. Externalisez dans des variables d’environnement ou un store (Vault, AWS/GCP secrets, etc.) et limitez l’exposition en logs. Pour la gestion des erreurs, la règle opérationnelle est : ne pas afficher en prod, tout tracer côté serveur (corrélation request-id).
Un ajout simple mais très utile : ajoutez un identifiant de corrélation (ex. X-Request-Id) propagé entre Nginx/Apache → PHP-FPM → logs applicatifs. En incident, vous gagnez un temps énorme pour reconstruire la requête, le user, et l’action.
Sur PrestaShop, outillez-vous : logs PHP-FPM, logs applicatifs, alerting. Référence utile pour bâtir une observabilité « erreurs d’abord » : PrestaShop monitoring : erreurs, logs et alertes.
Qualité de code : PSR, analyse statique, tests et refactoring mesurable
Normaliser le style n’est pas du perfectionnisme, c’est de l’industrialisation : moins de friction en revue, moins de diff inutiles, moins d’erreurs triviales. La base : PSR‑12 + PHP-CS-Fixer (ou ECS) avec une config partagée, et un pipeline CI qui refuse les PR non conformes. Ça s’applique aussi aux modules PrestaShop : un module sans règles de style finit vite en « patchwork » (et devient dangereux à maintenir sous pression).
L’étape qui fait réellement monter le niveau, c’est l’analyse statique (PHPStan ou Psalm). Fixez un niveau cible et montez progressivement. C’est exactement ce que vous voulez sur PrestaShop : repérer les null non gérés, les types trop larges, les retours ambigus, les appels à des méthodes inexistantes à cause de surcharges dynamiques. Exemple (PHPStan) :
parameters:
level: 6
paths:
- src
treatPhpDocTypesAsCertain: false
Un pattern efficace (surtout sur un codebase module + legacy) : stabiliser la surface avant de refactorer.
- Ajouter des types de retour (
: void,: array,: ?string) là où c’est sûr. - Remplacer les
mixed« faciles » par des types plus précis, ou isoler lemixedau bord (entrée/sortie). - Marquer les invariants (exceptions métier) plutôt que de laisser des
false|nullse propager.
Tests : visez d’abord ce qui casse en prod (checkout, synchro stocks/prix, imports, webhooks). Combinez PHPUnit (unit + intégration) et des tests E2E si vous touchez au front. Dans PrestaShop, un test « utile » est souvent un test d’intégration sur services + base, car les bugs viennent des interactions (ObjectModel, hooks, cache).
Pour diagnostiquer et corriger vite, équipez les devs : Xdebug & VS Code : configurer le débogage PHP en local et mesurez l’impact avec des indicateurs simples :
- volume d’erreurs 500 / exceptions par release
- temps moyen de résolution (MTTR) sur incidents checkout / BO
- nombre de hotfixes post-déploiement
L’objectif n’est pas un « 100% coverage » abstrait, mais une baisse mesurable du risque sur les flux business (paiement, commande, stock, livraison).
Sécurité de la supply chain PHP : Composer, SBOM, audit et CI reproductible
Les vulnérabilités modernes arrivent souvent via les dépendances. Composer fournit un point d’entrée simple : composer audit. Sa doc est explicite : « The audit command checks for security vulnerabilities in your dependencies » (Composer documentation : Composer audit). L’erreur classique est de lancer l’audit « une fois de temps en temps » au lieu de le bloquer en CI, avec une politique (fail sur critical/high, exceptions justifiées et temporaires).
Deux règles simples qui évitent beaucoup de dérives :
- Toujours déployer avec
composer.lock: c’est lui qui fige vos versions réelles. - Différencier dev/prod : en prod, installez sans les dépendances de dev (sinon vous augmentez inutilement la surface d’attaque et la taille de l’artefact).
Pour aller plus loin, générez un SBOM (Software Bill of Materials) à chaque build, et gardez-le avec l’artefact déployé. Ça permet de répondre vite à une CVE (êtes-vous impacté, où, quand déployé). Sur PrestaShop, c’est particulièrement pertinent parce que les modules ajoutent leur propre graphe de dépendances. Pour un workflow concret orienté PrestaShop : CI PrestaShop : SBOM et validation automatique des modules.
Reproductibilité : ne déployez pas « un zip construit à la main ». Build en CI, tag, artefact immuable, signature si possible, et déploiement identique entre staging et prod. Si vous gérez de gros pics (soldes, campagnes), vous n’avez pas le luxe de découvrir une divergence au moment où PHP-FPM sature.
Sur la partie infra/app, une défense complémentaire utile est un WAF correctement réglé (sans casser le checkout) : WAF PrestaShop : réduire les faux positifs.
Enfin, côté e‑commerce, n’oubliez pas que la sécurité « applicative » se combine à des exigences de conformité (notamment paiement). Même si vous externalisez le paiement, connaître les attentes du standard PCI DSS aide à cadrer vos choix (journalisation, durcissement, accès) : PCI DSS.
Checklist opérationnelle (module/stack PrestaShop) : ce qui doit passer en revue avant merge
Typage et robustesse : imposez declare(strict_types=1) sur les nouvelles classes de service, des signatures strictes, et des retours explicites (void, never, ?Type). Remplacez les « tableaux fourre-tout » par des DTO typés. Dans les zones legacy, ajoutez des garde-fous : cast explicite, validation stricte, exceptions métier, et mapping d’entrée/sortie. Le but n’est pas d’éradiquer le legacy (irréaliste), mais de l’empêcher de contaminer les couches critiques.
Sécurité applicative : revoyez systématiquement (1) sources d’entrée (Request, Tools::getValue, webhooks), (2) sinks (SQL, templating, filesystem, HTTP sortant), (3) contrôles (CSRF, authz, rate limiting, logs).
Checklist rapide (pratique en PR) :
- [ ] Toute écriture en base passe par paramètres (DBAL) ou échappement conscient + typage strict au bord
- [ ] Aucune sortie en template n’utilise
rawsans justification + désinfection en amont si HTML riche - [ ] Routes BO sensibles : token CSRF + contrôle d’autorisation (rôle + contexte boutique)
- [ ] Webhooks / endpoints machine-to-machine : signature/clé + rotation + limitation de débit + logs d’audit
- [ ] Upload : allowlist +
finfo+ renommage + stockage sûr + pas d’exécution possible dans le répertoire
Ajoutez des headers de sécurité adaptés au contexte (CSP, HSTS, etc.) plutôt que des snippets copiés-collés : guide pratique ici HTTP Security Headers en PHP. Et si vous modifiez la config serveur (php.ini, FPM, vhosts), faites-le via infra as code et prévoyez un rollback : en prod, « tester à la main » est une stratégie de panne.
Qualité et delivery : CI obligatoire (lint + style + analyse statique + tests + audit Composer), artefact immuable, et surveillance post-déploiement (erreurs, latence, timeouts, saturation PHP-FPM). Si vous êtes en trajectoire PrestaShop 9, alignez vos modules sur la structure services/Symfony plutôt que d’empiler des overrides : vous y gagnerez en maintenabilité, et vous réduirez le risque de régression à chaque update core (voir : Module PrestaShop 9 : structure et bonnes pratiques et la checklist de migration : Migration PrestaShop 9 : checklist complète).
