Skip to main content

Nouvelles fonctionnalités de Java 17 pour renforcer la sécurité et la sérialisation

Écrit par
Blog Feature Java deserialize

21 octobre 2021

0 minutes de lecture

En décembre 2020, j’ai écrit l’article Sérialisation et désérialisation en Java : comprendre la vulnérabilité de désérialisation Java sur les problèmes liés à l’implémentation de la sérialisation personnalisée de Java. Le framework de sérialisation est si profondément intégré à Java qu’il est important de savoir à quel point certaines implémentations peuvent être dangereuses. La désérialisation non sécurisée peut entraîner l’exécution de code arbitraire si une chaîne de gadgets est créée à partir des classes de votre classpath.

Java 17, la nouvelle version LTS, vient de sortir. Mais quel est l’impact des nouvelles fonctionnalités sur ce problème, et peuvent-elles mieux prévenir les vulnérabilités de désérialisation ?

Dans cet article, je vais examiner trois fonctionnalités clés de Java 17 :

  1. Records

  2. Améliorations de Java Flight Recorder (JFR)

  3. JEP 415 (Java Enhancement Proposal) : filtres de désérialisation contextuels.

1. Records

Les Records ont été introduits dans Java 14 en tant que fonctionnalité en préversion, puis intégrés définitivement à Java 16. Toutefois, comme de nombreux développeurs préfèrent passer uniquement aux versions LTS, il est pertinent d’aborder les Records dans le contexte de la sérialisation maintenant que Java 17 est officiellement disponible.

Contrairement aux POJO classiques, lors de la désérialisation d’un Record, le constructeur est utilisé pour recréer l’objet. Ce n’est pas le cas des objets Java ordinaires, pour lesquels le framework dépend fortement de la réflexion. Cela signifie que la logique déclenchée par le constructeur ne s’exécute pas lors de la désérialisation d’un objet Java ordinaire. Consultez mon précédent article pour en savoir plus. Avec les Records, il n’y a pas de mécanisme caché lors de la recréation de l’objet pendant la désérialisation. Si, pour une raison quelconque, vous placez une logique de validation dans le constructeur d’un Record, nous savons désormais que cette logique sera appliquée,

Cela dit, on peut se demander s’il faut vraiment placer de la logique dans un record. Une mauvaise utilisation peut créer des gadgets susceptibles de participer à une chaîne de gadgets de désérialisation. Plus important encore, nous utilisons toujours la fonction readObject() pour désérialiser. Cela signifie que les POJO classiques restent vulnérables aux chaînes de gadgets, que les Records existent ou non.

2. Filtres de désérialisation en Java

Pour remédier aux vulnérabilités de désérialisation en Java, vous pouvez définir des filtres de sérialisation. Cette fonctionnalité a été introduite dans Java 9 avec l’implémentation de JEP 290. Vous pouvez définir des limites pour la taille des tableaux, la profondeur du graphe, le nombre total de références et la taille du flux. Vous pouvez également créer des listes d’autorisation et de blocage basées sur un modèle afin de limiter les classes désérialisées.

Vous pouvez définir un tel filtre de manière globale pour la JVM ou individuellement pour chaque flux. Pour le filtre global, vous pouvez définir un argument de la JVM ou le configurer dans le code. Ci-dessous, j’ai créé un filtre qui autorise toutes les classes de mypackage et bloque toutes les autres.

Argument de la JVM :

-Djdk.serialFilter=nl.brianvermeer.example.*;!*

Code :

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("nl.brianvermeer.example.*;!*");
ObjectInputFilter.Config.setSerialFilter(filter);

Vous pouvez également définir un filtre pour un flux spécifique, comme ci-dessous.

ObjectInputStream in = new ObjectInputStream(fileIn);
ObjectInputFilter filesOnlyFilter = ObjectInputFilter.Config.createFilter("nl.brianvermeer.example2.Object;!*");
in.setObjectInputFilter(filesOnlyFilter);

Jusqu’à Java 17, lorsqu’on définissait un filtre pour un flux spécifique, celui-ci remplaçait le filtre global pour ce flux. Les deux filtres n’étaient pas combinés. Cette approche manque de flexibilité. De plus, votre filtre global risque de ne pas fonctionner si une bibliothèque que vous utilisez effectue la désérialisation à votre place.

Filtres de désérialisation contextuels dans Java 17

Java 17 a amélioré le filtre de désérialisation grâce à l’implémentation de JEP 415.

L’une des principales nouveautés consiste à définir unSerialFilterFactory pour ObjectInputFilter.Config. Cette fabrique doit être un BinaryOperator et définit le comportement à adopter lorsqu’un filtre spécifique est ajouté à un flux donné.

Dans l’exemple ci-dessous, je définis pour Config une fabrique très simple qui utilise la méthode de fusion par défaut pour combiner le filtre existant avec le nouveau. Cet outil me permet de décider si les filtres doivent être fusionnés et comment. Le problème évoqué plus haut concernant la gestion du filtre global par vos bibliothèques est ainsi résolu.

ObjectInputFilter.Config.setSerialFilterFactory((f1, f2) -> ObjectInputFilter.merge(f2,f1));

En plus de la Filter Factory, Java 17 propose des méthodes pratiques pour créer facilement des filtres. À mon avis, les fonctions comme allowFilter() et rejectFilter() de ObjectInputFilter offrent une approche plus déclarative et plus lisible de la création de filtres.

