Leçon 5/6 10 min

Désérialisation non sécurisée

Un cookie sérialisé devient un objet piégé qui écrit un webshell. Déclenche la chaîne ; fix : JSON, jamais unserialize.

Un cookie qui se transforme en objet piégé

Dans la leçon précédente sur les injections de template (SSTI), un moteur exécutait une expression injectée dans du texte. Ici, même principe, autre vecteur : c'est un objet entier que PHP ressuscite depuis un cookie, avec son code embarqué. Une donnée redevient du code.

Pour retenir les préférences d'un visiteur, une appli sérialise un objet PHP et le range dans un cookie. À chaque visite, elle le reconstruit :

$prefs = unserialize($_COOKIE['prefs']);   // reconstruit l'objet depuis le cookie
echo "Thème : " . $prefs->theme;

Le problème : le cookie est entre les mains de l'utilisateur. Et unserialize() ne reconstruit pas qu'une donnée, il recrée un objet de la classe indiquée dans la chaîne, avec les propriétés qu'on lui donne. Or PHP exécute automatiquement certaines méthodes magiques (__wakeup, __destruct…) lors de cette reconstruction.

L'attaquant fabrique alors un cookie qui décrit un objet d'une autre classe de l'application, choisie parce que sa méthode magique fait quelque chose d'utile. Exemple : une classe Logger dont le __destruct écrit un fichier. En forgeant le bon objet, on lui fait écrire un webshell :

class Logger {
    public $file, $src;
    function __destruct() { file_put_contents($this->file, $this->src); }
}
// cookie forgé → Logger(file = "shell.php", src = "<?php system(...) ?>")
// à la fin de la requête, __destruct écrit shell.php : RCE

C'est la désérialisation non sécurisée : reconstruire un objet à partir d'une donnée non fiable déclenche du code que l'attaquant a choisi. C'est une faille d'intégrité des données (A08 du Top 10 OWASP 2025, « Software and Data Integrity Failures »).

Désérialisation : objet reconstruit vs donnée lue unserialize (objet) cookie O:... objet reconstruit RCE reconstruit __destruct() json_decode (donnée) cookie JSON données (tableau) aucun code lu inerte
unserialize recrée un objet et déclenche ses méthodes magiques : du code s'exécute. json_decode ne produit que des données, aucun objet, aucune méthode.

Pourquoi ça marche

La confusion est subtile : on croit échanger une donnée (« mes préférences »), alors qu'on échange en réalité un objet complet, avec sa classe et ses propriétés. La sérialisation transporte le type, pas seulement les valeurs. Reconstruire ce type, c'est exécuter de la logique.

L'attaquant ne crée pas une nouvelle classe : il réutilise celles déjà présentes dans l'application (ou ses bibliothèques). Il cherche une classe dont une méthode magique fait une action intéressante, et enchaîne parfois plusieurs classes pour atteindre son but. On appelle ça une chaîne de gadgets (POP chain). Un « gadget » n'est pas un outil externe : c'est une classe déjà chargée dans l'appli que l'attaquant détourne. Il ne forge pas le couteau ; il trouve le tiroir où il est déjà rangé.

  • Les méthodes magiques sont les déclencheurs : __wakeup, __destruct, __toString s'exécutent toutes seules.
  • Le format trahit la cible : une chaîne PHP commence par O:, un objet Java sérialisé par rO0, un pickle Python a sa propre signature.
  • L'impact final va de l'écriture de fichier au RCE complet.

