1. Sécurité de la sérialisation binaire
La sérialisation en Java — ce n’est pas simplement sauvegarder les champs d’un objet. C’est la possibilité de « reconstruire » n’importe quel objet avec n’importe quel contenu, pourvu qu’il implémente l’interface Serializable. Cela semble pratique ! Mais si votre application désérialise des données provenant d’une source non fiable (par exemple, du réseau ou d’un fichier qu’un attaquant a pu remplacer), elle risque de devenir la victime d’attaques.
Comment cela fonctionne-t-il ?
Pendant la désérialisation, Java crée des objets à partir d’un flux binaire sans appeler les constructeurs des classes. Si des méthodes spéciales comme readObject sont présentes dans la classe, elles seront appelées automatiquement. Un attaquant peut « construire » le flux d’octets de manière à exécuter du code vulnérable lors de la désérialisation.
Exemple : attaque « gadget chain »
Imaginez que vous avez une classe qui, lors de la désérialisation, lance une commande externe (lit un fichier ou appelle le shell). Si un attaquant sait que l’application désérialise des objets de certains types, il peut fournir un flux d’octets spécialement conçu qui conduira à l’exécution de code malveillant. Ces attaques se construisent comme une « chaîne de gadgets » — une séquence d’appels se terminant par une opération dangereuse.
Pourquoi est-ce si critique ?
La désérialisation est un processus où des objets réels sont reconstruits à partir d’un flux de données, et à ce moment-là du code arbitraire peut s’exécuter. Si les données proviennent d’une source non fiable, cela ouvre la porte à l’exécution de commandes à distance (RCE). De grandes entreprises ont publié des recommandations pour éviter la désérialisation non sécurisée ; dans les projets d’entreprise modernes, la sérialisation binaire est souvent interdite par les politiques de sécurité.
Comment se protéger ?
- Ne désérialisez jamais des objets provenant de sources non fiables.
- Utilisez une liste blanche (whitelisting) — une liste explicite des types autorisés à la désérialisation.
- Préférez les formats texte pour les intégrations externes : JSON, XML.
- Si la sérialisation est inévitable, utilisez des bibliothèques avec des réglages de désérialisation sécurisée (par exemple, Jackson avec restriction des types).
- Limitez l’utilisation de méthodes de sérialisation non standard (readObject, readResolve, etc.) si vous n’êtes pas certain de leur sécurité.
2. Compatibilité des versions de classes
La sérialisation binaire en Java est étroitement liée à la structure de la classe. Vous avez sérialisé un objet en version 1.0, puis mis à jour la classe (ajout/suppression d’un champ) — tenter de désérialiser l’« ancien » objet dans la nouvelle version peut conduire à une erreur ou à une perte de données.
Comment Java détermine-t-il la compatibilité ?
Pour cela, un champ spécial est utilisé — serialVersionUID. C’est l’identifiant de version de la classe. Si l’objet sérialisé a un serialVersionUID et que la classe actuelle en a un autre — une InvalidClassException sera levée et la désérialisation ne se fera pas.
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L; // version spécifiée explicitement
private String name;
private int age;
}
Si vous modifiez la structure de la classe (par exemple, en ajoutant un champ email) sans changer le serialVersionUID, Java considérera la classe comme compatible et essaiera de désérialiser l’ancien objet. Si vous n’avez pas indiqué serialVersionUID explicitement, la JVM le générera automatiquement sur la base de la structure, et toute modification entraînera une incompatibilité.
Que se passe-t-il en cas de non-correspondance/correspondance des versions ?
Si les identifiants ne correspondent pas — la désérialisation n’aura pas lieu : InvalidClassException. S’ils correspondent — les champs sont appariés par nom et par type : les nouveaux champs recevront des valeurs par défaut (null, 0), les champs supprimés seront ignorés. En cas de modification du type ou du nom d’un champ, des erreurs et une interprétation incorrecte des données sont possibles.
Conseil pratique. Dans les classes sérialisables, indiquez toujours explicitement le serialVersionUID. Ne le modifiez qu’en cas de changements incompatibles (suppression/changement de type d’un champ important). Lors de l’ajout de nouveaux champs, l’identifiant peut rester inchangé — la JVM gérera correctement les anciens objets.
Tableau : Que se passe-t-il lors d’une modification de la classe
| Modification dans la classe | Que se passe-t-il lors de la désérialisation ? |
|---|---|
| Ajout d’un nouveau champ | Reçoit la valeur par défaut (0, null) |
| Suppression d’un champ | Ignoré lors de la lecture des anciennes données |
| Changement du type d’un champ | Exception ou données incorrectes |
| Changement du nom d’un champ | L’ancien champ est ignoré, le nouveau — valeur par défaut |
| Modification de serialVersionUID | Exception InvalidClassException |
3. Limites de la sérialisation standard
Tous les objets ne sont pas sérialisables
Les champs avec les modificateurs transient et static ne sont pas sérialisés. static — parce qu’il appartient à la classe et non à l’objet ; transient — parce que vous avez explicitement interdit la sérialisation du champ.
Certains objets ne sont, par définition, pas sérialisables : Thread, connexions à la base de données, sockets, Scanner, etc. Si votre classe possède un champ de ce type et qu’il n’est pas transient, vous obtiendrez une NotSerializableException.
import java.io.Serializable;
import java.util.Scanner;
public class Session implements Serializable {
private transient Scanner scanner; // ne se sérialise pas !
private String login;
}
Problèmes de performance et d’évolutivité
La sérialisation de grands graphes d’objets peut être lente et gourmande en mémoire.
Le format binaire se prête mal aux intégrations avec d’autres plateformes et langages — seul Java le « comprend ».
Il est difficile de contrôler précisément ce qui est sérialisé, surtout avec des hiérarchies profondes et des références cycliques.
Problèmes liés à la prise en charge des anciennes données
La conservation à long terme d’instantanés binaires est risquée. En un ou deux ans, la structure des classes change et les anciens fichiers ne se chargent plus.
Histoire réelle : « Nous avons enregistré un cache d’utilisateurs sérialisé il y a trois ans, nous avons mis à jour l’application, et maintenant nous ne pouvons plus le charger. Bonjour, données perdues ! »
4. Bonnes pratiques : comment éviter les pièges
- N’utilisez la sérialisation binaire que pour des besoins internes, où vous contrôlez les deux côtés du processus.
- N’utilisez pas la sérialisation binaire pour les intégrations externes et le stockage à long terme de données importantes.
- Indiquez toujours explicitement le serialVersionUID dans les classes sérialisables.
- Marquez les champs qui ne doivent pas être sérialisés avec le modificateur transient.
- Pour les échanges avec des systèmes externes — formats texte et bibliothèques modernes : JSON, XML, Jackson, Gson, JAXB.
- Pour la compatibilité, utilisez la gestion de versions : enregistrez la version de l’objet dans la classe elle-même et adaptez le traitement lors de la désérialisation.
- Si la sérialisation n’est nécessaire que pour le cache — ne cherchez pas à maintenir la compatibilité à tout prix : il est plus simple de recalculer le cache.
- N’enregistrez pas de données sensibles (mots de passe, clés) dans des objets sérialisables — la sérialisation ne chiffre pas les données.
5. Erreurs courantes lors de l’utilisation de la sérialisation binaire
Erreur n° 1 : Désérialiser des données provenant d’une source non fiable. L’erreur la plus dangereuse — accepter et désérialiser des objets venus de « l’extérieur » (du réseau, de l’utilisateur, d’un fichier substitué). C’est une voie directe vers des vulnérabilités, jusqu’à l’exécution de commandes à distance (RCE).
Erreur n° 2 : Modifier implicitement la structure de la classe sans mettre à jour serialVersionUID. Si vous ne spécifiez pas l’identifiant explicitement, la JVM le générera automatiquement. Tout changement de structure (même l’ordre des champs) entraînera une incompatibilité et l’impossibilité de charger les anciens objets.
Erreur n° 3 : Tenter de sérialiser des objets contenant des champs non sérialisables. Si une classe possède un champ qui n’implémente pas Serializable et qu’il n’est pas transient, la sérialisation se terminera par une exception.
Erreur n° 4 : Stocker dans des objets sérialisables des données temporaires ou sensibles. Jetons, mots de passe, descripteurs temporaires de ressources — tout cela peut se retrouver par inadvertance dans un fichier.
Erreur n° 5 : Utiliser la sérialisation binaire pour un stockage à long terme et pour l’échange entre versions. Après la première mise à jour des classes, le risque de « données corrompues » et de problèmes de compatibilité est élevé.
Erreur n° 6 : S’attendre à ce que les champs static et transient « reviennent » après désérialisation. Ces champs ne sont pas sérialisés ; après chargement, ils auront des valeurs par défaut.
GO TO FULL VERSION