Remplacer le SecurityManager de Java par un agent grâce à Byte Buddy

Remplacer le SecurityManager de Java par un agent grâce à Byte Buddy

Le SecurityManager de Java était un composant essentiel du modèle de sécurité de Java. Il permettait de mettre en places des règles de sécurités en implémentant des méthodes check*() telle que checkExec(String cmd) pour l’exécution d’une commande sur la machine hôte ou checkRead(String file) pour l’accès en lecture à un fichier. Plus de détail dans sa JavaDoc.

Il a été déprécié en Java 17 (JEP 411) et supprimé en Java 24 (JEP 486).

Au sein de Kestra, la plateforme d’orchestration universelle, nous l’utilisions pour :

  • Restreindre l’accès au système de fichier de la machine hôte (liste allow/deny).
  • Empêcher l’exécution d’une commande sur la machine hôte.
  • Empêcher le démarrage d’un thread.
  • Empêcher d’arrêter la JVM.

Kestra a un système de plugin extensible, chaque plugin pouvant exécuter de code non fiable, offrir de la sécurité avancée est primordiale dans les contextes d’exécution critiques.

Suite à la suppression du SecurityManager, nous l’avons remplacé par un agent Byte Buddy : les mêmes règles, implémentées différemment.

Ici, je vais uniquement prendre en exemple la restriction de l’accès aux fichiers, mais la même technique a été utilisée pour les autres règles de sécurité.

Byte Buddy est une librairie open source qui permet de créer ou modifier des classes à l’exécution de votre application. Elle offre une API haut-niveau et déclarative qui ne nécessite pas de connaissance du bytecode java. Il est utilisé par beaucoup de framework populaire tel qu’Hibernate, Mockito, OpenTelemetry, …

Voici un exemple, by-the-book d’un agent Byte Buddy pour intercepter l’accès aux fichiers via un RandomAccessFile.

static void main() {
    Instrumentation instrumentation = ByteBuddyAgent.install(); // <1>

    new AgentBuilder.Default() // <2>
        .with(new AgentBuilder.Listener.WithErrorsOnly(AgentBuilder.Listener.StreamWriting.toSystemError())) // <3>
        .type(ElementMatchers.is(RandomAccessFile.class)) // <4>
        .transform((builder, _, _, _, _) ->
                builder.method(
                        ElementMatchers.isMethod()
                                                .and(ElementMatchers.named("open")) // <5>
                            .and(ElementMatchers.takesArguments(String.class, int.class))
                            .and(ElementMatchers.returns(void.class))
                            .and(ElementMatchers.isPrivate())
                    )
                    .intercept(MethodDelegation.to(FileInterceptor.class)) // <6>
            )
            .installOn(instrumentation); // <7>
}

public static class FileInterceptor { // <8>
    public static void open(String path, int mode) {
        throw new RuntimeException("You shall not pass");
    }
}
  1. Installe l’agent Byte Buddy, c’est une sorte de meta agent (mes mots) qui permet d’installer facilement votre propre agent. Retourne une instance d’Instrumentation.
  2. Pour créer notre agent, on va utiliser l’AgentBuilder de Byte Buddy qui offre une API fluide permettant de chaîner les transformations.
  3. Log les erreurs dans stderr.
  4. Cible la classe RandomAccessFile. ElementMatchers est une des classes centrale de Byte Buddy qui permet de cibler des éléments de votre code applicatif tels qu’une classe, une méthode, un champ, …
  5. Cible la méthode nommée open qui prend en argument une String et un int, ne retourne rien, et est privée. C’est la méthode de la classe RandomAccessFile qui est appelée pour chaque ouverture de fichier.
  6. Délègue l’implémentation de cette méthode à la classe FileInterceptor : c’est une des formes de transformation, on remplace la méthode transformée par une autre.
  7. Installe l’agent dans l’application.
  8. Une classe statique contenant une méthode statique dont la signature correspond à la méthode interceptée : cette méthode sera appellée à la place de la méthode correspondante de la classe RandomeAccessFile.

Cet exemple by-the-book ne marche hélas pas pour les classes internes du JDK chargée au démarrage de l’application, donc, via le Bootstrap class loader. Pour celles-ci, il faut configurer spécifiquement l’agent, et utiliser un Advice et non une délégation de méthode.

Les Advice Byte Buddy, bien qu’absent de la documentation, sont plus puissants et flexibles d’utilisation qu’une délégation de méthode ; ils reprennent les principes de la programmation orientée object (AOP) en permettant de définir une méthode appelée avant ou après l’appel de la méthode cible.

static void main() {
    Instrumentation instrumentation = ByteBuddyAgent.install();

    new AgentBuilder.Default()
            .ignore(ElementMatchers.none()) // <1>
            .with(AgentBuilder.RedefinitionStrategy.REDEFINITION) // <2>
            .with(new AgentBuilder.Listener.WithErrorsOnly(AgentBuilder.Listener.StreamWriting.toSystemError()))
            .type(ElementMatchers.is(RandomAccessFile.class))
            .transform((builder, _, _, _, _) ->
                builder.visit( // <3>
                        Advice.to(FileInterceptor.class).on(
                        ElementMatchers.isMethod()
                                                .and(ElementMatchers.named("open"))
                            .and(ElementMatchers.takesArguments(String.class, int.class))
                            .and(ElementMatchers.returns(void.class))
                            .and(ElementMatchers.isPrivate())
                    ))
            )
            .installOn(instrumentation);
}

public static class FileInterceptor { // <4>
    @Advice.OnMethodEnter
    public static void openEnter(String path, int mode) {
        IO.println("Try to access file: " + path);
    }

    @Advice.OnMethodExit
    public static void openExit(String path, int mode) {
        throw new RuntimeException("You shall not pass");
    }
}
  1. Par défaut, les classes du JDK sont ignorées ; il faut configurer l’agent pour n’ignorer aucune classes.
  2. Par défaut, les classes déjà chargées ne sont pas re-transformées ; il faut configurer l’agent pour retransformer les classes déjà chargées via redéfinition.
  3. Au lieu de transformer une méthode, on visite la méthode avec un advice.
  4. Un advice est défini via des méthodes annotées par @Advice.OnMethodEnter, exécutée avant l’entrée dans la méthode ciblées, et/ou @Advice.OnMethodExit, exécutée après la sortie de la méthode.

Voilà, maintenant, vous savez tous les secrets de transformations de classes internes de la JVM !

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.