Песочница против вредоносного кода в приложении Java

В среде сервера моделирования, где пользователям разрешено отправлять свой собственный код для запуска сервером, было бы явно выгодно, чтобы любой пользовательский код запускался на стороне изолированной программной среды, в отличие от апплетов в браузере, Я хотел бы иметь возможность использовать JVM самостоятельно, вместо того, чтобы добавлять другой уровень VM для изоляции этих представленных компонентов.

Такое ограничение, по-видимому, возможно с использованием существующей модели Java-песочницы, но существует ли динамический способ включения этого только для частей приложения, запущенных пользователем?

Ответ 1

  • Запустите ненадежный код в своем потоке. Это, например, предотвращает проблемы с бесконечными циклами и т.д. И облегчает будущие шаги. Попросите главный поток подождать, пока поток завершится, и если он затянется слишком долго, убейте его с помощью Thread.stop. Thread.stop устарел, но поскольку ненадежный код не должен иметь доступа к каким-либо ресурсам, было бы безопасно его убить.

  • Установите SecurityManager в этой теме. Создайте подкласс SecurityManager, который переопределяет checkPermission (Permission perm), чтобы просто выбросить SecurityException для всех разрешений, кроме нескольких избранных. Там список методов и разрешений, которые они требуют здесь: Разрешения в SDK Java TM 6.

  • Используйте пользовательский ClassLoader для загрузки ненадежного кода. Ваш загрузчик классов будет вызван для всех классов, которые использует ненадежный код, поэтому вы можете делать такие вещи, как отключить доступ к отдельным классам JDK. Для этого нужно иметь белый список разрешенных классов JDK.

  • Возможно, вы захотите запустить ненадежный код в отдельной JVM. Хотя предыдущие шаги сделают код безопасным, есть одна досаждающая вещь, которую может по-прежнему выполнять изолированный код: выделить как можно больше памяти, что приведет к увеличению видимого следа основного приложения.

JSR 121: Спецификация API изоляции приложений была разработана для решения этой проблемы, но, к сожалению, она еще не реализована.

Это довольно подробная тема, и я в основном пишу это все с головы.

Но, в любом случае, какой-то несовершенный, используемый для вашего собственного риска, вероятно, глючный (псевдо) код:

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

SecurityManager

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
}

Тема

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 main method for example:
      loader.loadClass("customclassname")
        .getMethod("main", String[].class)
        .invoke(null, new Object[]{...});
    } catch (Throwable t) {}
  }
}

Ответ 2

Очевидно, что такая схема вызывает всевозможные проблемы безопасности. Java имеет строгую систему безопасности, но это не тривиально. Не следует упускать из виду возможность его завинчивания и предоставления непривилегированному пользователю доступа к жизненно важным компонентам системы.

Это предупреждение в сторону, если вы принимаете пользовательский ввод в форме исходного кода, первое, что вам нужно сделать, это скомпилировать его в байт-код Java. AFIAK, это невозможно сделать изначально, поэтому вам нужно будет сделать системный вызов javac и скомпилировать исходный код на байт-код на диске. Вот учебник, который можно использовать в качестве отправной точки для этого. Изменить: как я узнал в комментариях, вы действительно можете скомпилировать Java-код из исходного кода, используя javax.tools.JavaCompiler

Как только вы используете байт-код JVM, вы можете загрузить его в JVM с помощью ClassLoader defineClass. Чтобы установить контекст безопасности для этого загруженного класса, вам необходимо указать ProtectionDomain. Минимальный конструктор для ProtectionDomain требует как CodeSource, так и PermissionCollection. PermissionCollection является объектом первичного использования для вас здесь - вы можете использовать его для указания точных разрешений, которые имеет загруженный класс. Эти разрешения должны быть в конечном счете применены JVM AccessController.

Здесь много возможных ошибок, и вы должны быть предельно осторожны, чтобы полностью понять все, прежде чем что-либо реализовать.

Ответ 3

Java-Sandbox - это библиотека для выполнения Java-кода с ограниченным набором разрешений. Он может использоваться для доступа только к набору классов и ресурсов, перечисленных в белом списке. Это не кажется чтобы иметь возможность ограничить доступ к отдельным методам. Он использует систему с пользовательским загрузчиком классов и для достижения этой цели.

Я не использовал его, но он выглядит хорошо продуманным и достаточно хорошо документированным.

@waqas дал очень интересный ответ, объясняющий, как это можно реализовать самостоятельно. Но гораздо безопаснее оставлять критический и сложный код безопасности экспертам.

Обратите внимание, что проект не обновлялся с 2013 года, а создатели описывают его как "экспериментальный". Его домашняя страница исчезла, но запись Source Forge остается.

Пример кода, адаптированного с веб-сайта проекта:

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

Ну, очень поздно давать какие-либо предложения или решения, но все же я столкнулся с подобной проблемой, более ориентированной на исследования. В основном я пытался обеспечить предоставление и автоматическую оценку назначений программирования для курса Java на платформах электронного обучения.

  • Один способ может быть, Создайте отдельные виртуальные машины (не JVM), а виртуальные виртуальные машины с минимальной возможной конфигурацией ОС для каждого ученика.
  • Установите JRE для Java или библиотек в соответствии с вашими языками программирования, в зависимости от того, что вы хотите, чтобы ученики компилировались и выполнялись на этих машинах.

Я знаю, что это звучит довольно сложно и много задач, но Oracle Virtual Box уже предоставляет Java API для динамического создания или клонирования виртуальных машин. https://www.virtualbox.org/sdkref/index.html (Обратите внимание, что даже VMware также предоставляет API для этого)

А для минимального размера и конфигурации дистрибутива Linux вы можете обратиться к этому здесь http://www.slitaz.org/en/,

Итак, теперь, если учащиеся запутываются или пытаются это сделать, могут быть с памятью или файловой системой или сетью, сокет, максимум он может повредить собственную виртуальную машину.

Также внутри этой виртуальной машины вы можете предоставить дополнительную безопасность, такую ​​как Sandbox (менеджер безопасности) для Java или создание пользовательских учетных записей в Linux и, таким образом, ограничение доступа.

Надеюсь, это поможет!

Ответ 5

Здесь существует потокобезопасное решение проблемы:

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

}

Прокомментируйте!

CU

Арно

Ответ 6

Чтобы решить проблему в принятом ответе, в результате чего пользовательский SecurityManager будет применяться ко всем потокам в JVM, а не по каждому потоку, вы можете создать пользовательский SecurityManager, который можно включить/отключить для конкретных потоков следующим образом:

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 - это просто реализация java.security.Permission, чтобы гарантировать, что только авторизованный код может включать/отключать диспетчера безопасности. Это выглядит так:

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

}