如何使用Java8 Nashorn安全地执行一些用户提供的JS代码?
该脚本为一些基于servlet的报告扩展了一些计算。该应用程序有许多不同的(不受信任的)用户。脚本只能访问Java对象和定义成员返回的对象。默认情况下,脚本可以使用Class.forName()实例化任何类(使用我提供的对象的.getClass())。有没有办法禁止访问我未明确指定的任何Java类?
如何使用Java8 Nashorn安全地执行一些用户提供的JS代码?
该脚本为一些基于servlet的报告扩展了一些计算。该应用程序有许多不同的(不受信任的)用户。脚本只能访问Java对象和定义成员返回的对象。默认情况下,脚本可以使用Class.forName()实例化任何类(使用我提供的对象的.getClass())。有没有办法禁止访问我未明确指定的任何Java类?
我在一段时间前在Nashorn邮件列表中提出了这个问题:
有没有什么建议,可以将Nashorn脚本创建的类限制为白名单?还是与任何JSR223引擎的方法相同(在ScriptEngineManager构造函数上使用自定义类加载器)?
然后从Nashorn开发人员那里得到了这个答案:
你好,--no-java
(以及其他选项)传递给引擎:final ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine(new String[] { "--no-java" });
- ach从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(); } }
This example prints the following:
C:\Java\jre8 Create file variable Exception caught: java.lang.RuntimeException: java.lang.ClassNotFoundException: java.io.File
我研究了允许用户在沙盒中编写简单脚本并允许访问应用程序提供的一些基本对象的方法(类似于Google Apps Script的工作方式)。我的结论是,使用Rhino比Nashorn更容易/更好地记录文档。您可以:
定义类关闭器以避免访问其他类:http://codeutopia.net/blog/2009/01/02/sandboxing-rhino-in-java/
通过observeInstructionCount限制指令的数量,以避免无限循环:http://www-archive.mozilla.org/rhino/apidocs/org/mozilla/javascript/ContextFactory.html
但是请注意,对于不受信任的用户,这还不够,因为他们仍然可以(无意或有意)分配大量内存,导致您的JVM抛出OutOfMemoryError。我尚未找到解决此问题的安全解决方案。
您可以很容易地创建一个ClassFilter
,以对JavaScript中可用的Java类进行细粒度控制。
根据Oracle Nashorn文档中的示例:
class MyCF implements ClassFilter {
@Override
public boolean exposeToScripts(String s) {
if (s.compareTo("java.io.File") == 0) return false;
return true;
}
}
今天我将这个以及其他一些措施封装成了一个小型库:Nashorn沙箱(在GitHub上)。享受吧!
Class.forName
?你怎么能阻止它? - TheRealChx101quit
。但是似乎只需在用户提供的脚本前加上以下类似的前缀即可解决问题:var quit=function(){throw 'Unsupported operation: quit';};var exit=function(){throw 'Unsupported operation: exit';};
我还会禁用 print
和大多数其他全局函数,因为它们也可能会造成干扰或导致其他安全问题(例如,load
可以用于测试服务器上的文件是否存在)。 - Christian Semraupublic 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);
}
}
该函数目前使用已弃用的Thread stop()。改进方法可以将JS不在Thread中执行,而是在单独的进程中执行。
PS:这里通过反射加载了Nashorn,但等效的Java代码也在注释中提供了。
如果不使用安全管理器,就无法在Nashorn上安全地执行JavaScript。
在包含Nashorn的所有Oracle Hotspot版本中,都可以编写JavaScript代码,在此JVM上执行任何Java/JavaScript代码。 截至2019年1月,Oracle安全团队坚持要求使用安全管理器。
其中一个问题已经在https://github.com/javadelight/delight-nashorn-sandbox/issues/73中讨论过了。
如果您不想实现自己的ClassLoader和SecurityManager(这是目前唯一的沙箱方法),可以使用外部沙箱库。
我尝试过“Java沙箱”(http://blog.datenwerke.net/p/the-java-sandbox.html),虽然它有点不太完善,但它可以使用。
我认为覆盖提供的类的类加载器是控制访问类的最简单方法。
(免责声明:我不太熟悉较新的Java,因此这个答案可能有点老派/过时)