如何使用 Java8 Nashorn 安全地执行一些用户提供的 JS 代码?
该脚本为一些基于 servlet 的报告扩展了一些计算。该应用程序有许多不同的(不受信任的)用户。脚本应该只能访问 Java 对象以及由定义的成员返回的对象。默认情况下,脚本可以使用 Class.forName() 实例化任何类(使用我提供的对象的 .getClass() )。有什么方法可以禁止访问我没有明确指定的任何 java 类?
如何使用 Java8 Nashorn 安全地执行一些用户提供的 JS 代码?
该脚本为一些基于 servlet 的报告扩展了一些计算。该应用程序有许多不同的(不受信任的)用户。脚本应该只能访问 Java 对象以及由定义的成员返回的对象。默认情况下,脚本可以使用 Class.forName() 实例化任何类(使用我提供的对象的 .getClass() )。有什么方法可以禁止访问我没有明确指定的任何 java 类?
不久前,我在 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 shell 可以为您完成上述操作。即,Nashorn 不会在全局范围内初始化 Java 挂钩。
JSR223 不提供任何基于标准的挂钩来传递自定义类加载器。这可能必须在 jsr223 的(可能的)未来更新中解决。
希望这可以帮助,
-桑达
在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 更容易/更好地记录这一点。你可以:
定义一个类快门以避免访问其他类:http ://codeutopia.net/blog/2009/01/02/sandboxing-rhino-in-java/
通过 observeInstructionCount 限制指令的数量以避免 endess-loops:http ://www-archive.mozilla.org/rhino/apidocs/org/mozilla/javascript/ContextFactory.html
但是请注意,对于不受信任的用户,这还不够,因为他们仍然可以(偶然或故意)分配大量内存,导致您的 JVM 抛出 OutOfMemoryError。我还没有找到最后一点的安全解决方案。
您可以很容易地创建一个ClassFilter
允许对 JavaScript 中可用的 Java 类进行细粒度控制的方法。
按照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。不受信任的用户可以执行此处列出的“其他 Nashorn 内置函数”:
https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/shell.html
其中包括“退出()”。我测试了它;它完全退出JVM。
(顺便说一句,在我的设置中,全局对象 $ENV、$ARG 不起作用,这很好。)
如果我对此有误,请有人发表评论。
The best way to secure a JS execution in Nashorn is to enable the SecurityManager and let Nashorn deny the critical operations. In addition you can create a monitoring class that check the script execution time and memory in order to avoid infinite loops and outOfMemory. In case you run it in a restricted environment without possibility to setup the SecurityManager, you can think to use the Nashorn ClassFilter to deny all/partial access to the Java classes. In addition to that you must overwrite all the critical JS functions (like quit() etc.). Have a look at this function that manage all this aspects (except memory management):
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);
}
}
The function currently use the deprecated Thread stop(). An improvement can be execute the JS not in a Thread but in a separate Process.
PS: here Nashorn is loaded through reflexion but the equivalent Java code is also provided in the comments
如果您不想实现自己的 ClassLoader 和 SecurityManager(这是目前唯一的沙箱方式),可以使用外部沙箱库。
我已经尝试过“Java 沙盒”(http://blog.datenwerke.net/p/the-java-sandbox.html),虽然它的边缘有点粗糙,但它确实有效。
我想说覆盖提供的类的类加载器是控制对类的访问的最简单方法。
(免责声明:我对较新的 Java 不是很熟悉,所以这个答案可能是老派/过时的)
如果不使用安全管理器,就不可能在 Nashorn 上安全地执行 JavaScript。
在包括 Nashorn 的所有 Oracle Hotspot 版本中,人们都可以编写 JavaScript 来在这个 JVM 上执行任何 Java/JavaScript 代码。自 2019 年 1 月起,Oracle 安全团队坚持使用安全管理器是强制性的。
其中一个问题已经在https://github.com/javadelight/delight-nashorn-sandbox/issues/73中讨论过