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

Как можно безопасно выполнить некоторый пользовательский код JS, используя Java8 Nashorn?

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

8 ответов

Я задал этот вопрос в списке рассылки Nashorn некоторое время назад:

Существуют ли какие-либо рекомендации для лучшего способа ограничения классов, которые сценарии Nashorn могут создавать, в белый список? Или подход такой же, как у любого движка JSR223 (пользовательский загрузчик классов в конструкторе ScriptEngineManager)?

И получил ответ от одного из разработчиков Nashorn:

Привет,

  • Nashorn уже фильтрует классы - только общедоступные классы нечувствительных пакетов (пакеты перечислены в свойстве безопасности package.access, называемом "чувствительным"). Проверка доступа к пакету выполняется из контекста отсутствия прав. т. е. любой пакет, к которому можно получить доступ из класса без прав доступа, разрешен только.

  • Nashorn фильтрует рефлексивный Java и доступ jsr292 - если скрипт не имеет RuntimePermission("nashorn.JavaReflection"), скрипт не сможет выполнять рефлексию.

  • Вышеуказанные два требуют работы с включенным SecurityManager. При отсутствии диспетчера безопасности вышеупомянутая фильтрация не применяется.

  • Вы можете удалить глобальную функцию Java.type и объект Packages (+ com,edu,java,javafx,javax,org,JavaImporter) в глобальной области видимости и / или заменить их на любые функции фильтрации, которые вы реализуете. Потому что это единственные точки входа в доступ к Java из сценариев, которые настраивают эти функции => фильтрацию доступа к Java из сценариев.

  • Существует недокументированная опция (в настоящее время используется только для запуска тестов test262) "--no-java" оболочки nashorn, которая выполняет вышеуказанное за вас. то есть Nashorn не будет инициализировать Java-хуки в глобальной области видимости.

  • JSR223 не предоставляет никаких основанных на стандартах хуков для передачи пользовательского загрузчика классов. Это может быть решено в (возможном) будущем обновлении jsr223.

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

-Sundar

Добавлено в 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

Я исследовал способы, позволяющие пользователям писать простой сценарий в песочнице, которому разрешен доступ к некоторым базовым объектам, предоставленным моим приложением (так же, как работает скрипт Google Apps). Мой вывод состоял в том, что это легче / лучше задокументировано с Rhino, чем с Nashorn. Вы можете:

  1. Определите класс-затвор, чтобы избежать доступа к другим классам: http://codeutopia.net/blog/2009/01/02/sandboxing-rhino-in-java/

  2. Ограничьте количество инструкций, чтобы избежать циклов endess с помощью followInstructionCount: http://www-archive.mozilla.org/rhino/apidocs/org/mozilla/javascript/ContextFactory.html

Однако имейте в виду, что для ненадежных пользователей этого недостаточно, поскольку они все еще могут (случайно или намеренно) выделить огромный объем памяти, в результате чего ваша JVM выдает ошибку OutOfMemoryError. Я еще не нашел безопасного решения этого последнего вопроса.

Вы можете довольно легко создать 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). Наслаждайтесь!

Насколько я могу судить, ты не можешь песочницей, Нашорн. Ненадежный пользователь может выполнить "Дополнительные встроенные функции Nashorn", перечисленные здесь:

https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/shell.html

которые включают "quit()". Я проверил это; он полностью выходит из JVM.

(Кроме того, в моей настройке глобальные объекты, $ENV, $ARG, не работали, что хорошо.)

Если я ошибаюсь, кто-то, пожалуйста, оставьте комментарий.

Лучший способ обезопасить выполнение 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 также предоставляется в комментариях

Я бы сказал, что переопределение загрузчика классов предоставленного класса - это самый простой способ управления доступом к классам.

(Отказ от ответственности: я не очень знаком с новой Java, поэтому этот ответ может быть старой школы / устарел)

Без использования 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

Внешнюю библиотеку песочницы можно использовать, если вы не хотите реализовывать свой собственный ClassLoader & SecurityManager (пока это единственный способ песочницы).

Я пробовал "Песочницу Java" ( http://blog.datenwerke.net/p/the-java-sandbox.html), хотя она немного грубовата, но работает.

Другие вопросы по тегам