Table des matières :
- Pourquoi la désérialisation PHP est une surface d’attaque (et pourquoi PrestaShop y est exposé)
- Ce qui déclenche réellement une injection d’objets : sources de données, gadgets et “magic methods”
- Éviter
unserialize()en production : formats alternatifs, DTO et validation stricte - Si vous devez désérialiser :
allowed_classes, signature, et périmètre de confiance - Désérialisation “invisible” via PHAR : quand
unserialize()n’est même pas dans le code - Durcissement applicatif autour de PrestaShop : modules, contrôleurs, et frontières de confiance
- Audit : trouver les désérialisations, qualifier le risque, et couvrir par des tests
- Mise en production : patch management, monitoring, et réponse à incident si exploitation suspectée
- Check-list opérationnelle (désérialisation PHP) pour code PrestaShop et modules
Pourquoi la désérialisation PHP est une surface d’attaque (et pourquoi PrestaShop y est exposé)
La désérialisation PHP (via unserialize()) reconstruit en mémoire une structure à partir d’une chaîne produite par serialize(). Le piège, c’est que le format de sérialisation PHP encode non seulement des tableaux/scalaires, mais aussi des objets (type, propriétés, visibilité). À partir du moment où une chaîne sérialisée peut être influencée par un attaquant, on ouvre la porte à la PHP Object Injection (injection d’objets), avec comme effets possibles : contournement d’autorisations, suppression/écriture de fichiers, SSRF, voire exécution de code selon les “gadgets” disponibles.
Pour visualiser le “danger” : une chaîne sérialisée peut explicitement demander l’instanciation d’une classe. Par exemple, les objets commencent souvent par O: (object) :
a:2:{...}→ tableaus:5:"hello"→ chaîneO:8:"stdClass":1:{s:3:"foo";s:3:"bar";}→ objet (classe + propriétés)
Le manuel PHP est explicite sur le risque : « Do not pass untrusted user input to unserialize() » (documentation officielle unserialize())
https://www.php.net/manual/en/function.unserialize.php. Cette phrase est souvent ignorée parce que la désérialisation “marche” en dev et que le bug n’apparaît qu’en condition hostile. En production e-commerce, l’entrée “non fiable” n’est pas uniquement $_GET/$_POST : c’est aussi tout ce qui vient d’un client HTTP (cookie, header), d’un service tiers (webhook), d’un job async, ou d’une DB qui peut être altérée après compromission.
Dans l’écosystème PrestaShop, le problème n’est pas forcément le cœur moderne, mais plutôt le legacy et les modules. Sur PrestaShop 8.x/9.x (PHP 8.1–8.3), on voit encore des patterns où des modules stockent des états complexes en base (configuration, paniers, payloads de connecteurs) sous forme sérialisée, ou reconstruisent des objets applicatifs depuis un blob. Le core a aussi historiquement manipulé des structures “riches” côté cookie et cache ; la surface réelle dépend de la version et des modules installés.
Deux points “terrain” à garder en tête (particulièrement vrai sur des boutiques françaises/UE où la disponibilité et la confidentialité ont un impact business et conformité) :
- Les données sérialisées deviennent vite “persistantes” : une valeur sérialisée peut se retrouver en base (ex. table de configuration/module), dans un cache, ou dans un message de file d’attente, puis être relue dans un contexte plus privilégié (cron, back-office).
- L’impact dépasse la technique : sur une boutique, une compromission via injection d’objets peut mener à du skimming, à l’accès à des données personnelles ou à une défiguration. En cas de violation de données personnelles, les obligations RGPD (notamment notification sous 72h, art. 33) peuvent entrer en jeu selon le contexte et la portée.
Avant de patcher, il faut d’abord cartographier où la désérialisation est utilisée et quelles données la traversent.
Ce qui déclenche réellement une injection d’objets : sources de données, gadgets et “magic methods”
Une injection d’objets exploitable nécessite généralement trois éléments : (1) une désérialisation sur une donnée contrôlable, (2) la possibilité d’instancier une ou plusieurs classes “intéressantes”, et (3) l’existence d’un chemin d’exécution déclenché par des méthodes magiques (__wakeup(), __destruct(), __toString(), __call(), __invoke(), etc.). L’attaquant ne “télécharge” pas du code ; il orchestre un comportement déjà présent via une gadget chain (chaîne de gadgets), typiquement au travers de dépendances Composer (bibliothèques de logging, clients HTTP, helpers fichiers, etc.).
Le vecteur d’entrée le plus fréquent en boutique reste : cookie modifiable, paramètre HTTP “opaque”, champ de formulaire non filtré, ou contenu stocké en DB puis relu et désérialisé dans un contexte de confiance. En pratique, la donnée peut être encodée (Base64, URL-encoding), chiffrée “maison” (mauvaise idée si la clé fuit), ou compressée. Le point clé : si l’attaquant peut faire arriver une chaîne sérialisée arbitraire jusqu’à unserialize(), l’exploitation dépend du graphe de classes chargé (autoload) à cet instant.
OWASP souligne que la désérialisation non sécurisée est régulièrement associée à des impacts sévères (jusqu’à l’exécution de code à distance) et la traite dans ses recommandations de sécurité applicative, notamment via OWASP Top 10 2021 – A08: Software and Data Integrity Failures :
https://owasp.org/Top10/A08_2021-Software_and_Data_Integrity_Failures/
Sur des stacks PHP réelles, l’impact est souvent aggravé par des classes ayant des effets de bord sur le système de fichiers (write/cleanup), des wrappers réseau, des loggers configurables, ou des objets qui déclenchent des accès à des ressources lors du destruct. Dans une boutique PrestaShop, l’exposition est rarement “théorique” si vous avez un parc de modules tiers hétérogène.
Mini-scénario typique (réaliste sans être “spectaculaire”) :
- un module reçoit un payload (paramètre, cookie ou champ caché) qu’il stocke en base “pour reprise” ;
- un cron de synchronisation relit la valeur, appelle
unserialize()puis manipule le résultat comme un objet ; - l’attaquant ne vise pas l’exécution de code immédiate, mais force un effet de bord lors d’un
__destruct()(ex. suppression/écriture de fichier de cache, appel HTTP sortant), ce qui suffit parfois à exfiltrer des données, empoisonner un cache, ou préparer une compromission plus profonde.
Éviter unserialize() en production : formats alternatifs, DTO et validation stricte
La mesure la plus fiable reste de ne pas désérialiser d’objets PHP depuis des données non maîtrisées. Si l’objectif est d’échanger/stocker un état, remplacez les payloads sérialisés par du JSON (ou un format structuré équivalent) et reconstruisez ensuite des objets explicitement via des DTO (Data Transfer Objects) typés. En PHP 8.2/8.3, l’option JSON_THROW_ON_ERROR + JsonException permet d’éviter les états partiellement valides et de gérer proprement les erreurs.
Exemple de pattern “stockage d’état” à préférer (PrestaShop 8/9, PHP ≥ 8.1) :
final class ExportJobState
{
public function __construct(
public readonly int $shopId,
public readonly string $status,
public readonly int $processed,
public readonly int $total,
) {}
public static function fromArray(array $data): self
{
foreach (['shopId','status','processed','total'] as $k) {
if (!array_key_exists($k, $data)) {
throw new InvalidArgumentException("Missing key: $k");
}
}
return new self(
(int) $data['shopId'],
(string) $data['status'],
(int) $data['processed'],
(int) $data['total'],
);
}
}
// Stockage
$payload = json_encode($state, JSON_THROW_ON_ERROR);
// Lecture
$data = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
$state = ExportJobState::fromArray($data);
L’intérêt est double : vous supprimez la classe d’attaque “injection d’objets PHP”, et vous forcez une validation de schéma (même minimaliste) qui protège aussi contre la corruption accidentelle des données.
Pour aller plus loin sans complexifier à outrance, une “checklist” DTO/JSON efficace en équipe :
- Schéma explicite : clés attendues + types + valeurs autorisées (
statusdans une liste fermée). - Contrôles de bornes : tailles max (ex.
status≤ 32), entiers ≥ 0, etc. - Versionnement de payload : ajoutez
v: 1dans le JSON afin de pouvoir faire évoluer le format sans casser l’existant. - Gestion d’erreurs : ne “réparez” pas silencieusement un payload invalide ; logguez et rejetez.
Pour la sérialisation avancée (normalization, groupes, attributs), le Symfony Serializer (déjà présent dans des stacks proches de PrestaShop 9) peut être pertinent, mais évitez de le configurer pour “hydrater” arbitrairement des classes depuis une entrée utilisateur ; ce point est souvent mal compris en équipe.
Si vous devez désérialiser : allowed_classes, signature, et périmètre de confiance
Il existe des cas où serialize() est historiquement intégré (cache applicatif, compat module, files d’attente). Dans ces cas, la règle n’est pas “utiliser unserialize() proprement” (ça n’existe pas), mais réduire drastiquement la capacité d’instanciation d’objets et prouver que la donnée est authentique. Le premier garde-fou est allowed_classes.
// Interdit toute instanciation d'objet : renvoie des __PHP_Incomplete_Class si un objet est présent
$value = unserialize($raw, ['allowed_classes' => false]);
// Variante : liste blanche stricte si vous contrôlez 100% des classes
$value = unserialize($raw, ['allowed_classes' => [SafeDto::class]]);
allowed_classes => false n’est pas une baguette magique : il évite l’instanciation d’objets, mais il ne corrige pas un problème de design si vous désérialisez des structures profondément imbriquées sans contrôle, ou si votre application s’appuie sur les objets reconstruits. Donc il faut en plus :
- Limiter la taille du payload (sinon risque de consommation CPU/mémoire et de parsing coûteux).
- Valider les types attendus (tableau, scalaires, profondeur max).
- Refuser ce qui sort du schéma (y compris des tableaux inattendus, pas seulement des objets).
Astuce pratique : encapsulez la désérialisation dans une fonction “gatekeeper” plutôt que d’éparpiller des unserialize() :
function safe_unserialize_array(string $raw, int $maxBytes = 16384): array
{
if (strlen($raw) > $maxBytes) {
throw new RuntimeException('Payload trop volumineux');
}
$v = unserialize($raw, ['allowed_classes' => false]);
if (!is_array($v)) {
throw new RuntimeException('Type inattendu après désérialisation');
}
return $v;
}
Ensuite, si la donnée transite côté client (cookie, local storage via API, paramètre), il faut authentifier le contenu (HMAC) pour éviter la modification. En PHP moderne, utilisez Sodium si possible.
// Signature (serveur -> client)
$payload = base64_encode($raw);
$mac = base64_encode(sodium_crypto_auth($payload, $secretKey));
$token = $payload . "." . $mac;
// Vérification (client -> serveur)
[$payload, $mac] = explode('.', $token, 2);
if (!sodium_crypto_auth_verify(base64_decode($mac), $payload, $secretKey)) {
throw new RuntimeException('Tampering detected');
}
$raw = base64_decode($payload);
$value = unserialize($raw, ['allowed_classes' => false]);
Ça ne rend pas la désérialisation “safe” en soi, mais ça déplace le problème : l’attaquant doit d’abord voler la clé serveur. Dans un contexte PrestaShop, traitez cette clé comme un secret : hors webroot, permissions minimales, rotation planifiée, et séparation par environnement (prod ≠ préprod). Pour la gestion des clés et le durcissement API, voir aussi l’article interne API : sécuriser apikey, limiter le débit et renforcer la conformité.
Désérialisation “invisible” via PHAR : quand unserialize() n’est même pas dans le code
Un angle mort fréquent : la désérialisation peut être déclenchée indirectement via les archives PHAR. PHP peut désérialiser les métadonnées d’un PHAR lorsqu’un code manipule des chemins phar://... (ou, plus généralement, quand des fonctions PHP traitent un chemin interprété comme flux PHAR). Résultat : vous pouvez avoir une injection d’objets sans appel direct à unserialize(), simplement parce qu’un flux de fichiers (upload, import, génération de vignettes, lecture d’images) accepte un chemin contrôlable.
En e-commerce, ce risque monte quand des modules traitent des fichiers fournis par des tiers : imports CSV/Excel, images, archives, connecteurs PIM/ERP. Ce n’est pas le format du fichier qui suffit à protéger : un fichier peut être “valide” en surface mais contenir un PHAR malicieux, ensuite référencé via un chemin inattendu. Si vous avez un pipeline d’import, gardez une séparation stricte entre zone de dépôt et zone de traitement, et ne traitez jamais un chemin fourni tel quel.
Mesures applicatives simples (souvent suffisantes) :
- Interdire tout schéma dans un chemin reçu (refuser
:et//, ou valider via une allowlist stricte de répertoires). - Normaliser via
realpath()puis vérifier que le fichier est bien dans un répertoire attendu (ex./var/www/.../upload/imports/). - Ne jamais passer un chemin “utilisateur” à des fonctions de traitement sans contrôle (image, PDF, zip, etc.).
Côté configuration, durcissez PHAR : phar.readonly=1 empêche la création de PHAR à l’exécution, et phar.require_hash=1 force une signature PHAR (utile contre certaines altérations, pas contre tous les scénarios). Ces directives ne remplacent pas un correctif applicatif, mais elles réduisent le rayon d’action. Elles doivent être alignées avec votre modèle d’hébergement (FPM/Apache/LiteSpeed/FrankenPHP). Pour la partie runtime PrestaShop/PHP (workers, compatibilités), l’article interne FrankenPHP : stabilité des workers et compatibilité PrestaShop en pratique aide à cadrer les différences d’exécution et de persistance en mémoire.
Durcissement applicatif autour de PrestaShop : modules, contrôleurs, et frontières de confiance
Le point “sale” dans PrestaShop, c’est la coexistence de contrôleurs modernes, de legacy controllers, de hooks et de modules qui partagent des données de manière implicite. Une injection d’objets PHP ne vient pas toujours d’un code “évidemment vulnérable” : elle apparaît quand une donnée traverse plusieurs couches (HTTP → module → DB → cron → back-office) sans qu’aucune ne définisse une frontière de confiance claire.
Concrètement, sur une boutique PrestaShop 8.1/9.x, ciblez en priorité : (1) endpoints exposés (front controllers, AJAX), (2) webhooks tiers, (3) imports/exports, (4) fonctionnalités qui stockent un état côté client (cookie) ou côté DB (table de config/module). Beaucoup de correctifs sont des micro-changements : remplacer unserialize(Tools::getValue('x')) par json_decode(), ou au minimum empêcher les objets via allowed_classes => false.
Un moyen pragmatique de prioriser (utile quand vous avez “trop” de modules) est de classer chaque flux selon exposition et persistant :
| Flux | Exposition | Persistant (DB/cache) | Risque typique | Priorité |
|---|---|---|---|---|
Paramètre front (GET/POST/AJAX) → unserialize() |
Haute | Non | Injection directe | Critique |
Cookie/headers → stockage → unserialize() en cron |
Haute | Oui | Escalade via contexte privilégié | Critique |
DB interne (valeur écrite uniquement côté BO) → unserialize() |
Moyenne | Oui | Exploitable après autre compromission | Élevée |
| Cache serveur non exposé (clé non contrôlable) | Faible | Oui | Moins probable, mais à encadrer | Moyenne |
Ne vous reposez pas sur des “pare-feux logiques” approximatifs (regex sur O: dans la chaîne sérialisée, blacklist de classes). Ce type de filtrage est fragile, contournable (encodages, compression, variations), et génère des faux positifs. Si vous voulez un amortisseur côté edge, utilisez plutôt un WAF en sachant que ça ne remplace pas la correction. Sur ce sujet, l’article interne WAF PrestaShop : réduire les faux positifs et sécuriser le checkout est utile pour mettre en place des règles sans casser le tunnel.
Audit : trouver les désérialisations, qualifier le risque, et couvrir par des tests
Le minimum syndical, c’est de localiser les usages. Sur un codebase PrestaShop + modules, un ripgrep est souvent plus rentable qu’un audit “à l’œil” :
rg -n "\bunserialize\s*\(" -S .
rg -n "phar://" -S .
rg -n "serialize\s*\(" -S .
Ajoutez aussi, selon votre stack, la recherche des variantes (si présentes) : igbinary_unserialize, unserialize(base64_decode(, ou des helpers internes de module.
Ensuite, pour chaque occurrence, posez trois questions :
- Quelle est la source de
$raw(HTTP/DB/cache/fichier) ? - La donnée est-elle authentifiée (signature/HMAC) ou seulement “obscurcie” (Base64, chiffrement maison) ?
- Quelles classes sont chargées à cet endroit (core + vendor + modules) ? C’est cela qui décide de l’exploitabilité.
L’erreur classique est de patcher “là où l’on voit unserialize()” sans comprendre que la donnée vient d’un cookie non signé ou d’une colonne DB modifiable via une autre faille.
Pour industrialiser, ajoutez un contrôle en CI (Semgrep, Psalm/PHPStan avec règles custom). Un exemple simple : une règle Semgrep qui bloque unserialize($_GET) / unserialize($_POST) / unserialize(Tools::getValue(...)). Et surtout, ajoutez des tests de non-régression :
- tests unitaires sur vos parseurs JSON/DTO (cas limites, champs manquants, types invalides) ;
- tests fonctionnels sur les endpoints exposés (réponse attendue si payload invalide) ;
- tests “migration” si vous basculez un stockage de
serialize()vers JSON.
Si vous avez besoin d’observer finement en pré-prod, activez un débogage contrôlé (sans exposer en prod) ; voir Xdebug : installer et activer Xdebug 3 pour PHP CLI et web et Xdebug VS Code : configurer le débogage PHP en local.
Mise en production : patch management, monitoring, et réponse à incident si exploitation suspectée
Corriger une désérialisation vulnérable sur une boutique PrestaShop n’est pas un “commit et on push”. Vous touchez souvent à des formats persistés (DB/caches) : il faut prévoir une migration (convertir serialized → JSON), gérer la rétrocompat (double lecture), et purger les caches proprement.
Une approche de migration qui évite les coupures :
- Écrire en JSON dès maintenant (nouveau champ/clé), tout en continuant à lire l’ancien format si le nouveau n’est pas présent.
- Lancer un job (CLI/cron) de rattrapage qui convertit progressivement l’existant.
- Une fois le taux de conversion atteint, désactiver la lecture legacy, puis nettoyer (drop colonne/clé legacy).
Si vous êtes en train de migrer ou de mettre à jour la boutique, intégrez ces changements dans une séquence robuste : sauvegarde, pré-production, tests de charge, rollback. Référez-vous à Mise à jour PrestaShop : checklist sauvegarde, pré-production et tests et, en cas de dérapage, Mise à jour PrestaShop ratée : diagnostiquer et réparer rapidement.
Côté monitoring, surveillez les signaux faibles :
- augmentation des 500 ;
- erreurs de type
unserialize(): Error at offset ...(souvent signe de corruption ou de tentatives d’injection) ; - pics d’accès sur des endpoints “secondaires” (controllers AJAX oubliés, endpoints module) ;
- comportements anormaux sur
cron/workers ; - écritures inattendues sur le FS (répertoire cache, overrides, modules).
L’article PrestaShop monitoring d’erreurs : logs PHP, MySQL, JavaScript et alertes e-mail couvre une base saine pour ne pas découvrir la compromission via une plainte client.
Si vous suspectez une exploitation (ou que vous venez de corriger une injection d’objets), partez du principe que l’attaquant a cherché la persistance : webshell, cron, override, module ajouté, JS skimmer. Il faut un containment rapide (isoler, couper certaines routes, basculer en maintenance si nécessaire), puis investigation et rotation des secrets. Pour le cadrage IR, suivez une démarche outillée comme dans Sécurité PrestaShop : plan de réponse à incident et containment immédiat, et n’oubliez pas que la désérialisation non sécurisée se combine souvent avec d’autres failles applicatives (XSS, BAC/IDOR). À ce titre : XSS : checklist de durcissement PrestaShop, CSP et encodage contexte et Broken access control : prévenir IDOR et élévation de privilèges font partie des compléments logiques.
Check-list opérationnelle (désérialisation PHP) pour code PrestaShop et modules
Première passe (1–2h) : inventaire des occurrences (unserialize, serialize, phar://), tri par exposition (front vs back vs cron), et classification des sources de données (HTTP, cookie, DB, fichiers). Si vous trouvez une désérialisation sur une valeur issue de Tools::getValue() ou d’un cookie sans signature forte, considérez-le comme critique jusqu’à preuve du contraire.
Deuxième passe (1–2 jours) : refactor vers JSON + DTO typés, ou à défaut allowed_classes => false + validation stricte + signature (HMAC/Sodium). Prévoyez une migration des données persistées et une compatibilité transitoire (lecture des deux formats pendant une fenêtre), sinon vous allez casser des jobs ou des écrans BO en production.
Troisième passe (CI / run) : règle Semgrep/PHPStan bloquante, tests de non-régression sur les endpoints concernés, durcissement PHP (PHAR, permissions, séparation des répertoires d’upload), et monitoring des erreurs de désérialisation.
Checklist “anti-oublis” (utile pour les revues de PR module) :
- [ ] Aucune donnée issue du client (HTTP/cookie/header) n’est passée à
unserialize()(directement ou après base64/decrypt). - [ ] Si
unserialize()est indispensable :allowed_classes => false(ou allowlist stricte) + limite de taille + validation de type. - [ ] Tout payload côté client est authentifié (MAC) et versionné.
- [ ] Les chemins de fichiers passés aux traitements sont normalisés (
realpath) et bornés à un répertoire attendu ; aucun schéma (phar://, etc.) n’est accepté. - [ ] Migration planifiée pour supprimer à terme les blobs sérialisés persistants (DB/cache).
- [ ] Alerting sur erreurs de désérialisation + pics 500 + endpoints anormaux.
Le but n’est pas d’atteindre une “sécurité parfaite” (mythe), mais de rendre la désérialisation PHP non exploitable en éliminant l’instanciation d’objets depuis des données non fiables, et en fermant les routes d’entrée qui rendent l’injection d’objets rentable pour un attaquant.
