基于NashornSandbox实现Java中安全执行js的方案

基于NashornSandbox实现Java中安全执行js的方案

1. 场景

Java可以借助javax包接口来运行js,但是运行js时,由于js是属于用户端的输入内容,所以存在很多不确定性,很有安全隐患。
隐患例如:

  1. js代码存在死循环
  2. js代码可以操作宿主机上面的功能,删除机器上的文件
  3. js执行占用过多的java资源
    因此,我们应该使用sandbox,具体工具就是利用NashornSandbox来进行沙箱环境运行js。

2. 介绍

2.1 Nashorn的介绍

Nashorn 一个 javascript 引擎。
从JDK 1.8开始,Nashorn取代Rhino(JDK 1.6, JDK1.7)成为Java的嵌入式JavaScript引擎。Nashorn完全支持ECMAScript 5.1规范以及一些扩展。它使用基于JSR 292的新语言特性,其中包含在JDK 7中引入的 invokedynamic,将JavaScript编译成Java字节码。与先前的Rhino实现相比,这带来了2到10倍的性能提升。

2.2 NashornSandbox隔离限制

  1. 对java类的访问限制
  2. 对Nashorn引擎的资源限制

3. 实现

3.1 Maven引入

<!-- JS引擎沙盒 START -->
<delight-nashorn-sandbox.version>0.1.14</delight-nashorn-sandbox.version>
<!-- JS引擎沙盒 END -->
		      <dependency>
				<groupId>org.javadelight</groupId>
				<artifactId>delight-nashorn-sandbox</artifactId>
				<version>${delight-nashorn-sandbox.version}</version>
			</dependency>

3.2 实现类

package wang.fredia.app.api.engine;

import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;

import delight.nashornsandbox.NashornSandbox;
import delight.nashornsandbox.NashornSandboxes;
import lombok.extern.slf4j.Slf4j;

@SuppressWarnings("restriction")
@Slf4j
public abstract class AbstractNashornJsInvokeService2 extends AbstractJsInvokeService {

	private NashornSandbox sandbox;
	private ExecutorService monitorExecutorService;

	@PostConstruct
	public void init() {
			sandbox = NashornSandboxes.create();
			monitorExecutorService = Executors.newWorkStealingPool(100);
			sandbox.setExecutor(monitorExecutorService);
			sandbox.setMaxCPUTime(100L);
			sandbox.allowNoBraces(false);
			sandbox.allowLoadFunctions(true);
			sandbox.setMaxMemory(100*1024); 
			sandbox.setMaxPreparedStatements(30);
	}

	@PreDestroy
	public void stop() {
		if (monitorExecutorService != null) {
			monitorExecutorService.shutdownNow();
		}
	}


	@Override
	protected ListenableFuture<UUID> doEval(UUID scriptId, String functionName, String jsScript) {
		try {
			sandbox.eval(jsScript);
		} catch (Exception e) {
			log.warn("Failed to compile JS script: {}", e.getMessage(), e);
			return Futures.immediateFailedFuture(e);
		}
		return Futures.immediateFuture(scriptId);
	}

	@Override
	protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String functionName, Object[] args) {
		try {
			Object result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args);
			return Futures.immediateFuture(result);
		} catch (Exception e) {
			return Futures.immediateFailedFuture(e);
		}
	}
	

}

3.3 关键点配置

由于上面代码中包含了过多的细节成分,我们主要关注sandbox的初始化方法,如下

	@PostConstruct
	public void init() {
			sandbox = NashornSandboxes.create();
			monitorExecutorService = Executors.newWorkStealingPool(100);
			sandbox.setExecutor(monitorExecutorService);
			sandbox.setMaxCPUTime(100L);
			sandbox.allowNoBraces(false);
			sandbox.allowLoadFunctions(true);
			sandbox.setMaxMemory(100*1024); 
			sandbox.setMaxPreparedStatements(30);
	}

sandbox 初始化时的具体参数使用如下:

  1. setMaxCPUTime // 设置脚本执行允许的最大CPU时间(以毫秒为单位),超过则会报异常
  2. setExecutor // 指定执行程序服务,该服务用于在CPU时间运行脚本
  3. allowNoBraces // 是否允许使用大括号
  4. allowLoadFunctions // 是否允许nashorn加载全局函数
  5. setMaxPreparedStatements // LRU初缓存的初始化大小,默认为0
  6. setMaxMemory // 设置JS执行程序线程可以分配的最大内存(以字节为单位),超过会报ScriptMemoryAbuseException错误

4. 总结

因为当前执行JS的业务场景是在IoT服务内,所以用户在配置规则链时,很容易将规则链里面的Rule(js)写得有问题,这种情况下,没有稳定的隔离机制,一定会发生巨大的生产事故。为确定服务的可用性以及稳定性,必须将可能存在问题都考虑到,尤其是物联网数据监控这样的实时且准确的功能。
该文章服务该监控功能

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://blog.wyatt.plus/?p=61

Buy me a cup of coffee ☕.