Sandbox gegen Schadcode in einer Java-Anwendung

90

In einer Simulationsserverumgebung, in der Benutzer ihren eigenen Code zur Ausführung durch den Server senden dürfen, wäre es eindeutig vorteilhaft, wenn jeder vom Benutzer übermittelte Code in einer Sandbox ausgeführt wird, ähnlich wie Applets in einem Browser. Ich wollte in der Lage sein, die JVM selbst zu nutzen, anstatt eine weitere VM-Schicht hinzuzufügen, um diese übermittelten Komponenten zu isolieren.

Diese Art der Einschränkung scheint mit dem vorhandenen Java-Sandbox-Modell möglich zu sein. Gibt es jedoch eine dynamische Möglichkeit, dies nur für die vom Benutzer eingereichten Teile einer laufenden Anwendung zu aktivieren?

Alan Krueger
quelle

Antworten:

108
  1. Führen Sie den nicht vertrauenswürdigen Code in einem eigenen Thread aus. Dies verhindert beispielsweise Probleme mit Endlosschleifen und dergleichen und erleichtert die zukünftigen Schritte. Lassen Sie den Haupt-Thread warten, bis der Thread fertig ist. Wenn er zu lange dauert, beenden Sie ihn mit Thread.stop. Thread.stop ist veraltet, aber da der nicht vertrauenswürdige Code keinen Zugriff auf Ressourcen haben sollte, wäre es sicher, ihn zu beenden.

  2. Legen Sie einen SecurityManager für diesen Thread fest. Erstellen Sie eine Unterklasse von SecurityManager, die checkPermission (Permission perm) überschreibt, um einfach eine SecurityException für alle Berechtigungen mit Ausnahme einiger weniger auszulösen . Hier finden Sie eine Liste der Methoden und der erforderlichen Berechtigungen: Berechtigungen im Java TM 6 SDK .

  3. Verwenden Sie einen benutzerdefinierten ClassLoader, um den nicht vertrauenswürdigen Code zu laden. Ihr Klassenladeprogramm wird für alle Klassen aufgerufen, die der nicht vertrauenswürdige Code verwendet, sodass Sie beispielsweise den Zugriff auf einzelne JDK-Klassen deaktivieren können. Die Sache zu tun ist, eine weiße Liste der erlaubten JDK-Klassen zu haben.

  4. Möglicherweise möchten Sie den nicht vertrauenswürdigen Code in einer separaten JVM ausführen. Während die vorherigen Schritte den Code sicherer machen würden, gibt es eine ärgerliche Sache, die der isolierte Code noch tun kann: so viel Speicher wie möglich zuweisen, wodurch der sichtbare Footprint der Hauptanwendung wächst.

JSR 121: Application Isolation API Specification wurde entwickelt, um dieses Problem zu lösen, hat aber leider noch keine Implementierung.

Dies ist ein ziemlich detailliertes Thema, und ich schreibe das meistens aus dem Kopf.

Aber trotzdem, ein unvollkommener, auf eigenes Risiko verwendbarer, wahrscheinlich fehlerhafter (Pseudo-) Code:

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
  }
}

Sicherheitsmanager

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
}

