Песочница против вредоносного кода в приложении 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 "";
}
}
Ответ 7
Вам, вероятно, понадобится использовать SecurityManger и/или AccessController. Для получения подробной информации см. Архитектура безопасности Java и другая документация по безопасности от Солнца.