Подтвердить что ты не робот

Безопасное выполнение Nashorn JS

Как я могу безопасно выполнить какой-то пользовательский JS-код с помощью Java8 Nashorn?

script расширяет некоторые вычисления для некоторых отчетов на основе сервлетов. В приложении много разных (ненадежных) пользователей. Сценарии должны иметь доступ только к объекту Java и к тем, которые возвращаются определенными членами. По умолчанию скрипты могут создавать экземпляр любого класса, используя Class.forName() (используя .getClass() моего поставленного объекта). Есть ли способ запретить доступ к любому классу java, явно не указанному мной?

4b9b3361

Ответ 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