Sandbox contre le code malveillant dans une application Java

91

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é dans un bac à sable, contrairement aux applets dans un navigateur. Je voulais pouvoir tirer parti de la JVM elle-même, plutôt que d'ajouter une autre couche VM pour isoler ces composants soumis.

Ce type de limitation semble possible en utilisant le modèle de bac à sable Java 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?

Alan Krueger
la source

Réponses:

109
  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 étapes futures. Demandez au thread principal d'attendre que le thread se termine, et si cela prend trop de temps, supprimez-le avec Thread.stop. Thread.stop est obsolète, mais comme le code non approuvé ne devrait avoir accès à aucune ressource, il serait prudent de le tuer.

  2. Définissez un SecurityManager sur ce thread. Créez une sous-classe de SecurityManager qui remplace checkPermission (Permission perm) pour simplement lancer une SecurityException pour toutes les autorisations sauf quelques-unes. Il existe une liste de méthodes et des autorisations dont elles ont besoin ici: Autorisations dans le SDK Java TM 6 .

  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 approuvé, 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 encore faire: allouer autant de mémoire que possible, ce qui augmente l'empreinte visible de l'application principale.

JSR 121: La spécification d'API d'isolation d'application a été conçue pour résoudre ce problème, mais malheureusement, elle n'a pas encore d'implémentation.

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

Mais de toute façon, du code imparfait, à utiliser à vos risques et périls, probablement bogué (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
  }
}

Responsable de la sécurité

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) {}
  }
}
waqas
la source
4
Ce code pourrait nécessiter du travail. Vous ne pouvez pas vraiment vous prémunir contre la disponibilité JVM. Soyez prêt à tuer le processus (probablement automatiquement). Le code passe sur d'autres threads - par exemple le thread finaliser. Thread.stopcausera 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. Beaucoup mieux pour permettre l' SecurityManagerutilisation java.security.AccessController. Le chargeur de classe devrait probablement également autoriser l'accès aux propres classes du code utilisateur.
Tom Hawtin - tackline
3
Étant donné qu'il s'agit d'un sujet si compliqué, n'y a-t-il pas de solutions existantes pour gérer les "plugins" Java de manière sûre?
Nick Spacek
9
Le problème de cette approche est que lorsque vous définissez SecurityManager sur System, cela n'affecte pas seulement le thread en cours d'exécution, mais également les autres threads!
Gelin Luo
2
Désolé mais thread.stop () peut être attrapé avec throwable. Vous pouvez while (thread.isAlive) Thread.stop (), mais je peux alors appeler récursivement une fonction qui intercepte l'exception. Testée sur mon pc, la fonction récursive l'emporte sur stop (). Maintenant, vous avez un fil d'ordures, vol de CPU et de ressources
Lesto
8
Outre le fait que System.setSecurityManager(…)cela affectera l'ensemble de la JVM, pas seulement le thread invoquant cette méthode, l'idée de prendre des décisions de sécurité basées sur le thread a été abandonnée lorsque Java est passé de 1.0 à 1.1. À ce moment, il a été reconnu que du code non approuvé peut invoquer du code approuvé et vice versa, quel que soit le thread qui exécute le code. Aucun développeur ne doit répéter l'erreur.
Holger
18

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

Cet avertissement mis à part, si vous prenez une entrée utilisateur sous forme de code source, la première chose à faire est de la compiler en bytecode Java. AFIAK, cela ne peut pas être fait en natif, vous devrez donc faire un appel système à javac et compiler le code source en 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 en fait compiler le code Java à partir des sources de manière native en utilisant javax.tools.JavaCompiler

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

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

shsmurfy
la source
2
La compilation Java est assez simple en utilisant l'API javax.tools de JDK 6.
Alan Krueger
10

Le Java Sandbox est une bibliothèque pour exécuter du code Java avec un nombre limité d'autorisations. Il peut être utilisé pour autoriser l'accès uniquement à un ensemble de classes et de ressources sur liste blanche. Il ne semble pas être en mesure 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é 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 il est possible de l'implémenter soi-même. Mais il est beaucoup plus sûr de laisser ce 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 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());
Lii
la source
4

Pour résoudre le problème dans la réponse acceptée selon laquelle la personnalisation SecurityManagers'appliquera à tous les threads de la JVM, plutôt que sur une base par thread, vous pouvez créer une personnalisation SecurityManagerqui peut être activée / désactivée 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();
  }

}

ToggleSecurirtyManagerPermissionest juste une simple implémentation de java.security.Permissionpour s'assurer 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 "";
  }

}
alphaloop
la source
Utilisation très intelligente de ThreadLocal pour rendre les SecurityManagers à l'échelle du système effectivement à l'échelle des threads (ce que la plupart des utilisateurs souhaiteraient). Envisagez également d'utiliser InheritableThreadLocal pour transmettre automatiquement la propriété non autorisée aux threads générés par du code non approuvé.
Nick
4

Eh bien, il est très tard pour donner des suggestions ou des solutions, mais j'étais toujours confronté à un problème similaire, un peu plus axé sur la recherche. Fondamentalement, j'essayais de fournir une disposition et des évaluations automatiques pour les affectations de programmation pour le cours Java dans 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 du système d'exploitation possible pour chaque étudiant.
  2. Installez JRE pour Java ou les bibliothèques en fonction de vos langages de programmation, selon ce que vous voulez que les étudiants compilent et exécutent sur ces machines.

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

Et pour la taille minimale et la configuration de la distribution Linux, 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 la mémoire ou le système de fichiers ou le réseau, le socket, au maximum, il peut endommager sa propre VM.

É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 ainsi restreindre l'accès.

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

Shrikant Havale
la source
3

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

Arno Unkrig
la source