1. Principales meilleures pratiques pour une sérialisation sécurisée
La sérialisation, c’est comme emballer ses bagages à l’aéroport : si vous ne savez pas ce qu’il y a dedans et à qui vous confiez la valise, vous risquez une mauvaise surprise au contrôle de sécurité. En Java, la sérialisation permet d’enregistrer et de restaurer facilement des objets, mais elle ouvre aussi la porte à tout un éventail d’attaques lorsque les données proviennent de sources non fiables.
Menace classique :
La sérialisation en Java peut être dangereuse. Si un attaquant injecte un flux malveillant, la désérialisation peut entraîner les pires conséquences : de la modification de champs à l’exécution de code indésirable. Ce n’est pas une histoire pour faire peur — il y a réellement eu, dans l’histoire de Java, des cas où des attaques reposaient précisément sur ce mécanisme.
Pourquoi cela se produit‑il ?
Le point clé est que la désérialisation n’est pas qu’une restauration de valeurs de champs. Au cours du processus, un objet complet est créé : des méthodes spéciales peuvent être appelées (par exemple, readObject, readResolve) et, parfois, des points vulnérables du code via la réflexion. Les classes de bibliothèques tierces sont particulièrement dangereuses : certaines effectuent des actions dès la phase de désérialisation. Par conséquent, ne faites jamais confiance aux données sérialisées provenant de l’extérieur.
Utilisez transient pour les données sensibles
Si votre classe contient des champs qui stockent des mots de passe, des jetons, des clés privées ou d’autres informations sensibles, déclarez‑les transient. Ces données ne seront pas incluses dans le flux sérialisé.
import java.io.Serializable;
public class User implements Serializable {
private String username;
private transient String password; // ne sera pas sérialisé
// ...constructeurs, getters, setters...
}
Que se passe‑t‑il lors de la désérialisation ? Le champ password aura la valeur par défaut (null pour les chaînes). C’est une bonne chose : les mots de passe ne seront ni stockés dans des fichiers ni transmis sur le réseau.
Définissez explicitement serialVersionUID
Indiquez toujours explicitement serialVersionUID. Cela réduit la probabilité d’erreurs de compatibilité et minimise le risque de substitution de classes lors de la désérialisation.
private static final long serialVersionUID = 1L;
Pourquoi est‑ce important pour la sécurité ? Si vous ne spécifiez pas serialVersionUID, le compilateur le générera automatiquement à partir de la structure de la classe. Cela peut conduire à des divergences inattendues et, en théorie, à des abus via la substitution de classes portant le même nom mais une structure différente.
Vérifiez les types des objets lors de la désérialisation
Ne faites pas confiance à ce qui arrive par le réseau ou depuis un fichier. Après désérialisation, vérifiez toujours que l’objet obtenu est du type attendu avant de travailler avec lui.
Object obj = objectInputStream.readObject();
if (obj instanceof User) {
User user = (User) obj;
// on peut travailler en sécurité avec user
} else {
// type inattendu — lever une exception ou gérer l’erreur
}
Pourquoi est‑ce nécessaire ? Un flux malveillant peut contenir un objet d’une autre classe qui implémente Serializable, mais ne correspond pas à votre logique métier.
Limitez les classes autorisées à être désérialisées (ObjectInputFilter)
À partir de Java 9, utilisez des filtres — ObjectInputFilter — pour restreindre l’ensemble des classes autorisées à être désérialisées. C’est comme un contrôle à l’entrée.
Exemple : mise en place du filtre
import java.io.*;
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.User;com.example.Address;!*"
);
ObjectInputStream in = new ObjectInputStream(inputStream);
in.setObjectInputFilter(filter);
Object obj = in.readObject(); // désormais, seuls User et Address sont désérialisés
Ce filtre n’autorise que les classes User et Address de votre application. Toutes les autres seront bloquées — une exception sera levée. Cela réduit considérablement le risque d’introduction d’un objet malveillant.
Ne désérialisez pas des données provenant de sources non fiables
Règle d’or : si vous n’êtes pas sûr de la source des données — ne désérialisez pas. Préférez des formats qui n’exécutent pas de code lors de l’analyse (par exemple JSON, XML avec des parseurs sécurisés).
Exemple de mauvaise pratique :
// Ne faites jamais ça avec des données venant d'Internet!
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object obj = in.readObject(); // dangereux!
Que faire de mieux ?
- Utiliser des parseurs JSON (par exemple Gson/Jackson) ou des parseurs XML avec validation.
- Si la sérialisation binaire est nécessaire — filtrez les classes via ObjectInputFilter et vérifiez les types (instanceof).
Utilisez des formats alternatifs pour l’échange avec des systèmes externes
Pour les intégrations, utilisez des formats qui n’exécutent pas de code lors de l’analyse : JSON, XML, Protocol Buffers, etc. Cela élimine presque les attaques par désérialisation.
// Utilisez un parseur JSON au lieu de ObjectInputStream
User user = gson.fromJson(jsonString, User.class);
Ne stockez pas les objets sérialisés dans des emplacements publics
Les fichiers contenant des objets sérialisés peuvent inclure des données sensibles. Ne les stockez pas dans des répertoires publics et limitez les droits d’accès au niveau du système de fichiers.
Ne comptez pas sur la sérialisation pour le contrôle d’intégrité
La sérialisation ne garantit ni l’intégrité ni l’authenticité des données. Utilisez des signatures numériques, des sommes de contrôle ou le chiffrement si les modifications sont inacceptables.
2. Pratique : exemple avec ObjectInputFilter et démonstration d’une vulnérabilité
Exemple de filtrage des classes
Supposons que nous ayons une classe User :
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password;
// ...constructeurs, getters, setters...
}
Le filtre n’autorise que User :
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.User;!*"
);
in.setObjectInputFilter(filter);
Désormais, si quelqu’un essaie d’injecter un objet d’une autre classe, la désérialisation se terminera par une erreur.
Démonstration d’une vulnérabilité potentielle
Classe malveillante :
// Imaginons que quelqu’un ait injecté une telle classe
public class Evil implements java.io.Serializable {
static {
System.out.println("Code malveillant exécuté!");
// il peut y avoir n’importe quoi ici...
}
}
Sans filtrage des classes, lors de la désérialisation, un objet Evil peut être créé, et l’initialiseur statique s’exécutera au chargement de la classe — c’est déjà une attaque réelle.
4. Erreurs typiques lors de la sécurisation de la sérialisation
Erreur n° 1 : Désérialisation sans filtrage ni vérification de type. Les développeurs lisent souvent un objet depuis le flux et le castent immédiatement vers le type voulu. Cela ouvre la porte aux attaques. Utilisez ObjectInputFilter et vérifiez le type avec instanceof.
Erreur n° 2 : Stockage de données sensibles sans transient. Si vous oubliez de déclarer les mots de passe/clefs comme transient, ils iront dans le flux et pourront fuiter avec le fichier.
Erreur n° 3 : Absence de serialVersionUID. Sans serialVersionUID explicite, vous risquez des erreurs de compatibilité inattendues et des risques liés à la substitution de classes.
Erreur n° 4 : Utiliser la sérialisation pour l’échange avec des systèmes externes. La sérialisation binaire est pratique à l’intérieur d’une application (par exemple, pour le cache), mais dangereuse pour les échanges externes. Préférez JSON/XML/Proto avec des parseurs sécurisés.
Erreur n° 5 : Ignorer l’intégrité des données. Toute modification des octets d’un fichier sérialisé passera inaperçue. Appliquez des signatures numériques, des sommes de contrôle ou du chiffrement.
GO TO FULL VERSION