Sandbox contro codice dannoso in un'applicazione Java


91

In un ambiente server di simulazione in cui agli utenti è consentito inviare il proprio codice che deve essere eseguito dal server, sarebbe chiaramente vantaggioso che qualsiasi codice inviato dall'utente venga eseguito in una sandbox, non diversamente dalle applet all'interno di un browser. Volevo essere in grado di sfruttare la stessa JVM, piuttosto che aggiungere un altro livello VM per isolare questi componenti inviati.

Questo tipo di limitazione sembra essere possibile utilizzando il modello sandbox Java esistente, ma esiste un modo dinamico per abilitarlo solo per le parti inviate dall'utente di un'applicazione in esecuzione?

Risposte:


109
  1. Eseguire il codice non attendibile nel proprio thread. Questo, ad esempio, previene problemi con cicli infiniti e simili e semplifica i passaggi futuri. Fai in modo che il thread principale attenda che il thread finisca e, se impiega troppo tempo, interrompilo con Thread.stop. Thread.stop è deprecato, ma poiché il codice non attendibile non dovrebbe avere accesso a nessuna risorsa, sarebbe sicuro ucciderlo.

  2. Imposta un SecurityManager su quel thread. Crea una sottoclasse di SecurityManager che sovrascrive checkPermission (Permission perm) per lanciare semplicemente un'eccezione SecurityException per tutte le autorizzazioni tranne alcune selezionate. C'è un elenco di metodi e autorizzazioni che richiedono qui: Autorizzazioni in Java TM 6 SDK .

  3. Utilizzare un ClassLoader personalizzato per caricare il codice non attendibile. Il tuo programma di caricamento classi verrebbe chiamato per tutte le classi utilizzate dal codice non attendibile, quindi puoi fare cose come disabilitare l'accesso alle singole classi JDK. La cosa da fare è avere una lista bianca delle classi JDK consentite.

  4. Potresti voler eseguire il codice non attendibile in una JVM separata. Mentre i passaggi precedenti renderebbero il codice sicuro, c'è una cosa fastidiosa che il codice isolato può ancora fare: allocare quanta più memoria possibile, il che fa crescere l'impronta visibile dell'applicazione principale.

JSR 121: la specifica API di isolamento dell'applicazione è stata progettata per risolvere questo problema, ma sfortunatamente non ha ancora un'implementazione.

Questo è un argomento piuttosto dettagliato e perlopiù lo sto scrivendo tutto dalla cima della mia testa.

Ma comunque, un codice imperfetto, da usare a proprio rischio, probabilmente con bug (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
  }
}

Manager della sicurezza

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
}

Filo

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

4
Quel codice potrebbe richiedere del lavoro. Non puoi davvero proteggerti dalla disponibilità di JVM. Preparati a terminare il processo (probabilmente automaticamente). Il codice entra in altri thread, ad esempio il thread del finalizzatore. Thread.stopcauserà problemi nel codice della libreria Java. Allo stesso modo, il codice della libreria Java richiederà autorizzazioni. Molto meglio per consentire l' SecurityManagerutilizzo java.security.AccessController. Il programma di caricamento classi dovrebbe probabilmente consentire anche l'accesso alle classi del codice utente.
Tom Hawtin - tackline

3
Dato che questo è un argomento così complicato, non esistono soluzioni per gestire i "plugin" Java in modo sicuro?
Nick Spacek

9
Il problema di questo approccio è che quando si imposta SecurityManager su Sistema, non solo ha un impatto sul thread in esecuzione, ma influisce anche su altri thread!
Gelin Luo

2
Siamo spiacenti ma thread.stop () può essere catturato con throwable. Puoi while (thread.isAlive) Thread.stop (), ma poi posso chiamare ricorsivamente una funzione che cattura l'eccezione. Testata sul mio pc, la funzione ricorsiva vince sullo stop (). Ora hai un filo di spazzatura, che ruba CPU e risorse
Lesto

8
Oltre al fatto che System.setSecurityManager(…)influenzerà l'intera JVM, non solo il thread che invoca quel metodo, l'idea di prendere decisioni di sicurezza basate sul thread è stata abbandonata quando Java è passato da 1.0 a 1.1. A questo punto è stato riconosciuto che il codice non attendibile può richiamare codice attendibile e viceversa, indipendentemente da quale thread esegue il codice. Nessuno sviluppatore dovrebbe ripetere l'errore.
Holger

18

Ovviamente un tale schema solleva tutti i tipi di problemi di sicurezza. Java ha un rigoroso framework di sicurezza, ma non è banale. La possibilità di rovinarla e di consentire a un utente non privilegiato di accedere a componenti vitali del sistema non dovrebbe essere trascurata.

