93 votes

Sandbox contre le code malveillant dans une application Java

Dans un environnement de serveur de simulation où les utilisateurs sont autorisés à soumettre leur propre code pour qu'il soit exécuté par le serveur, il serait clairement avantageux que tout code soumis par l'utilisateur soit exécuté dans une sandbox, un peu comme les applets le sont dans un navigateur. Je voulais être en mesure de tirer parti de la JVM elle-même, plutôt que d'ajouter une autre couche de VM pour isoler ces composants soumis.

Ce type de limitation semble être possible en utilisant le modèle existant de bac à sable Java, mais existe-t-il un moyen dynamique de l'activer uniquement pour les parties soumises par l'utilisateur d'une application en cours d'exécution ?

112voto

waqas Points 2940
  1. Exécuter le code non fiable dans son propre thread. Cela permet par exemple d'éviter les problèmes de boucles infinies et autres, et facilite les étapes suivantes. Demandez au thread principal d'attendre que le thread se termine, et si cela prend trop de temps, tuez-le avec Thread.stop. Thread.stop est déprécié, mais puisque le code non fiable ne devrait avoir accès à aucune ressource, il serait sûr de le tuer.

  2. Définir un SecurityManager sur ce Thread. Créer une sous-classe de SecurityManager qui surcharge checkPermission(Permission perm) pour simplement lancer un SecurityException pour toutes les permissions, à l'exception de quelques-unes. Une liste des méthodes et des autorisations qu'elles requièrent est disponible ici : Les permissions dans l'environnement Java TM 6 SDK .

  3. Utilisez un ClassLoader personnalisé pour charger le code non approuvé. Votre chargeur de classes sera appelé pour toutes les classes que le code non approuvé utilise, de sorte que vous pouvez faire des choses comme désactiver l'accès à des classes JDK individuelles. La chose à faire est d'avoir une liste blanche des classes JDK autorisées.

  4. Vous pourriez vouloir exécuter le code non fiable dans une JVM séparée. Bien que les étapes précédentes aient rendu le code sûr, il y a une chose ennuyeuse que le code isolé peut encore faire : allouer autant de mémoire que possible, ce qui fait augmenter l'empreinte visible de l'application principale.

JSR 121 : Spécification de l'API d'isolation des applications a été conçu pour résoudre ce problème, mais malheureusement il n'y a pas encore d'implémentation.

Il s'agit d'un sujet assez détaillé, et j'écris tout cela de but en blanc.

Quoi qu'il en soit, un code (pseudo) imparfait, à utiliser à ses propres risques et probablement bogué :

ClassLoader

class MyClassLoader extends ClassLoader {
  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
    if (name is white-listed JDK class) return super.loadClass(name);
    return findClass(name);
  }
  @Override
  public Class findClass(String name) {
    byte[] b = loadClassData(name);
    return defineClass(name, b, 0, b.length);
  }
  private byte[] loadClassData(String name) {
    // load the untrusted class data here
  }
}

SecurityManager

class MySecurityManager extends SecurityManager {
  private Object secret;
  public MySecurityManager(Object pass) { secret = pass; }
  private void disable(Object pass) {
    if (pass == secret) secret = null;
  }
  // ... override checkXXX method(s) here.
  // Always allow them to succeed when secret==null
}

Fil conducteur

class MyIsolatedThread extends Thread {
  private Object pass = new Object();
  private MyClassLoader loader = new MyClassLoader();
  private MySecurityManager sm = new MySecurityManager(pass);
  public void run() {
    SecurityManager old = System.getSecurityManager();
    System.setSecurityManager(sm);
    runUntrustedCode();
    sm.disable(pass);
    System.setSecurityManager(old);
  }
  private void runUntrustedCode() {
    try {
      // run the custom class's main method for example:
      loader.loadClass("customclassname")
        .getMethod("main", String[].class)
        .invoke(null, new Object[]{...});
    } catch (Throwable t) {}
  }
}

5 votes

Ce code pourrait avoir besoin d'être retravaillé. Vous ne pouvez pas vraiment vous prémunir contre la disponibilité de la JVM. Soyez prêt à tuer le processus (probablement automatiquement). Le code se retrouve sur d'autres threads - par exemple le thread du finalisateur. Thread.stop causera des problèmes dans le code de la bibliothèque Java. De même, le code de la bibliothèque Java nécessitera des autorisations. Il est préférable d'autoriser le SecurityManager à utiliser java.security.AccessController . Le chargeur de classes devrait probablement aussi permettre l'accès aux propres classes du code utilisateur.

0 votes