Faden

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
quelle
4
Dieser Code benötigt möglicherweise etwas Arbeit. Sie können sich nicht wirklich gegen die Verfügbarkeit von JVM schützen. Seien Sie bereit, den Prozess abzubrechen (wahrscheinlich automatisch). Code wird auf andere Threads übertragen - zum Beispiel auf den Finaliser-Thread. Thread.stopverursacht Probleme im Java-Bibliothekscode. Ebenso erfordert der Java-Bibliothekscode Berechtigungen. Viel besser zuzulassen, dass die SecurityManagerverwenden java.security.AccessController. Der Klassenlader sollte wahrscheinlich auch den Zugriff auf die eigenen Klassen des Benutzercodes ermöglichen.
Tom Hawtin - Tackline
3
Gibt es angesichts der Tatsache, dass dies ein so kompliziertes Thema ist, keine Lösungen für den sicheren Umgang mit Java-Plugins?
Nick Spacek
8
Das Problem dieses Ansatzes ist, wenn Sie SecurityManager auf System setzen, wirkt sich dies nicht nur auf den laufenden Thread aus, sondern auch auf andere Threads!
Gelin Luo
2
Sorry aber thread.stop () kann mit throwable gefangen werden. Sie können während (thread.isAlive) Thread.stop (), aber dann kann ich rekursiv eine Funktion aufrufen, die die Ausnahme abfängt. Auf meinem PC getestet, gewinnt die rekursive Funktion über stop (). Jetzt haben Sie einen Müll-Thread, der CPU und Ressourcen stiehlt
Lesto
8
Neben der Tatsache, dass System.setSecurityManager(…)sich dies auf die gesamte JVM auswirkt, nicht nur auf den Thread, der diese Methode aufruft, wurde die Idee, Sicherheitsentscheidungen basierend auf dem Thread zu treffen, aufgegeben, als Java von 1.0 auf 1.1 umgestellt wurde. Zu diesem Zeitpunkt wurde erkannt, dass nicht vertrauenswürdiger Code vertrauenswürdigen Code aufrufen kann und umgekehrt, unabhängig davon, welcher Thread den Code ausführt. Kein Entwickler sollte den Fehler wiederholen.
Holger
18

Offensichtlich wirft ein solches System alle möglichen Sicherheitsbedenken auf. Java verfügt über ein strenges Sicherheits-Framework, das jedoch nicht trivial ist. Die Möglichkeit, es zu vermasseln und einem nicht privilegierten Benutzer den Zugriff auf wichtige Systemkomponenten zu ermöglichen, sollte nicht übersehen werden.

Abgesehen von dieser Warnung müssen Sie, wenn Sie Benutzereingaben in Form von Quellcode vornehmen, diese zunächst in Java-Bytecode kompilieren. AFIAK, dies kann nicht nativ durchgeführt werden, daher müssen Sie einen Systemaufruf an javac durchführen und den Quellcode zu Bytecode auf der Festplatte kompilieren. Hier ist ein Tutorial, das als Ausgangspunkt dafür verwendet werden kann. Bearbeiten : Wie ich in den Kommentaren erfahren habe, können Sie Java-Code aus der Quelle nativ mit javax.tools.JavaCompiler kompilieren

Sobald Sie einen JVM-Bytecode haben, können Sie ihn mit der defineClass- Funktion eines ClassLoaders in die JVM laden . Um einen Sicherheitskontext für diese geladene Klasse festzulegen, müssen Sie eine ProtectionDomain angeben . Der minimale Konstruktor für eine ProtectionDomain erfordert sowohl eine CodeSource als auch eine PermissionCollection . Die PermissionCollection ist das Objekt, das Sie hier hauptsächlich verwenden. Sie können damit die genauen Berechtigungen angeben, über die die geladene Klasse verfügt. Diese Berechtigungen sollten letztendlich vom AccessController der JVM erzwungen werden .

Hier gibt es viele mögliche Fehlerpunkte, und Sie sollten äußerst vorsichtig sein, um alles vollständig zu verstehen, bevor Sie etwas implementieren.

shsmurfy
quelle
2
Die Java-Kompilierung ist mit der javax.tools-API von JDK 6 recht einfach.
Alan Krueger
10

Die Java-Sandbox ist eine Bibliothek zum Ausführen von Java-Code mit begrenzten Berechtigungen. Es kann verwendet werden, um nur auf eine Reihe von Klassen und Ressourcen auf der weißen Liste zuzugreifen. Es scheint nicht in der Lage zu sein, den Zugriff auf einzelne Methoden einzuschränken. Dazu wird ein System mit einem benutzerdefinierten Klassenladeprogramm und einem Sicherheitsmanager verwendet.

