JEP 415, Context-Specific Deserialization Filters

Contexte

Retour sur les nouvelles fonctionalités du JDK 17, le filtre pour se protéger de la désérialisation a été améliorée. Nous avons abordés ce sujet dans le billet précédent

Pour rappel, depuis le JDK 9, nous avons l’interface ObjectInputFilter :

interface ObjectInputFilter {
    Status checkInput(FilterInput filterInfo);

    enum Status {
        UNDECIDED,
        ALLOWED,
        REJECTED;
    }

    ...

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

JEP 415 Context Specific Deserialization Filters

L’objectif est d’améliorer la granularité au niveau du filtre. En effet, avant le JDK 17, nous avons un paramétrage :

  • soit global au niveau de la JVM

  • soit par flux (c’est à dire pour chaque intance de ObjectInputStream)

Comment cela fonctionne

Le principe est la mise en place d’un système de fabrique de filtre. L’objectif est de pouvoir déterminer le filtre à utiliser selon le contexte.

La fabrique doit implémenter l’interface BinaryOperator<ObjectInputFilter>.

public class FiltreSelonContexte implements BinaryOperator<ObjectInputFilter> {

    public ObjectInputFilter apply(ObjectInputFilter curr, ObjectInputFilter next) {
        ....
    }
}

Naturellement, la nouvelle mécanique doit rester compatible avec l’existant.

Ligne de commande

La première possibilité est d’utiliser la ligne de commande. Pour cela, il existe une nouvelle propriété jdk.serialFilterFactory où nous indiquons la classe concernée (qui doit être accessible par le chargeur de classes)

API

Côté API, nous avons des méthodes qui permettent de positionner la fabrique au niveau de la classe ObjectInputFilter.Config.

interface ObjectInputFilter {
    Status checkInput(FilterInput filterInfo);
    ...

    public static class Config {
        ...
        public static BinaryOperator<ObjectInputFilter> getSerialFilterFactory();
        public static void setSerialFilterFactory(BinaryOperator<ObjectInputFilter> filterFactory);
    }
}

Les filtres peuvent s’additionner pour s’adapter au contexte. Pour cela, il existe la méthode ObjectInputFilter.merge(). Voici sa signature :

interface ObjectInputFilter {
    ObjectInputFilter ObjectInputFilter.merge(ObjectInputFilter premier, ObjectInputFilter second);
}

Ainsi, nous avons la séquence suivante :

  • Appel du filtre premier et obtient le status

  • Retourne REJECTED si le status est REJECTED

  • Appel du filtre second et obtient le status

  • Retourne REJECTED si le status du second filtre est REJECTED

  • Retourne ALLOWED si l’un des deux status (le premier ou le second) est ALLOWED

  • Sinon, retourne UNDECIDED

Exemple de mise en oeuvre

L’exemple inclus dans le JEP et la Javadoc consiste à obtenir un contexte par thread (donc un filtre par thread). Pour cela, nous commençons par la définition de la classe suivante :

public static final class FilterInThread implements BinaryOperator<ObjectInputFilter> {

     private final ThreadLocal<ObjectInputFilter> filterThreadLocal = new ThreadLocal<>();

     // Constructeur de la farbique du filtre de la désérialisation
     public FilterInThread() {}

     // Retourne un filtre composite
     public ObjectInputFilter apply(ObjectInputFilter curr, ObjectInputFilter next) {
         ...
     }
     // Applique le filtre au niveau du Thread
     // et appelle la méthode `Runnable.run()`
     public void doWithSerialFilter(ObjectInputFilter filter, Runnable runnable) {
         var prevFilter = filterThreadLocal.get();
         try {
             filterThreadLocal.set(filter);
             runnable.run();
         } finally {
             filterThreadLocal.set(prevFilter);
         }
     }
}

La méthode doWithSerialFilter va ainsi :

  • positionner le filtre souhaité (c’est à dire passer en paramètre)

  • appeler la méthode du Runnable.run().

  • puis, repositionner le filtre initial afin de rétablir la configuration par défaut.

Ainsi, c’est le filtre souhaité qui sera utilisé durant toute l’exécution de la méthode (et notamment du Runnable.run()).

L’utilisation est la suivante :

// Creation de la fabrique de filtre et le positionner de manière globale
var filterInThread = new FilterInThread();
ObjectInputFilter.Config.setSerialFilterFactory(filterInThread);

// Création d'un filtre qui autorise fr.lbenoit.exemple.*, les classes du module
// `java.base` et rejete toutes les autres
var filter = ObjectInputFilter.Config.createFilter("example.*;java.base/*;!*");
filterInThread.doWithSerialFilter(filter, () -> {
        byte[] bytes = ...;
        var o = deserializeObject(bytes);
});