web-dev-qa-db-fra.com

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 à exécuter par le serveur, il serait clairement avantageux que tout code soumis par l'utilisateur soit exécuté à côté d'un bac à sable, un peu comme les applets sont dans un navigateur. Je voulais pouvoir utiliser la JVM elle-même, plutôt que d'ajouter une autre couche VM pour isoler ces composants soumis.

Ce type de limitation semble être possible en utilisant le modèle Java sandbox existant, 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?

89
Alan Krueger
  1. Exécutez le code non approuvé dans son propre thread. Cela évite par exemple les problèmes de boucles infinies et autres, et facilite les futures étapes. Demandez au thread principal d'attendre la fin du thread, et si cela prend trop de temps, tuez-le avec Thread.stop. Thread.stop est obsolète, mais comme le code non approuvé ne devrait avoir accès à aucune ressource, il serait sûr de le tuer.

  2. Définissez un SecurityManager sur ce fil. Créez une sous-classe de SecurityManager qui remplace checkPermission (Permission perm) pour simplement lancer SecurityException pour toutes les autorisations sauf quelques-unes. Il y a une liste de méthodes et les autorisations dont ils ont besoin ici: Autorisations dans JavaTM 6 SDK .

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

  4. Vous souhaiterez peut-être exécuter le code non approuvé dans une machine virtuelle Java distincte. Alors que les étapes précédentes rendraient le code sûr, il y a une chose ennuyeuse que le code isolé peut toujours faire: allouer autant de mémoire que possible, ce qui entraîne une augmentation de l'empreinte visible de l'application principale.

JSR 121: Application Isolation API Specification a été conçu pour résoudre ce problème, mais malheureusement, il n'a pas encore d'implémentation.

C'est un sujet assez détaillé, et j'écris tout cela du haut de ma tête.

Mais de toute façon, un code imparfait, à utiliser à vos risques et périls, probablement (pseudo):

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

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) {}
  }
}
108
waqas

De toute évidence, un tel système soulève toutes sortes de problèmes de sécurité. Java possède un cadre de sécurité rigoureux, mais ce n'est pas anodin. La possibilité de le bousiller et de laisser un utilisateur non privilégié accéder aux composants essentiels du système ne doit pas être négligée.

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

Une fois que vous avez le bytecode JVM, vous pouvez le charger dans la JVM en utilisant une fonction ClassLoaderdefineClass . Pour définir un contexte de sécurité pour cette classe chargée, vous devrez spécifier un ProtectionDomain . Le constructeur minimal pour un ProtectionDomain nécessite à la fois un CodeSource et un PermissionCollection . PermissionCollection est l'objet principal de votre utilisation ici - vous pouvez l'utiliser pour spécifier les autorisations exactes de la classe chargée. Ces autorisations doivent être finalement appliquées par la JVM AccessController .

Il y a beaucoup de points d'erreur possibles ici, et vous devez être extrêmement prudent pour tout comprendre complètement avant d'implémenter quoi que ce soit.

18
shsmurfy

Java-Sandbox est une bibliothèque pour exécuter Java code avec un ensemble limité d'autorisations. Il peut être utilisé pour autoriser l'accès à seulement un ensemble de classes sur liste blanche Il ne semble pas pouvoir restreindre l'accès aux méthodes individuelles. Il utilise un système avec un chargeur de classe personnalisé et un gestionnaire de sécurité pour y parvenir.

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

@waqas a donné une réponse très intéressante expliquant comment cela est possible de vous implémenter. Mais il est beaucoup plus sûr de laisser un tel code critique et complexe à des experts.

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

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());
10
Lii

Eh bien, il est très tard pour donner des suggestions ou des solutions, mais j'étais toujours confronté à un problème similaire, plutôt orienté vers la recherche. Fondamentalement, j'essayais de fournir une disposition et des évaluations automatiques pour les affectations de programmation pour Java sur les plates-formes d'apprentissage en ligne).

  1. une façon pourrait être de créer des machines virtuelles distinctes (pas JVM) mais des machines virtuelles réelles avec une configuration minimale possible pour chaque élève.
  2. Installez JRE pour Java ou des bibliothèques selon vos langages de programmation, selon ce que vous voulez que les élèves compilent et exécutent sur ces machines.