Ich habe es nicht benutzt, aber es sieht gut gestaltet und ziemlich gut dokumentiert aus.

@waqas hat eine sehr interessante Antwort gegeben, die erklärt, wie dies möglich ist, sich selbst zu implementieren. Es ist jedoch viel sicherer, solchen sicherheitskritischen und komplexen Code Experten zu überlassen.

Beachten Sie jedoch, dass das Projekt seit 2013 nicht mehr aktualisiert wurde und von den Erstellern als "experimentell" beschrieben wird. Die Homepage ist verschwunden, der Eintrag in Source Forge bleibt jedoch erhalten.

Beispielcode angepasst von der Projektwebsite:

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
quelle
4

Nun, es ist sehr spät, Vorschläge oder Lösungen zu geben, aber ich hatte immer noch ein ähnliches Problem, eher forschungsorientiert. Grundsätzlich habe ich versucht, eine Bereitstellung und automatische Auswertungen für Programmieraufgaben für Java-Kurse in E-Learning-Plattformen bereitzustellen.

  1. Eine Möglichkeit könnte sein, eine separate virtuelle Maschine (nicht JVM) zu erstellen, sondern tatsächliche virtuelle Maschinen mit einem für jeden Schüler minimal konfigurierbaren Betriebssystem.
  2. Installieren Sie JRE für Java oder Bibliotheken entsprechend Ihren Programmiersprachen, je nachdem, was die Schüler auf diesen Computern kompilieren und ausführen sollen.

Ich weiß, dass dies ziemlich komplex und mit vielen Aufgaben verbunden ist, aber Oracle Virtual Box bietet bereits eine Java-API zum dynamischen Erstellen oder Klonen von virtuellen Maschinen. https://www.virtualbox.org/sdkref/index.html (Hinweis: Selbst VMware bietet dafür eine API.)

Informationen zur Mindestgröße und Konfiguration der Linux-Distribution finden Sie hier http://www.slitaz.org/en/ ,

Wenn die Schüler es nun vermasseln oder versuchen, dies zu tun, kann es sein, dass er seine eigene VM beschädigen kann, möglicherweise mit Speicher oder Dateisystem oder Netzwerk, Socket.

Auch intern in diesen VMs können Sie zusätzliche Sicherheit wie Sandbox (Sicherheitsmanager) für Java bereitstellen oder benutzerspezifische Konten unter Linux erstellen und so den Zugriff einschränken.

Hoffe das hilft !!

Shrikant Havale
quelle
3

Hier ist eine thread-sichere Lösung für das Problem:

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.

}

Bitte kommentieren!

CU

Arno

Arno Unkrig
quelle
3

Um das Problem in der akzeptierten Antwort zu beheben, bei der der benutzerdefinierte Benutzer SecurityManagerfür alle Threads in der JVM und nicht für jeden Thread gilt, können Sie einen benutzerdefinierten Benutzer erstellen SecurityManager, der für bestimmte Threads wie folgt aktiviert / deaktiviert werden kann:

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();
  }

}

ToggleSecurirtyManagerPermissionist nur eine einfache Implementierung von, java.security.Permissionum sicherzustellen, dass nur autorisierter Code den Sicherheitsmanager aktivieren / deaktivieren kann. Es sieht aus wie das:

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
quelle
2
Zitieren Sie Ihre (eigenen) Quellen: alphaloop.blogspot.com/2014/08/… und github.com/alphaloop/selective-security-manager .
ziesemer
Sehr intelligente Verwendung von ThreadLocal, um SecurityManager mit Systembereich effektiv mit Thread-Bereich zu versehen (was die meisten Benutzer wünschen würden). Verwenden Sie auch InheritableThreadLocal, um die nicht zugelassene Eigenschaft automatisch an Threads zu übertragen, die durch nicht vertrauenswürdigen Code erzeugt werden.
Nick