@Tom : Il est possible d'implémenter le gestionnaire de sécurité de manière à ce qu'il ignore le code de la bibliothèque Java.

4 votes

Étant donné la complexité du sujet, n'existe-t-il pas des solutions pour gérer les "plugins" Java de manière sûre ?

18voto

shsmurfy Points 732

Il est évident qu'un tel schéma soulève toutes sortes de problèmes de sécurité. Java dispose d'un cadre de sécurité rigoureux, mais il n'est pas trivial. Il ne faut pas négliger la possibilité de le faire échouer et de laisser un utilisateur non privilégié accéder à des composants vitaux du système.

Cet avertissement mis à part, si vous prenez l'entrée de l'utilisateur sous forme de code source, la première chose que vous devez faire est de le compiler en bytecode Java. AFIAK, cela ne peut pas être fait nativement, donc vous devrez faire un appel système à javac, et compiler le code source en bytecode sur le disque. Voici un tutoriel qui peut servir de point de départ à cette démarche. Modifier : comme je l'ai appris dans les commentaires, vous pouvez en fait compiler du code Java à partir de la source de manière native en utilisant javax.tools.JavaCompiler

Une fois que vous disposez du bytecode de la JVM, vous pouvez le charger dans la JVM à l'aide d'une commande L'interface de ClassLoader defineClass fonction. Pour définir un contexte de sécurité pour cette classe chargée, vous devrez spécifier une valeur de ProtectionDomaine . Le constructeur minimal d'un ProtectionDomaine nécessite à la fois un CodeSource et un Collection de permissions . La PermissionCollection est l'objet le plus utile ici - vous pouvez l'utiliser pour spécifier les permissions exactes dont dispose la classe chargée. Ces permissions doivent être appliquées en dernier ressort par l'outil de gestion de la JVM Contrôleur d'accès .

Il y a beaucoup de points d'erreur possibles ici, et vous devez être extrêmement attentif à tout comprendre avant de mettre en œuvre quoi que ce soit.

2 votes

La compilation Java est assez facile en utilisant l'API javax.tools du JDK 6.

11voto

Lii Points 690

Java-Sandbox est une bibliothèque permettant d'exécuter du code Java avec un ensemble limité de permissions.

Il peut être utilisé pour n'autoriser l'accès qu'à un ensemble de classes et de ressources figurant sur une liste blanche. Il ne semble pas être capable de restreindre l'accès à des méthodes individuelles. Il utilise un système avec un chargeur de classe personnalisé et un gestionnaire de sécurité personnalisés pour y parvenir.

Je ne l'ai pas utilisé mais il semble bien conçu et raisonnablement bien documenté.

@waqas a donné une réponse très intéressante expliquant comment il est possible de l'implémenter soi-même. Mais il est beaucoup plus sûr de laisser ce genre de code complexe et critique pour la sécurité à des experts.

Note : Le projet n'a pas été mis à jour depuis 2013 et les créateurs le décrivent comme "expérimental". Sa page d'accueil a disparu mais l'entrée Source Forge demeure.

Exemple de code adapté du site web du projet :

SandboxService sandboxService = SandboxServiceImpl.getInstance();

// Configure context 
SandboxContext context = new SandboxContext();
context.addClassForApplicationLoader(getClass().getName());
context.addClassPermission(AccessType.PERMIT, "java.lang.System");

// Whithout this line we get a SandboxException when touching System.out
context.addClassPermission(AccessType.PERMIT, "java.io.PrintStream");

String someValue = "Input value";

class TestEnvironment implements SandboxedEnvironment<String> {
    @Override
    public String execute() throws Exception {
        // This is untrusted code
        System.out.println(someValue);
        return "Output value";
    }
};

// Run code in sandbox. Pass arguments to generated constructor in TestEnvironment.
SandboxedCallResult<String> result = sandboxService.runSandboxed(TestEnvironment.class, 
    context, this, someValue);

System.out.println(result.get());

4voto

Arno Unkrig Points 11

Voici une solution sûre pour ce problème :

https://svn.code.sf.net/p/loggifier/code/trunk/de.unkrig.commons.lang/src/de/unkrig/commons/lang/security/Sandbox.java

package de.unkrig.commons.lang.security;

import java.security.AccessControlContext;
import java.security.Permission;
import java.security.Permissions;
import java.security.ProtectionDomain;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

import de.unkrig.commons.nullanalysis.Nullable;

