web-dev-qa-db-fra.com

Exécution sécurisée de JS Nashorn

Comment exécuter en toute sécurité du code JS fourni par l'utilisateur à l'aide de Java8 Nashorn?

Le script étend certains calculs pour certains rapports basés sur des servlets. L'application compte de nombreux utilisateurs (non fiables). Les scripts doivent uniquement pouvoir accéder à un objet Java et à ceux renvoyés par les membres définis. Par défaut, les scripts peuvent instancier n'importe quelle classe à l'aide de Class.forName () (en utilisant .getClass () de l'objet fourni). Existe-t-il un moyen d'interdire l'accès à une classe Java non spécifiée explicitement par moi?

40
tom_ma

Je ai posé cette question sur la liste de diffusion de Nashorn il y a quelque temps:

Existe-t-il des recommandations sur le meilleur moyen de limiter les classes que les scripts Nashorn peuvent créer à une liste blanche? Ou bien l’approche est-elle la même que n’importe quel moteur JSR223 (chargeur de classes personnalisé Sur le constructeur ScriptEngineManager)? 

Et j'ai eu cette réponse d'un des développeurs de Nashorn:

Salut,

  • Nashorn filtre déjà les classes - uniquement les classes publiques de packages non sensibles (packages répertoriés dans package.access security Property aka 'sensitive'). La vérification de l'accès au paquet est effectuée à partir d'un contexte sans autorisation. c’est-à-dire quel que soit le paquet accessible à partir d'une classe sans autorisation sont uniquement autorisés.

  • Nashorn filtre les accès Java reflective et jsr292 - à moins que le script ait RuntimePermission ("nashorn.JavaReflection"), le script ne sera pas .__ capable de faire de la réflexion.

  • Les deux précédents nécessitent une exécution avec SecurityManager activé. Sous aucun gestionnaire de sécurité, le filtrage ci-dessus ne s'appliquera pas.

  • Vous pouvez supprimer la fonction globale Java.type et l'objet Packages (+ com, edu, Java, javafx, javax, org, JavaImporter) de la portée globale et/ou remplacez-les par les fonctions de filtrage que vous implémentez. En effet, ce sont les seuls points d'entrée d'un accès Java à partir d'un script, personnalisation de ces fonctions => filtrage des accès Java à partir de scripts.

  • Il existe une option non documentée (actuellement utilisée uniquement pour exécuter des tests test262) "--no-Java" de nashorn Shell qui effectue ce qui précède à votre place. c'est à dire., Nashorn n'initialisera pas les points d'ancrage Java dans une portée globale.

  • JSR223 ne fournit aucun hook basé sur des normes permettant de transmettre un chargeur de classes personnalisé. Cela devra peut-être être résolu dans un avenir (possible) mise à jour de jsr223.

J'espère que cela t'aides,

-Sundar

25
Kong

Ajoutée dans 1.8u40, vous pouvez utiliser ClassFilter pour limiter les classes pouvant être utilisées par le moteur.

Voici un exemple tiré de la documentation 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();
  }
}

Cet exemple affiche les éléments suivants:

C:\Java\jre8
Create file variable
Exception caught: Java.lang.RuntimeException: Java.lang.ClassNotFoundException:
Java.io.File
17
mkobit

J'ai recherché des moyens permettant aux utilisateurs d'écrire un script simple dans un bac à sable autorisant l'accès à certains objets de base fournis par mon application (de la même manière Script Google Apps travaux). Ma conclusion était que ceci est plus facile/mieux documenté avec Rhino qu'avec Nashorn. Vous pouvez:

  1. Définissez un obturateur de classe pour éviter l'accès à d'autres classes: http://codeutopia.net/blog/2009/01/02/sandboxing-rhino-in-Java/

  2. Limitez le nombre d'instructions à éviter avec observeInstructionCount: http://www-archive.mozilla.org/rhino/apidocs/org/mozilla/javascript/ContextFactory.html

Toutefois, sachez que cela n’est pas suffisant pour les utilisateurs non fiables, car ils peuvent toujours (par accident ou exprès) allouer une énorme quantité de mémoire, ce qui oblige votre machine virtuelle à générer une erreur OutOfMemoryError. Je n'ai pas encore trouvé de solution sûre à ce dernier point.

8
Tomas

Autant que je sache, vous ne pouvez pas utiliser le bac à sable Nashorn. Un utilisateur non approuvé peut exécuter les "Fonctions intégrées Nashorn supplémentaires" répertoriées ici:

https://docs.Oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/Shell.html

qui incluent "quit ()". Je l'ai testé; il sort entièrement de la machine virtuelle Java.

(En passant, dans ma configuration, les objets globaux $ ENV, $ ARG ne fonctionnaient pas, ce qui est bien.)

Si je me trompe à ce sujet, s'il vous plaît laissez un commentaire.

6
ccleve

Vous pouvez assez facilement créer une ClassFilter qui permet un contrôle fin des classes Java disponibles en JavaScript.

En suivant l'exemple de Oracle Nashorn Docs :

class MyCF implements ClassFilter {
    @Override
    public boolean exposeToScripts(String s) {
      if (s.compareTo("Java.io.File") == 0) return false;
      return true;
    }
}

J'ai emballé cette une quelques autres mesures dans une petite bibliothèque aujourd'hui: Nashorn Sandbox (sur GitHub). Prendre plaisir!

5
mxro

Le meilleur moyen de sécuriser une exécution JS à Nashorn est d'activer le SecurityManager et de laisser Nashorn refuser les opérations critiques . De plus, vous pouvez créer une classe de surveillance qui vérifie le temps d'exécution et la mémoire du script afin d'éviter des boucles infinies et une sortie illimitée. . Si vous l'exécutez dans un environnement restreint sans possibilité de configurer SecurityManager, vous pouvez penser à utiliser le ClassFilter de Nashorn pour refuser tout/accès partiel aux classes Java. En plus de cela, vous devez écraser toutes les fonctions critiques de JS (telles que quit (), etc.) . Regardez cette fonction qui gère tous ces aspects (sauf la gestion de la mémoire):

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

La fonction utilise actuellement l’arrêt fileté obsolète (). Une amélioration peut être exécutée par le JS non pas dans un thread mais dans un processus séparé.

PS: ici Nashorn est chargé par réflexion mais le code Java équivalent est également fourni dans les commentaires.

2
Damiano Falcioni

Je dirais que surcharger le classloader de la classe fournie est le moyen le plus simple de contrôler l'accès aux classes.

(Avertissement: je ne suis pas vraiment familiarisé avec la version plus récente de Java, cette réponse est peut-être obsolète)

0
Amadan

Sans l'utilisation de Security Manager, il n'est pas possible d'exécuter JavaScript en toute sécurité sur Nashorn.

Dans toutes les versions d'Oracle Hotspot incluant Nashorn, il est possible d'écrire du code JavaScript qui exécutera tout code Java/JavaScript sur cette machine virtuelle Java . À compter de janvier 2019, l'équipe de sécurité Oracle insiste sur le fait que l'utilisation de Security Manager est obligatoire.

Un des problèmes est déjà discuté dans https://github.com/javadelight/delight-nashorn-sandbox/issues/73

0
zezuha

Une bibliothèque de bac à sable externe peut être utilisée si vous ne souhaitez pas implémenter votre propre ClassLoader & SecurityManager (c'est la seule façon de procéder au bac à sable pour le moment).

J'ai essayé "The Java Sandbox" ( http://blog.datenwerke.net/p/the-Java-sandbox.html ) bien que ce soit un peu approximatif, mais cela fonctionne.

0
Edu Garcia