Sandbox przeciwko złośliwemu kodowi w aplikacji Java

92

W środowisku serwera symulacji, w którym użytkownicy mogą przesyłać swój własny kod do uruchomienia przez serwer, byłoby oczywiście korzystne, gdyby każdy kod przesłany przez użytkownika był uruchamiany po stronie piaskownicy, podobnie jak aplety w przeglądarce. Chciałem móc wykorzystać samą maszynę JVM, zamiast dodawać kolejną warstwę maszyny wirtualnej, aby odizolować te przesłane komponenty.

Wydaje się, że tego rodzaju ograniczenie jest możliwe przy użyciu istniejącego modelu piaskownicy Javy, ale czy istnieje dynamiczny sposób, aby włączyć to tylko dla części uruchomionej aplikacji przesłanych przez użytkownika?

Alan Krueger
źródło

Odpowiedzi:

110
  1. Uruchom niezaufany kod we własnym wątku. Na przykład zapobiega to problemom z nieskończonymi pętlami i tym podobnymi, a także ułatwia przyszłe kroki. Niech główny wątek zaczeka na zakończenie wątku, a jeśli trwa to zbyt długo, zabij go za pomocą Thread.stop. Thread.stop jest przestarzały, ale ponieważ niezaufany kod nie powinien mieć dostępu do żadnych zasobów, bezpieczne byłoby jego zabicie.

  2. Ustaw SecurityManager w tym wątku. Utwórz podklasę SecurityManager, która zastępuje checkPermission (uprawnienie Permission), aby po prostu zgłosić SecurityException dla wszystkich uprawnień z wyjątkiem kilku wybranych. Oto lista metod i wymaganych przez nie uprawnień: Uprawnienia w Java TM 6 SDK .

  3. Użyj niestandardowego ClassLoadera, aby załadować niezaufany kod. Twój program ładujący klasy zostałby wywołany dla wszystkich klas, których używa niezaufany kod, więc możesz zrobić takie rzeczy, jak wyłączenie dostępu do poszczególnych klas JDK. Trzeba tylko mieć białą listę dozwolonych klas JDK.

  4. Możesz chcieć uruchomić niezaufany kod w oddzielnej JVM. Podczas gdy poprzednie kroki sprawiłyby, że kod byłby bezpieczny, jest jedna irytująca rzecz, którą izolowany kod może nadal zrobić: alokować jak najwięcej pamięci, co powoduje, że widoczny ślad głównej aplikacji rośnie.

JSR 121: Specyfikacja API izolacji aplikacji została zaprojektowana, aby rozwiązać ten problem, ale niestety nie ma jeszcze implementacji.

Jest to dość szczegółowy temat i głównie piszę to wszystko od początku do końca.

Tak czy inaczej, jakiś niedoskonały, używany na własne ryzyko, prawdopodobnie błędny (pseudo) kod:

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

Menadżer ochrony

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
}

Wątek

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
źródło
4
Ten kod może wymagać trochę pracy. Tak naprawdę nie można ustrzec się przed dostępnością JVM. Przygotuj się na zabicie procesu (prawdopodobnie automatycznie). Kod przechodzi do innych wątków - na przykład do wątku finalizującego. Thread.stopspowoduje problemy w kodzie biblioteki Java. Podobnie kod biblioteki Java będzie wymagał uprawnień. Znacznie lepiej pozwolić na SecurityManagerużycie java.security.AccessController. Program ładujący klasy powinien prawdopodobnie również umożliwiać dostęp do własnych klas kodu użytkownika.
Tom Hawtin - tackline
4
Biorąc pod uwagę, że jest to tak skomplikowany temat, czy nie istnieją rozwiązania umożliwiające bezpieczną obsługę „wtyczek” Java?
Nick Spacek,
10
Problem z tym podejściem polega na tym, że ustawienie SecurityManager na System nie tylko wpływa na działający wątek, ale także wpływa na inny wątek!
Gelin Luo,
2
Przepraszamy, ale thread.stop () może zostać przechwycona za pomocą throwable. Możesz while (thread.isAlive) Thread.stop (), ale wtedy mogę rekurencyjnie wywołać funkcję, która przechwytuje wyjątek. Funkcja rekurencyjna, przetestowana na moim komputerze, wygrywa z funkcją stop (). Teraz masz wątek śmieciowy, kradnący procesor i zasoby
Lesto
9
Poza tym, że System.setSecurityManager(…)wpłynie to na całą maszynę JVM, a nie tylko na wątek wywołujący tę metodę, pomysł podejmowania decyzji dotyczących bezpieczeństwa na podstawie wątku został porzucony, gdy Java przełączyła się z wersji 1.0 na 1.1. W tym czasie uznano, że niezaufany kod może wywołać zaufany kod i odwrotnie, niezależnie od tego, który wątek wykonuje kod. Żaden programista nie powinien powtórzyć tego błędu.
Holger
18

Oczywiście taki schemat budzi różnego rodzaju obawy dotyczące bezpieczeństwa. Java ma rygorystyczne ramy bezpieczeństwa, ale nie jest to trywialne. Nie należy przeoczyć możliwości jej zepsucia i umożliwienia nieuprzywilejowanemu użytkownikowi dostępu do ważnych elementów systemu.