Le faux remède : « je chiffre ou j'encode le cookie » ne suffit pas si la clé fuit ou si l'encodage est public (base64 n'est pas un secret). Et filtrer des mots-clés dans la chaîne sérialisée est vain : les formats ont mille variantes. La vraie parade ne sécurise pas unserialize, elle l'évite sur toute donnée non fiable (section 4).

À vous d'attaquer : forgez l'objet piégé

Voici un lecteur de cookie de préférences vraiment vulnérable, simulé dans votre navigateur. En mode vulnérable, il unserialize() le cookie tel quel. Chargez l'objet piégé et observez sa méthode magique se déclencher. Activez ensuite la version sécurisée (JSON). Tout est simulé.

Cookie « prefs » · monsite.com

        
Bloqué ? Voir la solution

Cliquez sur Objet piégé puis Charger en mode vulnérable : le cookie décrit un objet Logger. En le reconstruisant, unserialize déclenche son __destruct, qui écrit shell.php sur le serveur : un webshell est déposé, c'est le RCE.

En version sécurisée, le serveur fait json_decode : il ne lit que des données, n'instancie aucun objet et n'appelle aucune méthode. La chaîne piégée n'est même pas du JSON valide : elle est ignorée.

Le correctif : des données, pas des objets

La parade tient en une idée : n'échangez que des données, jamais des objets reconstruits. Pour transporter des préférences, un panier, une session, utilisez un format de données pures comme JSON, qui n'instancie aucune classe et n'exécute aucune méthode.

// ✗ Vulnérable : reconstruit un objet arbitraire
$prefs = unserialize($_COOKIE['prefs']);

// ✓ Sûr : JSON ne lit que des données (tableau associatif)
$prefs = json_decode($_COOKIE['prefs'], true);
$theme = in_array($prefs['theme'] ?? '', ['clair', 'sombre'], true) ? $prefs['theme'] : 'clair';

Si vous devez absolument désérialiser des objets (héritage technique), deux garde-fous se combinent : signer la charge avec un HMAC (pour détecter toute modification) et limiter les classes autorisées.

// refuser la reconstruction de toute classe
$data = unserialize($input, ['allowed_classes' => false]);
// + vérifier une signature HMAC AVANT de désérialiser

Ne comptez pas sur le chiffrement seul. Chiffrer ou signer le cookie aide, mais reste fragile si la clé fuit, et beaucoup d'apps oublient de vérifier la signature avant de désérialiser. La règle robuste : ne jamais unserialize ce que l'utilisateur peut toucher. Du JSON pour les données, et basta.

Défense en profondeur :

  1. JSON (ou un format de données pur) pour tout ce qui transite par le client ;
  2. jamais unserialize (PHP), pickle (Python) ou readObject (Java) sur de l'entrée non fiable ;
  3. si inévitable : signature HMAC vérifiée avant, et liste de classes autorisées ;
  4. moindre privilège sur le process (limite l'impact d'une chaîne de gadgets) ;
  5. dépendances à jour : les gadgets viennent souvent des bibliothèques (leçon « Dépendances »).

Référence : OWASP Deserialization Cheat Sheet.

La méthode et l'arsenal du pentester

Vous savez maintenant comment s'en défendre. Voici comment un attaquant la trouve et l'exploite concrètement.

1. Repérer la sérialisation. Un cookie, un champ caché ou un jeton qui ressemble à O:4:"User"..., à du base64 commençant par rO0 (Java) ou gAR/gAJ (pickle Python) : la donnée transporte un objet.

2. Confirmer la reconstruction. On modifie la charge et on observe : si l'app se comporte selon le type qu'on injecte, elle désérialise notre entrée. Parfois une erreur trahit la classe attendue.

3. Forger la chaîne de gadgets. On assemble des classes déjà présentes dans l'app ou ses dépendances pour atteindre une action dangereuse, jusqu'au RCE. C'est rarement fait à la main : des outils connaissent les chaînes des frameworks courants.

L'arsenal.

L'impact

La désérialisation non sécurisée est l'une des failles les plus puissantes, car elle mène souvent au RCE sans aucune authentification : il suffit d'un cookie ou d'un jeton manipulable. Côté Java, elle a provoqué une vague de compromissions massives (Apache Struts, Oracle WebLogic, Jenkins) où un simple objet sérialisé livrait le serveur.

Même sans aller jusqu'au RCE, une chaîne de gadgets peut écrire ou supprimer des fichiers, déclencher une SSRF, contourner une authentification, ou corrompre l'état de l'application. Et le danger est discret : la faille ne vit pas dans votre code mais dans la combinaison de classes que vos dépendances apportent, ce qui la rend difficile à repérer.

Ce que ça révèle côté défense : une donnée qui transite par le client n'est jamais de confiance, et la reconstruire en objet, c'est lui donner le pouvoir d'agir. On reste sur des données pures (JSON), on signe ce qui doit l'être, et on garde ses dépendances à jour. La frontière code / donnée, encore et toujours. La confiance se vérifie (leçon 1).

La checklist désérialisation

À vérifier sur tout cookie, jeton ou champ qui transporte un état.

  • JSON (ou format de données pur) pour tout ce qui passe par le client.
  • Jamais unserialize / pickle / readObject sur de l'entrée non fiable.
  • Si inévitable : signature HMAC vérifiée avant, et liste de classes autorisées.
  • Moindre privilège sur le process de traitement.
  • Dépendances à jour : les gadgets viennent des bibliothèques.

Les références. L'OWASP Deserialization Cheat Sheet couvre chaque langage. Pour s'entraîner : les labs Insecure deserialization de la Web Security Academy.

Rappel. Forger un objet pour attaquer un service qui n'est pas le vôtre est une intrusion. On ne teste que ses propres systèmes ou une cible explicitement autorisée (leçon 1 du cours principal).

A cookie that turns into a booby-trapped object

In the previous lesson on template injection (SSTI), a template engine executed an expression injected into text. Here, same principle, different vector: it's a full object that PHP resurrects from a cookie, with its embedded code. Data becomes code again.

To remember a visitor's preferences, an app serializes a PHP object and stores it in a cookie. On every visit, it rebuilds it:

$prefs = unserialize($_COOKIE['prefs']);   // rebuilds the object from the cookie
echo "Theme: " . $prefs->theme;

The problem: the cookie is in the user's hands. And unserialize() doesn't rebuild just data, it recreates an object of the class named in the string, with the properties you give it. And PHP automatically runs certain magic methods (__wakeup, __destruct…) during that reconstruction.

The attacker then crafts a cookie describing an object of another class in the application, chosen because its magic method does something useful. Example: a Logger class whose __destruct writes a file. By forging the right object, you make it write a webshell:

class Logger {
    public $file, $src;
    function __destruct() { file_put_contents($this->file, $this->src); }
}
// forged cookie → Logger(file = "shell.php", src = "<?php system(...) ?>")
// at the end of the request, __destruct writes shell.php: RCE

That's insecure deserialization: rebuilding an object from untrusted data triggers code the attacker chose. It's a data integrity flaw (A08 of the OWASP Top 10 2025, "Software and Data Integrity Failures").

Deserialization: rebuilt object vs read data unserialize (object) cookie O:... object rebuilt RCE rebuilt __destruct() json_decode (data) JSON cookie data (array) no code read inert
unserialize recreates an object and fires its magic methods: code runs. json_decode only produces data, no object, no method.

Why it works

The confusion is subtle: you think you're exchanging data ("my preferences"), while you're actually exchanging a full object, with its class and properties. Serialization carries the type, not just the values. Rebuilding that type means running logic.

The attacker doesn't create a new class: they reuse the ones already present in the application (or its libraries). They look for a class whose magic method does something interesting, and sometimes chain several classes to reach their goal. That's called a gadget chain (POP chain). A "gadget" is not an external tool: it's a class already loaded in the app that the attacker repurposes. They don't forge the knife; they find the drawer where it's already stored.

  • Magic methods are the triggers: __wakeup, __destruct, __toString run on their own.
  • The format reveals the target: a PHP string starts with O:, a serialized Java object with rO0, a Python pickle has its own signature.
  • The final impact ranges from a file write to full RCE.

The false cure: "I'll encrypt or encode the cookie" isn't enough if the key leaks or the encoding is public (base64 isn't a secret). And filtering keywords in the serialized string is futile: the formats have a thousand variants. The real fix doesn't secure unserialize, it avoids it on any untrusted data (section 4).

