Sandbox contra código malicioso en una aplicación Java

91

En un entorno de servidor de simulación en el que los usuarios pueden enviar su propio código para que lo ejecute el servidor, sería claramente ventajoso que cualquier código enviado por el usuario se ejecutara en una caja de arena, al igual que los applets dentro de un navegador. Quería poder aprovechar la JVM en sí, en lugar de agregar otra capa de VM para aislar estos componentes enviados.

Este tipo de limitación parece ser posible utilizando el modelo de caja de arena de Java existente, pero ¿hay una forma dinámica de habilitar eso solo para las partes enviadas por el usuario de una aplicación en ejecución?

Alan Krueger
fuente

Respuestas:

109
  1. Ejecute el código que no es de confianza en su propio hilo. Esto, por ejemplo, evita problemas con bucles infinitos y demás, y facilita los pasos futuros. Haga que el hilo principal espere a que termine, y si tarda demasiado, elimínelo con Thread.stop. Thread.stop está obsoleto, pero dado que el código que no es de confianza no debería tener acceso a ningún recurso, sería seguro eliminarlo.

  2. Establezca un SecurityManager en ese hilo. Cree una subclase de SecurityManager que anule checkPermission (Permiso de permiso) para simplemente lanzar una SecurityException para todos los permisos excepto unos pocos seleccionados. Hay una lista de métodos y los permisos que requieren aquí: Permisos en Java TM 6 SDK .

  3. Utilice un ClassLoader personalizado para cargar el código que no es de confianza. Se llamaría a su cargador de clases para todas las clases que usa el código que no es de confianza, por lo que puede hacer cosas como deshabilitar el acceso a clases individuales de JDK. Lo que hay que hacer es tener una lista blanca de clases JDK permitidas.

  4. Es posible que desee ejecutar el código que no es de confianza en una JVM separada. Si bien los pasos anteriores harían que el código fuera seguro, hay una cosa molesta que el código aislado todavía puede hacer: asignar tanta memoria como pueda, lo que hace que la huella visible de la aplicación principal crezca.

JSR 121: La especificación de API de aislamiento de aplicaciones fue diseñada para resolver esto, pero desafortunadamente aún no tiene una implementación.

Este es un tema bastante detallado, y sobre todo estoy escribiendo todo esto de la cabeza.

Pero de todos modos, algún código imperfecto, de uso bajo su propio riesgo, probablemente con errores (pseudo) código:

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

Gerente de seguridad

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
}

Hilo

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
fuente
4
Ese código podría necesitar algo de trabajo. Realmente no puede protegerse contra la disponibilidad de JVM. Esté preparado para matar el proceso (probablemente automáticamente). El código llega a otros hilos, por ejemplo, el hilo del finalizador. Thread.stopcausará problemas en el código de la biblioteca de Java. De manera similar, el código de la biblioteca de Java requerirá permisos. Mucho mejor para permitir el SecurityManageruso java.security.AccessController. El cargador de clases probablemente también debería permitir el acceso a las propias clases del código de usuario.
Tom Hawtin - tackline
3
Dado que este es un tema tan complicado, ¿no existen soluciones para manejar los "complementos" de Java de una manera segura?
Nick Spacek
9
El problema de este enfoque es que cuando configura SecurityManager en Sistema, no solo afecta el hilo en ejecución, ¡sino que también afecta a otros hilos!
Gelin Luo
2
Lo siento, pero thread.stop () se puede atrapar con throwable. Puede while (thread.isAlive) Thread.stop (), pero luego puedo llamar de forma recursiva a una función que detecta la excepción. Probado en mi PC, la función recursiva gana sobre el stop (). Ahora tienes un hilo de basura, robando CPU y recursos
Lesto
8
Además del hecho de que System.setSecurityManager(…)afectará a toda la JVM, no solo al hilo que invoca ese método, la idea de tomar decisiones de seguridad basadas en el hilo se abandonó cuando Java cambió de 1.0 a 1.1. En este momento se reconoció que el código que no es de confianza puede invocar un código de confianza y viceversa, independientemente del hilo que ejecute el código. Ningún desarrollador debería repetir el error.
Holger
18

Obviamente, este esquema plantea todo tipo de problemas de seguridad. Java tiene un marco de seguridad riguroso, pero no es trivial. La posibilidad de estropearlo y permitir que un usuario sin privilegios acceda a componentes vitales del sistema no debe pasarse por alto.