A parte questo avvertimento, se stai prendendo l'input dell'utente sotto forma di codice sorgente, la prima cosa che devi fare è compilarlo in bytecode Java. AFIAK, questo non può essere fatto in modo nativo, quindi dovrai effettuare una chiamata di sistema a javac e compilare il codice sorgente in bytecode su disco. Ecco un tutorial che può essere utilizzato come punto di partenza per questo. Modifica : come ho appreso nei commenti, puoi effettivamente compilare il codice Java dal sorgente in modo nativo usando javax.tools.

Una volta che hai il bytecode JVM, puoi caricarlo nella JVM utilizzando la funzione defineClass di un ClassLoader . Per impostare un contesto di sicurezza per questa classe caricata, sarà necessario specificare un ProtectionDomain . Il costruttore minimo per un ProtectionDomain richiede sia un CodeSource che un PermissionCollection . PermissionCollection è l'oggetto di utilizzo principale per te qui: puoi usarlo per specificare le autorizzazioni esatte di cui dispone la classe caricata. Tali autorizzazioni devono essere in ultima analisi, applicate dalla JVM AccessController .

Ci sono molti possibili punti di errore qui e dovresti essere estremamente attento a capire completamente tutto prima di implementare qualsiasi cosa.


2
La compilazione Java è piuttosto semplice utilizzando l'API javax.tools di JDK 6.
Alan Krueger

10

Il Java-Sandbox è una libreria per l'esecuzione di codice Java con un insieme limitato di permessi. Può essere utilizzato per consentire l'accesso solo a un set di classi e risorse white list. Non sembra essere in grado di limitare l'accesso ai singoli metodi. Utilizza un sistema con un caricatore di classi personalizzato e un gestore della sicurezza per raggiungere questo obiettivo.

Non l'ho usato ma sembra ben progettato e ragionevolmente ben documentato.

@waqas ha fornito una risposta molto interessante spiegando come questo sia possibile implementare da soli. Ma è molto più sicuro lasciare un codice così complesso e critico per la sicurezza agli esperti.

Si noti però che il progetto non è stato aggiornato dal 2013 e i creatori lo descrivono come "sperimentale". La sua home page è scomparsa ma rimane la voce di Source Forge.

Codice di esempio adattato dal sito web del progetto:

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

4

Per risolvere il problema nella risposta accettata in base al quale l'usanza SecurityManagersi applicherà a tutti i thread nella JVM, piuttosto che su base per thread, è possibile creare un custom SecurityManagerche può essere abilitato / disabilitato per thread specifici come segue:

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è solo una semplice implementazione di java.security.Permissionper garantire che solo il codice autorizzato possa abilitare / disabilitare il gestore della sicurezza. Assomiglia a questo:

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

}


Uso molto intelligente di ThreadLocal per rendere i SecurityManager con ambito di sistema in modo efficace con ambito thread (cosa che la maggior parte degli utenti vorrebbe). Considera anche l'utilizzo di InheritableThreadLocal per trasmettere automaticamente la proprietà non consentita ai thread generati da codice non attendibile.
Nick

4

Beh, è ​​molto tardi per dare suggerimenti o soluzioni, ma stavo ancora affrontando un tipo di problema simile, un po 'più orientato alla ricerca. Fondamentalmente stavo cercando di fornire un provvedimento e valutazioni automatiche per incarichi di programmazione per corsi Java in piattaforme e-learning.

  1. un modo potrebbe essere, creare macchine virtuali separate (non JVM) ma macchine virtuali effettive con il sistema operativo minimo di configurazione possibile per ciascuno studente.
  2. Installa JRE per Java o le librerie in base ai tuoi linguaggi di programmazione, a seconda di quale desideri che gli studenti compilino ed eseguano su queste macchine.

So che questo sembra un compito piuttosto complesso e molte attività, ma Oracle Virtual Box fornisce già API Java per creare o clonare macchine virtuali dinamicamente. https://www.virtualbox.org/sdkref/index.html (Nota, anche VMware fornisce anche API per fare lo stesso)

E per la dimensione minima e la configurazione della distribuzione Linux puoi fare riferimento a questa qui http://www.slitaz.org/en/ ,

Quindi ora se gli studenti sbagliano o cercano di farlo, potrebbe essere con memoria o file system o rete, socket, al massimo può danneggiare la propria VM.

Anche internamente a queste VM è possibile fornire sicurezza aggiuntiva come Sandbox (gestore della sicurezza) per Java o creare account specifici per utente su Linux e quindi limitare l'accesso.

Spero che questo ti aiuti !!


3

Ecco una soluzione thread-safe per il problema:

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.

}

Per favore, commenta!

CU

Arno


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.