Безопасное выполнение Nashorn JS
Как я могу безопасно выполнить какой-то пользовательский JS-код с помощью Java8 Nashorn?
script расширяет некоторые вычисления для некоторых отчетов на основе сервлетов. В приложении много разных (ненадежных) пользователей. Сценарии должны иметь доступ только к объекту Java и к тем, которые возвращаются определенными членами. По умолчанию скрипты могут создавать экземпляр любого класса, используя Class.forName() (используя .getClass() моего поставленного объекта). Есть ли способ запретить доступ к любому классу java, явно не указанному мной?
Ответы
Ответ 1
I задал этот вопрос в списке рассылки Nashorn:
Есть ли какие-либо рекомендации по наилучшему способу ограничить классы, которые скрипты Nashorn могут создать в белый список? Или подход такой же, как любой движок JSR223 (пользовательский загрузчик классов в конструкторе ScriptEngineManager)?
И получил этот ответ от одного из разработчиков Nashorn:
Привет,
-
Nashorn уже фильтрует классы - только общедоступные классы нечувствительных пакетов (пакеты, перечисленные в security.access security свойство aka 'sensitive'). Проверка доступа к пакетам осуществляется с контекст без разрешений. то есть любой пакет, к которому можно получить доступ из класса без полномочий разрешены.
-
Nashorn фильтрует Java-отражающий и jsr292-доступ - если script не имеет RuntimePermission ( "nashorn.JavaReflection" ), script не будет способный отражать.
-
Вышеупомянутые два требуют запуска с включенным SecurityManager. Не имея никакого менеджера безопасности, вышеуказанная фильтрация не будет применяться.
-
Вы можете удалить глобальную функцию Java.type и объект Packages (+ com, edu, java, javafx, javax, org, JavaImporter) в глобальной области действия и/или замените их любыми функциями фильтрации, которые вы реализуете. Поскольку они являются единственными входными точками доступа Java из script, настройка этих функций = > фильтрация доступа Java из сценариев.
-
Существует недокументированная опция (теперь используется только для запуска тестов test262) "--no-java" оболочки nashorn, которая делает это выше для вас. т.е. Nashorn не будет инициализировать Java-перехватчики в глобальной области.
-
JSR223 не предоставляет никаких оснований на основе стандартов для передачи пользовательского загрузчика классов. Это, возможно, придется решать в (возможном) будущем обновление jsr223.
Надеюсь, что это поможет,
-Sundar
Ответ 2
Добавленный в 1.8u40, вы можете использовать ClassFilter
, чтобы ограничить классы, которые может использовать движок.
Вот пример из документации Oracle:
import javax.script.ScriptEngine;
import jdk.nashorn.api.scripting.ClassFilter;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
public class MyClassFilterTest {
class MyCF implements ClassFilter {
@Override
public boolean exposeToScripts(String s) {
if (s.compareTo("java.io.File") == 0) return false;
return true;
}
}
public void testClassFilter() {
final String script =
"print(java.lang.System.getProperty(\"java.home\"));" +
"print(\"Create file variable\");" +
"var File = Java.type(\"java.io.File\");";
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
ScriptEngine engine = factory.getScriptEngine(
new MyClassFilterTest.MyCF());
try {
engine.eval(script);
} catch (Exception e) {
System.out.println("Exception caught: " + e.toString());
}
}
public static void main(String[] args) {
MyClassFilterTest myApp = new MyClassFilterTest();
myApp.testClassFilter();
}
}
В этом примере печатается следующее:
C:\Java\jre8
Create file variable
Exception caught: java.lang.RuntimeException: java.lang.ClassNotFoundException:
java.io.File
Ответ 3
Я исследовал способы разрешить пользователям писать простой script в песочнице, которым разрешен доступ к некоторым базовым объектам, предоставленным моим приложением (таким же образом Google Apps Script). Я пришел к выводу, что это легче/лучше документировано с Rhino, чем с Нашорном. Вы можете:
Однако следует предупредить, что с ненадежными пользователями этого недостаточно, поскольку они могут (случайно или по назначению) выделять объем памяти hugh, заставляя вашу JVM выкидывать OutOfMemoryError. Я еще не нашел безопасного решения для этой последней точки.
Ответ 4
Насколько я могу судить, ты не можешь песочницей, Нашорн. Ненадежный пользователь может выполнить "Дополнительные встроенные функции Nashorn", перечисленные здесь:
https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/shell.html
которые включают "quit()". Я проверил это; он полностью выходит из JVM.
(Кроме того, в моей настройке глобальные объекты, $ ENV, $ ARG, не работали, что хорошо.)
Если я ошибаюсь, кто-то, пожалуйста, оставьте комментарий.
Ответ 5
Вы можете довольно легко создать ClassFilter
который позволяет детально контролировать, какие классы Java доступны в JavaScript.
Следуя примеру из Oracle Nashorn Docs:
class MyCF implements ClassFilter {
@Override
public boolean exposeToScripts(String s) {
if (s.compareTo("java.io.File") == 0) return false;
return true;
}
}
Сегодня я поместил несколько других мер в небольшую библиотеку: Nashorn Sandbox (на GitHub). Наслаждайтесь!
Ответ 6
Лучший способ обезопасить выполнение JS в Nashorn - это включить SecurityManager и позволить Nashorn отрицать критические операции. Кроме того, вы можете создать класс мониторинга, который проверяет время выполнения скрипта и память, чтобы избежать бесконечных циклов и outOfMemory. Если вы запускаете его в ограниченной среде без возможности настройки SecurityManager, вы можете использовать Nashorn ClassFilter, чтобы запретить полный/частичный доступ к классам Java. В дополнение к этому вы должны перезаписать все критические функции JS (например, quit() и т.д.). Посмотрите на эту функцию, которая управляет всеми этими аспектами (кроме управления памятью):
public static Object javascriptSafeEval(HashMap<String, Object> parameters, String algorithm, boolean enableSecurityManager, boolean disableCriticalJSFunctions, boolean disableLoadJSFunctions, boolean defaultDenyJavaClasses, List<String> javaClassesExceptionList, int maxAllowedExecTimeInSeconds) throws Exception {
System.setProperty("java.net.useSystemProxies", "true");
Policy originalPolicy = null;
if(enableSecurityManager) {
ProtectionDomain currentProtectionDomain = this.getClass().getProtectionDomain();
originalPolicy = Policy.getPolicy();
final Policy orinalPolicyFinal = originalPolicy;
Policy.setPolicy(new Policy() {
@Override
public boolean implies(ProtectionDomain domain, Permission permission) {
if(domain.equals(currentProtectionDomain))
return true;
return orinalPolicyFinal.implies(domain, permission);
}
});
}
try {
SecurityManager originalSecurityManager = null;
if(enableSecurityManager) {
originalSecurityManager = System.getSecurityManager();
System.setSecurityManager(new SecurityManager() {
//allow only the opening of a socket connection (required by the JS function load())
@Override
public void checkConnect(String host, int port, Object context) {}
@Override
public void checkConnect(String host, int port) {}
});
}
try {
ScriptEngine engineReflex = null;
try{
Class<?> nashornScriptEngineFactoryClass = Class.forName("jdk.nashorn.api.scripting.NashornScriptEngineFactory");
Class<?> classFilterClass = Class.forName("jdk.nashorn.api.scripting.ClassFilter");
engineReflex = (ScriptEngine)nashornScriptEngineFactoryClass.getDeclaredMethod("getScriptEngine", new Class[]{Class.forName("jdk.nashorn.api.scripting.ClassFilter")}).invoke(nashornScriptEngineFactoryClass.newInstance(), Proxy.newProxyInstance(classFilterClass.getClassLoader(), new Class[]{classFilterClass}, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("exposeToScripts")) {
if(javaClassesExceptionList != null && javaClassesExceptionList.contains(args[0]))
return defaultDenyJavaClasses;
return !defaultDenyJavaClasses;
}
throw new RuntimeException("no method found");
}
}));
/*
engine = new jdk.nashorn.api.scripting.NashornScriptEngineFactory().getScriptEngine(new jdk.nashorn.api.scripting.ClassFilter() {
@Override
public boolean exposeToScripts(String arg0) {
...
}
});
*/
}catch(Exception ex) {
throw new Exception("Impossible to initialize the Nashorn Engine: " + ex.getMessage());
}
final ScriptEngine engine = engineReflex;
if(parameters != null)
for(Entry<String, Object> entry : parameters.entrySet())
engine.put(entry.getKey(), entry.getValue());
if(disableCriticalJSFunctions)
engine.eval("quit=function(){throw 'quit() not allowed';};exit=function(){throw 'exit() not allowed';};print=function(){throw 'print() not allowed';};echo=function(){throw 'echo() not allowed';};readFully=function(){throw 'readFully() not allowed';};readLine=function(){throw 'readLine() not allowed';};$ARG=null;$ENV=null;$EXEC=null;$OPTIONS=null;$OUT=null;$ERR=null;$EXIT=null;");
if(disableLoadJSFunctions)
engine.eval("load=function(){throw 'load() not allowed';};loadWithNewGlobal=function(){throw 'loadWithNewGlobal() not allowed';};");
//nashorn-polyfill.js
engine.eval("var global=this;var window=this;var process={env:{}};var console={};console.debug=print;console.log=print;console.warn=print;console.error=print;");
class ScriptMonitor{
public Object scriptResult = null;
private boolean stop = false;
Object lock = new Object();
@SuppressWarnings("deprecation")
public void startAndWait(Thread threadToMonitor, int secondsToWait) {
threadToMonitor.start();
synchronized (lock) {
if(!stop) {
try {
if(secondsToWait<1)
lock.wait();
else
lock.wait(1000*secondsToWait);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
if(!stop) {
threadToMonitor.interrupt();
threadToMonitor.stop();
throw new RuntimeException("Javascript forced to termination: Execution time bigger then " + secondsToWait + " seconds");
}
}
public void stop() {
synchronized (lock) {
stop = true;
lock.notifyAll();
}
}
}
final ScriptMonitor scriptMonitor = new ScriptMonitor();
scriptMonitor.startAndWait(new Thread(new Runnable() {
@Override
public void run() {
try {
scriptMonitor.scriptResult = engine.eval(algorithm);
} catch (ScriptException e) {
throw new RuntimeException(e);
} finally {
scriptMonitor.stop();
}
}
}), maxAllowedExecTimeInSeconds);
Object ret = scriptMonitor.scriptResult;
return ret;
} finally {
if(enableSecurityManager)
System.setSecurityManager(originalSecurityManager);
}
} finally {
if(enableSecurityManager)
Policy.setPolicy(originalPolicy);
}
}
В настоящее время функция использует устаревший поток остановки(). Улучшение может быть выполнено JS не в потоке, а в отдельном процессе.
PS: здесь Nashorn загружается с помощью рефлексии, но эквивалентный код Java также предоставляется в комментариях
Ответ 7
Я бы сказал, что переопределение поставляемого класса classloader - самый простой способ контролировать доступ к классам.
(Отказ от ответственности: я не очень хорошо знаком с более новой Java, поэтому этот ответ может быть устаревшим или старым)
Ответ 8
Внешнюю библиотеку песочницы можно использовать, если вы не хотите реализовывать свой собственный ClassLoader и SecurityManager (это единственный способ песочницы сейчас).
Я пробовал "Песочницу Java" (http://blog.datenwerke.net/p/the-java-sandbox.html), хотя он немного груб по краям, но он работает.
Ответ 9
Без использования Security Manager невозможно безопасно выполнить JavaScript на Nashorn.
Во всех выпусках Oracle Hotspot, включающих Nashorn, можно написать JavaScript, который будет выполнять любой код Java/JavaScript на этой JVM. С января 2019 года Oracle Security Team настаивает на том, что использование Security Manager является обязательным.
Одна из проблем уже обсуждалась в https://github.com/javadelight/delight-nashorn-sandbox/issues/73