/**
 * This class establishes a security manager that confines the permissions for code executed through specific classes,
 * which may be specified by class, class name and/or class loader.
 * <p>
 * To 'execute through a class' means that the execution stack includes the class. E.g., if a method of class {@code A}
 * invokes a method of class {@code B}, which then invokes a method of class {@code C}, and all three classes were
 * previously {@link #confine(Class, Permissions) confined}, then for all actions that are executed by class {@code C}
 * the <i>intersection</i> of the three {@link Permissions} apply.
 * <p>
 * Once the permissions for a class, class name or class loader are confined, they cannot be changed; this prevents any
 * attempts (e.g. of the confined class itself) to release the confinement.
 * <p>
 * Code example:
 * <pre>
 *  Runnable unprivileged = new Runnable() {
 *      public void run() {
 *          System.getProperty("user.dir");
 *      }
 *  };
 *
 *  // Run without confinement.
 *  unprivileged.run(); // Works fine.
 *
 *  // Set the most strict permissions.
 *  Sandbox.confine(unprivileged.getClass(), new Permissions());
 *  unprivileged.run(); // Throws a SecurityException.
 *
 *  // Attempt to change the permissions.
 *  {
 *      Permissions permissions = new Permissions();
 *      permissions.add(new AllPermission());
 *      Sandbox.confine(unprivileged.getClass(), permissions); // Throws a SecurityException.
 *  }
 *  unprivileged.run();
 * </pre>
 */
public final
class Sandbox {

    private Sandbox() {}

    private static final Map<Class<?>, AccessControlContext>
    CHECKED_CLASSES = Collections.synchronizedMap(new WeakHashMap<Class<?>, AccessControlContext>());

    private static final Map<String, AccessControlContext>
    CHECKED_CLASS_NAMES = Collections.synchronizedMap(new HashMap<String, AccessControlContext>());

    private static final Map<ClassLoader, AccessControlContext>
    CHECKED_CLASS_LOADERS = Collections.synchronizedMap(new WeakHashMap<ClassLoader, AccessControlContext>());

    static {

        // Install our custom security manager.
        if (System.getSecurityManager() != null) {
            throw new ExceptionInInitializerError("There's already a security manager set");
        }
        System.setSecurityManager(new SecurityManager() {

            @Override public void
            checkPermission(@Nullable Permission perm) {
                assert perm != null;

                for (Class<?> clasS : this.getClassContext()) {

                    // Check if an ACC was set for the class.
                    {
                        AccessControlContext acc = Sandbox.CHECKED_CLASSES.get(clasS);
                        if (acc != null) acc.checkPermission(perm);
                    }

                    // Check if an ACC was set for the class name.
                    {
                        AccessControlContext acc = Sandbox.CHECKED_CLASS_NAMES.get(clasS.getName());
                        if (acc != null) acc.checkPermission(perm);
                    }

                    // Check if an ACC was set for the class loader.
                    {
                        AccessControlContext acc = Sandbox.CHECKED_CLASS_LOADERS.get(clasS.getClassLoader());
                        if (acc != null) acc.checkPermission(perm);
                    }
                }
            }
        });
    }

    // --------------------------

    /**
     * All future actions that are executed through the given {@code clasS} will be checked against the given {@code
     * accessControlContext}.
     *
     * @throws SecurityException Permissions are already confined for the {@code clasS}
     */
    public static void
    confine(Class<?> clasS, AccessControlContext accessControlContext) {

        if (Sandbox.CHECKED_CLASSES.containsKey(clasS)) {
            throw new SecurityException("Attempt to change the access control context for '" + clasS + "'");
        }

        Sandbox.CHECKED_CLASSES.put(clasS, accessControlContext);
    }

    /**
     * All future actions that are executed through the given {@code clasS} will be checked against the given {@code
     * protectionDomain}.
     *
     * @throws SecurityException Permissions are already confined for the {@code clasS}
     */
    public static void
    confine(Class<?> clasS, ProtectionDomain protectionDomain) {
        Sandbox.confine(
            clasS,
            new AccessControlContext(new ProtectionDomain[] { protectionDomain })
        );
    }

    /**
     * All future actions that are executed through the given {@code clasS} will be checked against the given {@code
     * permissions}.
     *
     * @throws SecurityException Permissions are already confined for the {@code clasS}
     */
    public static void
    confine(Class<?> clasS, Permissions permissions) {
        Sandbox.confine(clasS, new ProtectionDomain(null, permissions));
    }

    // Code for 'CHECKED_CLASS_NAMES' and 'CHECKED_CLASS_LOADERS' omitted here.

}

Veuillez commenter !

CU

Arno

0voto

Kieron Points 5096

Vous aurez probablement besoin d'utiliser un SecurityManger et/ou Contrôleur d'accès . Pour de nombreux détails, voir Architecture de sécurité Java y autres documents relatifs à la sécurité du soleil.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X