Sandbox contra código malicioso em um aplicativo Java

91

Em um ambiente de servidor de simulação em que os usuários têm permissão para enviar seu próprio código para ser executado pelo servidor, seria claramente vantajoso que qualquer código enviado pelo usuário fosse executado em uma sandbox, não muito diferente dos miniaplicativos dentro de um navegador. Eu queria ser capaz de aproveitar o próprio JVM, em vez de adicionar outra camada de VM para isolar esses componentes enviados.

Esse tipo de limitação parece ser possível usando o modelo de sandbox Java existente, mas há uma maneira dinâmica de habilitar isso apenas para as partes enviadas pelo usuário de um aplicativo em execução?

Alan Krueger
fonte

Respostas:

109
  1. Execute o código não confiável em seu próprio segmento. Isso, por exemplo, evita problemas com loops infinitos e outros, e torna as etapas futuras mais fáceis. Faça com que o thread principal espere até que o thread termine e, se demorar muito, elimine-o com Thread.stop. Thread.stop está obsoleto, mas como o código não confiável não deve ter acesso a nenhum recurso, seria seguro eliminá-lo.

  2. Defina um SecurityManager nesse Thread. Crie uma subclasse de SecurityManager que sobrescreve checkPermission (Permission perm) para simplesmente lançar uma SecurityException para todas as permissões, exceto algumas. Há uma lista de métodos e as permissões que eles exigem aqui: Permissões no Java TM 6 SDK .

  3. Use um ClassLoader personalizado para carregar o código não confiável. Seu carregador de classes seria chamado para todas as classes que o código não confiável usa, portanto, você pode fazer coisas como desabilitar o acesso a classes JDK individuais. A única coisa a fazer é ter uma lista branca de classes JDK permitidas.

  4. Você pode querer executar o código não confiável em uma JVM separada. Embora as etapas anteriores tornem o código seguro, há uma coisa irritante que o código isolado ainda pode fazer: alocar o máximo de memória possível, o que faz com que a pegada visível do aplicativo principal aumente.

JSR 121: Especificação de API de isolamento de aplicativo foi projetada para resolver isso, mas infelizmente ainda não tem uma implementação.

Este é um tópico bem detalhado, e principalmente estou escrevendo isso de cima da minha cabeça.

Mas de qualquer maneira, algum código imperfeito, use por sua própria conta e risco, provavelmente com erros (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
  }
}

Gerente de segurança

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
}

Fio

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
fonte
4
Esse código pode precisar de algum trabalho. Você realmente não pode se proteger contra a disponibilidade da JVM. Esteja preparado para encerrar o processo (provavelmente automaticamente). O código vai para outros threads - por exemplo, o thread finalizador. Thread.stopcausará problemas no código da biblioteca Java. Da mesma forma, o código da biblioteca Java exigirá permissões. Muito melhor permitir o SecurityManageruso java.security.AccessController. O carregador de classes provavelmente também deve permitir o acesso às próprias classes do código do usuário.
Tom Hawtin - tackline
3
Dado que este é um assunto tão complicado, não existem soluções para lidar com "plug-ins" Java de forma segura?
Nick Spacek
9
O problema dessa abordagem é que quando você define SecurityManager como System, isso não afeta apenas o encadeamento em execução, mas também impacta outro encadeamento!
Gelin Luo
2
Desculpe, mas thread.stop () pode ser capturado com lançável. Você pode while (thread.isAlive) Thread.stop (), mas então eu posso chamar recursivamente uma função que captura a exceção. Testado no meu pc, a função recursiva vence o stop (). Agora você tem um thread de lixo, roubando CPU e recursos
Lesto
8
Além do fato de que System.setSecurityManager(…)afetará toda a JVM, não apenas o encadeamento que invoca esse método, a ideia de tomar decisões de segurança com base no encadeamento foi abandonada quando o Java mudou de 1.0 para 1.1. Neste momento, foi reconhecido que o código não confiável pode invocar código confiável e vice-versa, independentemente de qual thread executa o código. Nenhum desenvolvedor deve repetir o erro.
Holger
18

Obviamente, tal esquema levanta todos os tipos de preocupações de segurança. Java tem uma estrutura de segurança rigorosa, mas não é trivial. A possibilidade de bagunçar tudo e permitir que um usuário sem privilégios acesse componentes vitais do sistema não deve ser esquecida.