Your turn to attack: forge the booby-trapped object

Here's a genuinely vulnerable preferences-cookie reader, simulated in your browser. In vulnerable mode, it unserialize()s the cookie as-is. Load the booby-trapped object and watch its magic method fire. Then switch on the secure version (JSON). Everything is simulated.

Cookie "prefs" · mysite.com

        
Stuck? Show the solution

Click Booby-trapped object then Load in vulnerable mode: the cookie describes a Logger object. While rebuilding it, unserialize fires its __destruct, which writes shell.php on the server: a webshell is dropped, that's RCE.

In the secure version, the server uses json_decode: it reads only data, instantiates no object and calls no method. The trapped string isn't even valid JSON: it's ignored.

The fix: data, not objects

The fix is one idea: only exchange data, never rebuilt objects. To carry preferences, a cart, a session, use a pure data format like JSON, which instantiates no class and runs no method.

// ✗ Vulnerable: rebuilds an arbitrary object
$prefs = unserialize($_COOKIE['prefs']);

// ✓ Safe: JSON reads only data (associative array)
$prefs = json_decode($_COOKIE['prefs'], true);
$theme = in_array($prefs['theme'] ?? '', ['light', 'dark'], true) ? $prefs['theme'] : 'light';

If you absolutely must deserialize objects (legacy), two guardrails combine: sign the payload with an HMAC (to detect any change) and limit the allowed classes.