Pomijając to ostrzeżenie, jeśli przyjmujesz dane wejściowe użytkownika w postaci kodu źródłowego, pierwszą rzeczą, którą musisz zrobić, jest skompilowanie go do kodu bajtowego Java. AFIAK, nie można tego zrobić natywnie, więc będziesz musiał wykonać wywołanie systemowe javac i skompilować kod źródłowy do kodu bajtowego na dysku. Oto samouczek, który może być użyty jako punkt wyjścia. Edycja : jak dowiedziałem się w komentarzach, możesz skompilować kod Java ze źródła natywnie, używając javax.tools.JavaCompiler

Po uzyskaniu kodu bajtowego maszyny JVM można go załadować do maszyny JVM za pomocą funkcji defineClass programu ClassLoader . Aby ustawić kontekst zabezpieczeń dla tej załadowanej klasy, musisz określić ProtectionDomain . Minimalny konstruktor dla ProtectionDomain wymaga zarówno CodeSource, jak i PermissionCollection . PermissionCollection jest tutaj przedmiotem podstawowego użytku - możesz go użyć do określenia dokładnych uprawnień, jakie ma załadowana klasa. Te uprawnienia powinny być ostatecznie wymuszone przez AccessController maszyny JVM .

Istnieje wiele możliwych błędów i powinieneś być bardzo ostrożny, aby całkowicie zrozumieć wszystko, zanim cokolwiek zaimplementujesz.

shsmurfy
źródło
2
Kompilacja Javy jest całkiem łatwa dzięki API javax.tools JDK 6.
Alan Krueger
10

Java-Sandbox jest biblioteką do wykonywania kodu Java z ograniczonego zestawu uprawnień. Można go użyć, aby umożliwić dostęp tylko do zestawu klas i zasobów z białej listy. Wydaje się, że nie jest w stanie ograniczyć dostępu do poszczególnych metod. Aby to osiągnąć, wykorzystuje system z niestandardowym programem ładującym klasy i menedżerem bezpieczeństwa.

Nie używałem go, ale wygląda na dobrze zaprojektowany i dość dobrze udokumentowany.

@waqas udzielił bardzo interesującej odpowiedzi wyjaśniającej, jak można to zrealizować samodzielnie. Ale znacznie bezpieczniej jest pozostawić taki krytyczny i złożony kod z punktu widzenia bezpieczeństwa ekspertom.

Zwróć jednak uwagę, że projekt nie był aktualizowany od 2013 roku, a twórcy określają go jako „eksperymentalny”. Jego strona główna zniknęła, ale wpis Source Forge pozostał.

Przykładowy kod zaadaptowany ze strony internetowej projektu:

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
źródło
4

Aby rozwiązać problem w zaakceptowanej odpowiedzi, zgodnie z którą niestandardowy SecurityManagerbędzie miał zastosowanie do wszystkich wątków w JVM, a nie na podstawie poszczególnych wątków, możesz utworzyć niestandardowy, SecurityManagerktóry można włączyć / wyłączyć dla określonych wątków w następujący sposób:

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

}

ToggleSecurirtyManagerPermissionto tylko prosta implementacja java.security.Permissionzapewniająca, że ​​tylko autoryzowany kod może włączać / wyłączać menedżera bezpieczeństwa. To wygląda tak:

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
źródło
Bardzo sprytne wykorzystanie ThreadLocal do sprawnego, aby SecurityManagers działały w zakresie systemu i efektywnie obejmowały zakres wątków (czego chciałaby większość użytkowników). Rozważ również użycie InheritableThreadLocal do automatycznego przesyłania niedozwolonej właściwości do wątków tworzonych przez niezaufany kod.
Nick
4

Cóż, jest już bardzo późno na sugestie lub rozwiązania, ale wciąż miałem do czynienia z podobnym problemem, bardziej ukierunkowanym na badania. Zasadniczo próbowałem zapewnić zabezpieczenie i automatyczne oceny zadań programistycznych dla kursu Java na platformach e-learningowych.

  1. jednym ze sposobów mogłoby być utworzenie oddzielnych maszyn wirtualnych (nie JVM), ale rzeczywistych maszyn wirtualnych z minimalną możliwą konfiguracją systemu operacyjnego dla każdego ucznia.
  2. Zainstaluj środowisko JRE dla języka Java lub biblioteki zgodnie z językami programowania, które chcesz, aby uczniowie mogli kompilować i uruchamiać na tych maszynach.

Wiem, że brzmi to dość skomplikowanie i wymaga wielu zadań, ale Oracle Virtual Box już zapewnia Java API do dynamicznego tworzenia lub klonowania maszyn wirtualnych. https://www.virtualbox.org/sdkref/index.html (Uwaga, nawet VMware zapewnia również API do robienia tego samego)

Aby uzyskać informacje o minimalnym rozmiarze i konfiguracji dystrybucji Linuksa, możesz odwołać się do tej tutaj http://www.slitaz.org/en/ ,

Więc teraz, jeśli uczniowie coś zepsują lub spróbują to zrobić, może mieć pamięć, system plików lub sieć, gniazdo, maksymalnie może uszkodzić własną maszynę wirtualną.

Również wewnętrznie w tych maszynach wirtualnych można zapewnić dodatkowe zabezpieczenia, takie jak Sandbox (menedżer bezpieczeństwa) dla języka Java lub tworzenie kont dla użytkowników w systemie Linux, a tym samym ograniczanie dostępu.

Mam nadzieję że to pomoże !!

Shrikant Havale
źródło
3

Oto bezpieczne wątkowo rozwiązanie problemu:

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.

}

Proszę skomentuj!

CU

Arno

Arno Unkrig
źródło