Esse aviso à parte, se você estiver pegando a entrada do usuário na forma de código-fonte, a primeira coisa que você precisa fazer é compilá-la para bytecode Java. AFIAK, isso não pode ser feito nativamente, então você precisará fazer uma chamada de sistema para javac e compilar o código-fonte para bytecode no disco. Aqui está um tutorial que pode ser usado como ponto de partida para isso. Edit : como aprendi nos comentários, você realmente pode compilar o código Java da fonte nativamente usando javax.tools.JavaCompiler

Depois de ter o bytecode JVM, você pode carregá-lo na JVM usando a função defineClass de um ClassLoader . Para definir um contexto de segurança para esta classe carregada, você precisará especificar um ProtectionDomain . O construtor mínimo para um ProtectionDomain requer um CodeSource e um PermissionCollection . O PermissionCollection é o objeto de uso principal para você aqui - você pode usá-lo para especificar as permissões exatas que a classe carregada possui. Essas permissões devem ser aplicadas pelo AccessController da JVM .

Há muitos pontos de erro possíveis aqui, e você deve ser extremamente cuidadoso para entender tudo completamente antes de implementar qualquer coisa.

shsmurfy
fonte
2
A compilação Java é muito fácil usando a API javax.tools do JDK 6.
Alan Krueger
10

O Java-Sandbox é uma biblioteca para execução de código Java com um conjunto limitado de permissões. Ele pode ser usado para permitir o acesso a apenas um conjunto de classes e recursos da lista branca. Não parece ser capaz de restringir o acesso a métodos individuais. Ele usa um sistema com um carregador de classes e gerenciador de segurança personalizados para fazer isso.

Não o usei, mas parece bem desenhado e razoavelmente bem documentado.

@waqas deu uma resposta muito interessante explicando como isso é possível implementar você mesmo. Mas é muito mais seguro deixar esse código complexo e crítico de segurança para especialistas.

Observe, porém, que o projeto não é atualizado desde 2013 e os criadores o descrevem como "experimental". Sua página inicial desapareceu, mas a entrada do Source Forge permanece.

Exemplo de código adaptado do site do projeto:

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

Para resolver o problema na resposta aceita em que o customizado SecurityManagerse aplicará a todos os threads na JVM, em vez de por thread, você pode criar um custom SecurityManagerque pode ser ativado / desativado para threads específicos da seguinte maneira:

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é apenas uma implementação simples de java.security.Permissionpara garantir que apenas o código autorizado pode ativar / desativar o gerenciador de segurança. Se parece com isso:

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
fonte
Uso muito inteligente de ThreadLocal para tornar SecurityManagers com escopo de sistema efetivamente escopo de thread (o que a maioria dos usuários gostariam). Considere também o uso de InheritableThreadLocal para transmitir automaticamente a propriedade não permitida para threads gerados por código não confiável.
Nick
4

Bem, é muito tarde para dar sugestões ou soluções, mas mesmo assim eu estava enfrentando um problema semelhante, mais voltado para a pesquisa. Basicamente, eu estava tentando fornecer uma provisão e avaliações automáticas para atribuições de programação para o curso Java em plataformas de e-learning.

  1. uma maneira poderia ser, criar máquinas virtuais separadas (não JVM), mas máquinas virtuais reais com o sistema operacional mínimo possível para cada aluno.
  2. Instale JRE para Java ou bibliotecas de acordo com suas linguagens de programação, o que você quiser que os alunos compilem e executem nessas máquinas.

Eu sei que isso parece muito complexo e com muitas tarefas, mas o Oracle Virtual Box já fornece API Java para criar ou clonar máquinas virtuais dinamicamente. https://www.virtualbox.org/sdkref/index.html (observe que até o VMware também fornece API para fazer o mesmo)

E para o tamanho mínimo e a configuração da distribuição do Linux, você pode consultar este aqui http://www.slitaz.org/en/ ,

Portanto, agora se os alunos bagunçarem ou tentarem fazer isso, pode ser com memória ou sistema de arquivos ou rede, soquete, no máximo ele pode danificar sua própria VM.

Também internamente nessas VMs, você pode fornecer segurança adicional como Sandbox (gerenciador de segurança) para Java ou criar contas específicas de usuário no Linux e, assim, restringir o acesso.

Espero que isto ajude !!

Shrikant Havale
fonte
3

Esta é uma solução thread-safe para o 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 comente!

CU

Arno

Arno Unkrig
fonte