Aparte de esa advertencia, si está tomando la entrada del usuario en forma de código fuente, lo primero que debe hacer es compilarlo en el código de bytes de Java. AFIAK, esto no se puede hacer de forma nativa, por lo que deberá realizar una llamada del sistema a javac y compilar el código fuente en bytecode en el disco. Aquí hay un tutorial que puede usarse como punto de partida para esto. Editar : como aprendí en los comentarios, en realidad puede compilar código Java desde la fuente de forma nativa usando javax.tools.JavaCompiler

Una vez que tenga el código de bytes de JVM, puede cargarlo en la JVM usando la función defineClass de ClassLoader . Para establecer un contexto de seguridad para esta clase cargada, deberá especificar un ProtectionDomain . El constructor mínimo de ProtectionDomain requiere tanto CodeSource como PermissionCollection . PermissionCollection es el objeto de uso principal para usted aquí; puede usarlo para especificar los permisos exactos que tiene la clase cargada. El AccessController de JVM debe hacer cumplir estos permisos en última instancia .

Aquí hay muchos puntos de error posibles, y debe tener mucho cuidado de comprender todo completamente antes de implementar cualquier cosa.

shsmurfy
fuente
2
La compilación de Java es bastante fácil usando la API javax.tools de JDK 6.
Alan Krueger
10

El Java-Sandbox es una biblioteca para la ejecución del código Java con un conjunto limitado de permisos. Se puede utilizar para permitir el acceso solo a un conjunto de clases y recursos incluidos en la lista blanca. No parece poder restringir el acceso a métodos individuales. Utiliza un sistema con un cargador de clases personalizado y un administrador de seguridad para lograr esto.

No lo he usado pero parece bien diseñado y razonablemente bien documentado.

@waqas ha dado una respuesta muy interesante explicando cómo es posible implementarlo usted mismo. Pero es mucho más seguro dejar un código tan complejo y crítico para la seguridad en manos de los expertos.

Sin embargo, tenga en cuenta que el proyecto no se ha actualizado desde 2013 y los creadores lo describen como "experimental". Su página de inicio ha desaparecido pero la entrada de Source Forge permanece.

Código de ejemplo adaptado del sitio web del proyecto:

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

Para abordar el problema en la respuesta aceptada por la cual la costumbre SecurityManagerse aplicará a todos los subprocesos en la JVM, en lugar de por subproceso, puede crear una personalizada SecurityManagerque se pueda habilitar / deshabilitar para subprocesos específicos de la siguiente manera:

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

}

ToggleSecurirtyManagerPermissiones solo una implementación simple de java.security.Permissionpara garantizar que solo el código autorizado pueda habilitar / deshabilitar el administrador de seguridad. Se parece a esto:

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
fuente
Uso muy inteligente de ThreadLocal para hacer SecurityManagers de ámbito de sistema con alcance de subprocesos de manera efectiva (lo que la mayoría de los usuarios querrían). También considere usar InheritableThreadLocal para transmitir automáticamente la propiedad no permitida a los subprocesos generados por código que no es de confianza.
Nick
4

Bueno, es muy tarde para dar sugerencias o soluciones, pero aún así me enfrentaba a un problema similar, más orientado a la investigación. Básicamente, estaba tratando de proporcionar una provisión y evaluaciones automáticas para tareas de programación para el curso de Java en plataformas de aprendizaje electrónico.

  1. una forma podría ser, Crear máquinas virtuales separadas (no JVM) pero máquinas virtuales reales con la configuración mínima posible de SO para cada estudiante.
  2. Instale JRE para Java o bibliotecas de acuerdo con sus lenguajes de programación, lo que desee que los estudiantes compilen y ejecuten en estas máquinas.

Sé que esto suena bastante complejo y con muchas tareas, pero Oracle Virtual Box ya proporciona la API de Java para crear o clonar máquinas virtuales de forma dinámica. https://www.virtualbox.org/sdkref/index.html (Tenga en cuenta que incluso VMware también proporciona API para hacer lo mismo)

Y para el tamaño mínimo y la configuración de distribución de Linux, puede consultar este aquí http://www.slitaz.org/en/ ,

Así que ahora, si los estudiantes se equivocan o intentan hacerlo, puede ser con la memoria, el sistema de archivos o la red, el socket, como máximo, puede dañar su propia VM.

También internamente en estas VM, puede proporcionar seguridad adicional como Sandbox (administrador de seguridad) para Java o crear cuentas específicas de usuario en Linux y, por lo tanto, restringir el acceso.

Espero que esto ayude !!

Shrikant Havale
fuente
3

Aquí hay una solución segura para subprocesos para el 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.

}

¡Por favor comenta!

CU

Arno

Arno Unkrig
fuente