Dans l’exemple de code Java 17 ci-dessous, j’utilise ces nouvelles fonctionnalités. Dans la méthode de désérialisation de cet exemple, je rejette explicitement la classe Gadget. La classe Gadget et le record TwoValue appartiennent au même package. Le filtre rejette désormais Gadget et autorise toutes les autres classes de ce package.

public static void main(String[] args) throws IOException, ClassNotFoundException {
   var filename = "file.ser";
   var value = new TwoValue("one", "two");
   //var value = new Gadget(new Command("ls -l")); //This will not be deserialized

   var filter1 = ObjectInputFilter.allowFilter(cl -> cl.getPackageName().contentEquals("nl.brianvermeer.example.serialize.records"), ObjectInputFilter.Status.REJECTED);
   ObjectInputFilter.Config.setSerialFilter(filter1);
   ObjectInputFilter.Config.setSerialFilterFactory((f1, f2) -> ObjectInputFilter.merge(f2,f1));

   serialize(value, filename);
   deserialize(filename);
}

public static void serialize(Object value, String filename) throws IOException {
   System.out.println("---serialize");
   FileOutputStream fileOut = new FileOutputStream(filename);
   ObjectOutputStream out = new ObjectOutputStream(fileOut);
   out.writeObject(value);
   out.close();
   fileOut.close();
}

public static void deserialize(String filename) throws IOException, ClassNotFoundException {
   System.out.println("---deserialize");
   FileInputStream fileIn = new FileInputStream(filename);
   ObjectInputStream in = new ObjectInputStream(fileIn);
   ObjectInputFilter intFilter = ObjectInputFilter.rejectFilter(cl -> cl.equals(Gadget.class), ObjectInputFilter.Status.UNDECIDED);
   in.setObjectInputFilter(intFilter);
   TwoValue tv = (TwoValue) in.readObject();
   System.out.println(tv);

3. Événements de désérialisation de Java Flight Recorder

Java 17 apporte également une belle amélioration à Java Flight Recorder (JFR) pour vous aider à lutter contre les exploits de désérialisation. Cette nouvelle version de Java prend désormais en charge un événement spécifique pour surveiller la désérialisation. Un événement est créé pour chaque objet d’un flux et enregistre toutes sortes d’informations utiles : le type réel, la présence d’un filtre, le filtrage ou non de l’objet, la profondeur de l’objet, le nombre de références, etc.

Ces informations permettent de repérer la présence d’une désérialisation dans votre application et de savoir ce qui est réellement désérialisé pendant l’exécution de votre processus. Vous devez toutefois veiller à activer cet événement. Il n’est pas enregistré par défaut : vous devez donc appliquer une configuration spécifique, comme celle-ci.

<?xml version="1.0" encoding="UTF-8"?>
<configuration version="2.0" description="test">
   <event name="jdk.Deserialization">
      <setting name="enabled">true</setting>
      <setting name="stackTrace">false</setting>
   </event>
</configuration>

Reprenons l’exemple de code précédent, mais en désérialisant cette fois la classe Gadget. Voici le résultat obtenu en exécutant le code dans IntelliJ IDEA avec Java Flight Recorder et la configuration personnalisée.

Vue Événements du profileur Java montrant un événement de désérialisation rejeté et ses propriétés, notamment le type, les octets lus et l’état du filtre.

Vous voyez que l’événement indique le type réel de l’objet pendant la désérialisation et que le filtre le rejette. Pour découvrir en détail comment utiliser cet événement de désérialisation spécifique de JFR, consultez l’article Surveiller la désérialisation pour renforcer la sécurité des applications de Chris Hegarty.

Passez à Java 17 pour bénéficier d’outils plus efficaces contre les exploits liés à la désérialisation non sécurisée

La version LTS de Java 17 apporte des améliorations significatives pour prévenir la désérialisation malveillante dans vos applications Java. Il est donc essentiel de passer à une version plus récente, comme la version 17, pour adopter ces pratiques. Cela dit, à mon avis, mieux vaut éviter complètement la sérialisation personnalisée de Java. Toutefois, si vous devez l’utiliser ou si vous dépendez d’une bibliothèque qui s’en sert, vous savez désormais comment vous protéger.

Veillez également à ne pas importer de bibliothèques contenant des chaînes de gadgets de désérialisation connues ou présentant d’autres problèmes de sécurité liés à la désérialisation.

Lancez-vous dans les challenges Capture The Flag

Apprenez à résoudre des challenges Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.

Lire la suite

Blog

Les modèles de pointe ont trouvé les vulnérabilités. Seul l’attaquant a trouvé les chaînes d’exploitation.

L’analyse statique a détecté les failles, mais seuls des tests d’attaque en conditions réelles ont prouvé comment elles pouvaient être enchaînées pour provoquer des compromissions. Comparaison d’Evo COS, de Claude Security et de Claude Code Security.

feature insights context
Blog

Les attaques autonomes sont déjà là. La défense doit suivre leur rythme.

Les attaquants autonomes réduisent la fenêtre de défense. Découvrez comment la découverte, la correction, la validation et la prévention continues peuvent aider les équipes de sécurité à suivre le rythme.

Blog

Pourquoi les agents de codage IA créent-ils sans cesse des failles de contrôle d’accès ?

Les agents de codage IA peuvent générer une logique d’autorisation qui compile et passe la revue, tout en exposant les données d’un tenant à un autre. Découvrez pourquoi les failles de contrôle d’accès sont difficiles à détecter et comment les prévenir.