// refuse rebuilding any class
$data = unserialize($input, ['allowed_classes' => false]);
// + verify an HMAC signature BEFORE deserializing

Don't rely on encryption alone. Encrypting or signing the cookie helps, but stays fragile if the key leaks, and many apps forget to verify the signature before deserializing. The robust rule: never unserialize what the user can touch. JSON for data, full stop.

Defense in depth:

  1. JSON (or a pure data format) for anything passing through the client;
  2. never unserialize (PHP), pickle (Python) or readObject (Java) on untrusted input;
  3. if unavoidable: verified HMAC signature first, and allowed-classes list;
  4. least privilege on the process (limits a gadget chain's impact);
  5. dependencies up to date: gadgets often come from libraries (the "Dependencies" lesson).

Reference: OWASP Deserialization Cheat Sheet.

The pentester's method and arsenal

Now that you know how to defend against it, here is how an attacker finds and exploits it in practice.

1. Spot the serialization. A cookie, hidden field or token that looks like O:4:"User"..., base64 starting with rO0 (Java) or gAR/gAJ (Python pickle): the data carries an object.

2. Confirm the reconstruction. You tweak the payload and watch: if the app behaves according to the type you inject, it's deserializing your input. Sometimes an error reveals the expected class.

3. Forge the gadget chain. You assemble classes already present in the app or its dependencies to reach a dangerous action, up to RCE. It's rarely done by hand: tools know the chains of common frameworks.

The arsenal.

The impact

Insecure deserialization is one of the most powerful flaws, because it often leads to RCE with no authentication: a manipulable cookie or token is enough. On the Java side, it caused a wave of massive compromises (Apache Struts, Oracle WebLogic, Jenkins) where a single serialized object handed over the server.

Even short of RCE, a gadget chain can write or delete files, trigger an SSRF, bypass authentication, or corrupt the app's state. And the danger is quiet: the flaw lives not in your code but in the combination of classes your dependencies bring in, which makes it hard to spot.

What this reveals for defense: data that passes through the client is never trusted, and rebuilding it into an object gives it the power to act. You stay on pure data (JSON), sign what must be signed, and keep your dependencies up to date. The code / data boundary, again and again. Trust is verified (lesson 1).

The deserialization checklist

To check on every cookie, token or field that carries state.

  • JSON (or a pure data format) for anything passing through the client.
  • Never unserialize / pickle / readObject on untrusted input.
  • If unavoidable: verified HMAC signature first, and an allowed-classes list.
  • Least privilege on the processing process.
  • Dependencies up to date: gadgets come from libraries.

The references. The OWASP Deserialization Cheat Sheet covers each language. To practice: the Insecure deserialization labs of the Web Security Academy.

Reminder. Forging an object to attack a service that isn't yours is an intrusion. Only test your own systems or an explicitly authorized target (lesson 1 of the main course).

Prédisez avant de lire

Un dev range les préférences dans un cookie : l'un fait unserialize($_COOKIE['prefs']), l'autre json_decode($_COOKIE['prefs'], true). Avant de dérouler : lequel peut mener à l'exécution de code ?

Voir la réponse

Le premier. unserialize reconstruit un objet de la classe nommée dans le cookie, et PHP exécute ses méthodes magiques : un attaquant forge un objet d'une classe dangereuse (chaîne de gadgets) et déclenche du code, jusqu'au RCE. Le second est sûr : json_decode ne produit que des données (tableaux, chaînes, nombres), n'instancie aucune classe et n'appelle aucune méthode. C'est toute la différence entre échanger une donnée et reconstruire un objet.

Predict before reading on

A dev stores preferences in a cookie: one does unserialize($_COOKIE['prefs']), the other json_decode($_COOKIE['prefs'], true). Before you expand: which one can lead to code execution?

Show the answer

The first. unserialize rebuilds an object of the class named in the cookie, and PHP runs its magic methods: an attacker forges an object of a dangerous class (gadget chain) and triggers code, up to RCE. The second is safe: json_decode only produces data (arrays, strings, numbers), instantiates no class and calls no method. That's the whole difference between exchanging data and rebuilding an object.

🎯 Pratique

S'entraîner (clique pour ouvrir) :

💬 Ré-explique sans regarder
Ré-explique sans regarder

Avec tes mots : pourquoi unserialize sur un cookie est dangereux (que reconstruit-il, qu'est-ce qui se déclenche ?), et pourquoi json_decode ne pose pas ce problème ?

Une bonne explication dit : unserialize ne lit pas qu'une donnée, il reconstruit un objet de la classe écrite dans la chaîne, avec les propriétés fournies. PHP exécute alors ses méthodes magiques (__wakeup, __destruct…) toutes seules. Comme le cookie est contrôlé par l'utilisateur, l'attaquant forge un objet d'une classe déjà présente (une chaîne de gadgets) dont une méthode magique fait une action dangereuse : écrire un webshell, exécuter une commande (RCE). json_decode, lui, ne produit que des données (tableaux, chaînes, nombres) : aucune classe instanciée, aucune méthode appelée. La parade est donc d'échanger des données pures (JSON) et de ne jamais unserialize une entrée non fiable.
🧠 Rappel libre
Rappel libre

Sans remonter : explique ce qu'est une désérialisation non sécurisée, le rôle des méthodes magiques et de la chaîne de gadgets, et la parade (JSON / signature / classes autorisées).

Reconstruire un objet à partir d'une donnée non fiable (unserialize, pickle, readObject) recrée un objet de la classe indiquée et déclenche ses méthodes magiques (__wakeup, __destruct). L'attaquant assemble des classes déjà présentes (une chaîne de gadgets) pour atteindre une action dangereuse, souvent le RCE, sans authentification. Parade : n'échanger que des données pures (JSON) qui n'instancient rien ; si l'objet est inévitable, vérifier une signature HMAC avant et limiter les classes autorisées ; garder ses dépendances à jour (les gadgets en viennent).
⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

Tu signales la désérialisation à l'IA. Elle répond : « Je sécurise en encodant le cookie en base64 avant de l'unserialize : comme ça, personne ne peut le lire ni le modifier. » Tu acceptes, ou tu rejettes ?

À rejeter : le base64 n'est pas un secret, c'est un simple encodage que n'importe qui décode et ré-encode. L'attaquant forge son objet piégé, l'encode en base64, et le serveur le décode puis l'unserialize exactement comme avant : la faille est intacte. Cacher la donnée ne change rien tant qu'on reconstruit un objet à partir d'une entrée que l'utilisateur contrôle. La vraie correction abandonne unserialize sur l'entrée : on passe à JSON (données seules, aucun objet instancié). Si la désérialisation d'objets est imposée, il faut une signature HMAC vérifiée avant de désérialiser et une liste de classes autorisées, pas un simple encodage.
Pourquoi unserialize($_COOKIE['x']) est-il dangereux ?
Qu'est-ce qu'une « chaîne de gadgets » (POP chain) ?
Pourquoi json_decode ne souffre pas du même problème qu'unserialize ?
Tu dois absolument unserialize des objets (héritage technique). Que fais-tu ?
Prochaine étape

Vous avez transformé une donnée en code exécuté. La dernière leçon change de monde : la sécurité des applications IA, où l'injection de prompt fait dévier un assistant et où la frontière donnée / instruction redevient le cœur du problème.

Leçon 6 : Sécurité des apps IA →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement