JEP 290, Filter Incoming Serialization Data

Contexte

Depuis quelques semaines, je déroule les nouveautés du JDK 17. Cette semaine, nous allons revenir en arrière et nous pencher sur une précédente JEP contenue dans le JDK 9. Elle tente de s’attaquer au problème de la désérialisation. Cela n’est pas une spécificité de la plateforme Java. Mais ce problème n’est pas anodin car la désérialisation non sécurisée fait partie du Top 10 OWASP des failles de sécurité des applications web.

Rappel sur la sérialisation

L’objectif est pouvoir transformer uns instance en flux d’octets. Ce dernier sera stocké dans un fichier, passera par le réseau ou autres. Puis, le flux d’octects sera retransformer en instance.

Sérialisation

Voici le code pour sérialiser une chaine de caractère et une date. Nous utilisons la classe ObjectOutputStream et la méthode writeObject

OutputStream o = ...
ObjectOutputStream s = new ObjectOutputStream(o);

s.writeObject("Today's date");
s.writeObject(new Date());

Désérialisation

La désérialisation est l’opération inverse. Elle permet de transformer le flux d’octets en instance. C’est la classe compagnon ObjectInputStream et la méthode readObject.

InputStream o = ...
ObjectInputStream s = new ObjectInputStream(o);

String str = (String) s.readObject();
Date d = (Date) s.readObject();

avertissement Vous aurez remarqué que la sérialisation et la désérialisation sont réalisées dans le même ordre : String, puis Date

Activation / Autorisation

La classe doit implémenter tout simplement l’interface java.lang.Serializable.

class MaClasse implements Serializable {

}

Et c’est tout !

Personnalisation

Les opérations sont réalisées par défaut par la JVM au niveau de la classe Object.

class Object {

    void readObject(ObjectInputStream ois);
    void writeObject(ObjectOutputStream oos)
}

De ce fait, le développeur peut surcharger les méthodes readObject et writeObject afin de réaliser sa propre implémentation.

avertissement C’est malheureusement par ce biais que les attaques peuvent se produire. Car la JVM va ainsi charger du code. Et ce dernier peut être malveillant.

JEP 290 Filter Incoming Serialization Data

Cette fonctionnalité va permettre l’utilisation de filtre lors de la lecture de flux entrants de la désérialization afin d’améliorer la robustesse et la sécurité.

Tout l’intérêt est que le filtrage est réalisé avant le chargement de la classe. (donc avant le chargement du code potentiellement malveillant).

Les objectifs sont les suivants :

  • Fournir un mécasnime de filtre flexible pour autoriser la désérialisation des classes,

  • Fournir des métriques pour le filtre concernant la taille et la complexité du graphe des instances désérialisés,

  • Fournir un mécanisme compatible avec RMI,

  • Ne pas utiliser de sous-classes ou de modification de la classe ObjectInputStream,

  • Pouvoir être configuré de manière globale avec des propriétés.

Définition de l’API

Le filtre est décrit via l’interface ObjectInputFilter:

interface ObjectInputFilter {
    Status checkInput(FilterInput filterInfo);

    enum Status {
        UNDECIDED,
        ALLOWED,
        REJECTED;
    }

   interface FilterInfo {
         Class<?> serialClass();
         long arrayLength();
         long depth();
         long references();
         long streamBytes();
   }

    public static class Config {
        public static void setSerialFilter(ObjectInputFilter filter);
        public static ObjectInputFilter getSerialFilter(ObjectInputFilter filter) ;
        public static ObjectInputFilter createFilter(String patterns);
    }
}

C’est la méthode checkInput qui va être appeler pour vérifier le flux d’entrée.

avertissement L’interface FiltreInfo correspond bien au deuxième objectif : avoir des métriques pendant la phase de désérialization.

Configuration du filtre de manière globale

Cela se passe par la propriété jdk.serialFilter. Il suffit de positionner la propriété sur la ligne de commande.

java -Djdk.serialFilter="fr.lbenoit.exemple*;java.base/*;!*" ...

avertissement Dans cet exemple, les classes du package fr.lbenoit.exemple et les classes du module java.base sont autorisés. Toutes les autres classes ne le sont pas.

La propriété peut être positionnée dans le fichier conf/security/java.properties

Configuration du filtre par programmation

Pour cela, il suffit d’invoquer la méthode statique createFilter :

 var filtre = ObjectInputFilter.Config.createFilter("fr.lbenoit.exemple.*;java.base/*;!*")

Le filtre peut être positionnée de deux manières :

  • de manière globale

 ObjectInputFilter.Config.setSerialFilter(filtre);
  • par flux

Chaque flux peut avoir son filtre dédié. En effet, la classe ObjectInputStream contient des méthodes pour manipuler le filtre.

public class ObjectInputStream ... {
    public final void setObjectInputFilter(ObjectInputFilter filter);
    public final ObjectInputFilter getObjectInputFilter(ObjectInputFilter filter);
}

Donc, pour positionner le filtre, il suffit d’appeler la méthode setObjectInputFilter sur le flux :

ObjectInputStream ois = new ...
ois.setObjectInputFilter(filtre);

Motifs possibles pour la configuration

Comme nous pouvons le voir dans les exemples, nous ne sommes pas obliger de créer une classe implémentant l’interface ObjectInputFilter. Nous pouvons parfaitement utiliser l’implémentation par défaut qui utilise des motifs pour construire les filtres.

Les différents motifs sont :

  • Si le motif correpond au nom de la classe, seule cette classe est autorisé : fr.lbenoit.exemple.Moto (Seule la classe fr.lbenoit.exemple.Moto est autorisée)

  • Si le motif est précédé par un point d’intérrogation : !fr.lbenoit.exemple.Voiture (Seule la classe fr.lbenoit.Voiture n’est pas autorisée, tous les autres le sont)

  • Si le motif se termine par .*, toutes les classes du packages sont autorisées : fr.lbenoit.exemple.* (fr.lbenoit.exemple.Moto et fr.lbenoit.exemple.Voiture sont autorisées)

  • Si le motif se termine par .**, toutes les classes et sous-classes du packages sont autorisées : fr.lbenoit.* * (fr.lbenoit.Moto, fr.lbenoit.exemple.Voiture et fr.lbenoit.composants.Moteur sont autorisées)

  • Si le motif contient le caractère /, cela concerne le module

  • Sinon la décision est UNDECIDED