Je sais que cela semble assez complexe et beaucoup de tâches, mais Oracle Virtual Box fournit déjà Java API pour créer ou cloner des machines virtuelles dynamiquement. https://www.virtualbox.org /sdkref/index.html (Remarque, même VMware fournit également une API pour faire de même)

Et pour la distribution Linux de taille et de configuration minimum, vous pouvez vous référer à celle-ci ici http://www.slitaz.org/en/ ,

Alors maintenant, si les étudiants gâchent ou essaient de le faire, peut-être avec de la mémoire ou un système de fichiers ou un réseau, un socket, au maximum, il peut endommager sa propre machine virtuelle.

Également en interne dans ces machines virtuelles, vous pouvez fournir une sécurité supplémentaire comme Sandbox (gestionnaire de sécurité) pour Java ou créer des comptes spécifiques à l'utilisateur sur Linux et restreindre ainsi l'accès.

J'espère que cela t'aides !!

4
Shrikant Havale

Pour résoudre le problème dans la réponse acceptée selon laquelle le SecurityManager personnalisé s'appliquera à tous les threads de la machine virtuelle Java, plutôt que par thread, vous pouvez créer un SecurityManager personnalisé qui peut être activé/désactivé pour des threads spécifiques comme suit:

import Java.security.Permission;

public class SelectiveSecurityManager extends SecurityManager {

  private static final ToggleSecurityManagerPermission TOGGLE_PERMISSION = new ToggleSecurityManagerPermission();

  ThreadLocal<Boolean> enabledFlag = null;

  public SelectiveSecurityManager(final boolean enabledByDefault) {

    enabledFlag = new ThreadLocal<Boolean>() {

      @Override
      protected Boolean initialValue() {
        return enabledByDefault;
      }

      @Override
      public void set(Boolean value) {
        SecurityManager securityManager = System.getSecurityManager();
        if (securityManager != null) {
          securityManager.checkPermission(TOGGLE_PERMISSION);
        }
        super.set(value);
      }
    };
  }

  @Override
  public void checkPermission(Permission permission) {
    if (shouldCheck(permission)) {
      super.checkPermission(permission);
    }
  }

  @Override
  public void checkPermission(Permission permission, Object context) {
    if (shouldCheck(permission)) {
      super.checkPermission(permission, context);
    }
  }

  private boolean shouldCheck(Permission permission) {
    return isEnabled() || permission instanceof ToggleSecurityManagerPermission;
  }

  public void enable() {
    enabledFlag.set(true);
  }

  public void disable() {
    enabledFlag.set(false);
  }

  public boolean isEnabled() {
    return enabledFlag.get();
  }

}

ToggleSecurirtyManagerPermission n'est qu'une simple implémentation de Java.security.Permission pour garantir que seul le code autorisé peut activer/désactiver le gestionnaire de sécurité. Cela ressemble à ceci:

import Java.security.Permission;

public class ToggleSecurityManagerPermission extends Permission {

  private static final long serialVersionUID = 4812713037565136922L;
  private static final String NAME = "ToggleSecurityManagerPermission";

  public ToggleSecurityManagerPermission() {
    super(NAME);
  }

  @Override
  public boolean implies(Permission permission) {
    return this.equals(permission);
  }

  @Override
  public boolean equals(Object obj) {
    if (obj instanceof ToggleSecurityManagerPermission) {
      return true;
    }
    return false;
  }

  @Override
  public int hashCode() {
    return NAME.hashCode();
  }

  @Override
  public String getActions() {
    return "";
  }

}
3
alphaloop

Voici une solution thread-safe pour le 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.

}

Commentez s'il vous plaît!

CU

Arno

2
Arno Unkrig

Vous devrez probablement utiliser un SecurityManger et/ou AccessController personnalisé. Pour beaucoup de détails, voir Java Security Architecture et autre documentation de sécurité de Sun.

0
Kieron