在Java中安全运行用户的groovy代码,会遇到很多挑战
1,如何在Java中运行groovy代码
pom.xml中加入依赖
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
<version>4.0.8</version>
</dependency>
Java代码中常用的运行groovy方法有
// 执行代码字符串
GroovyShell groovyShell = new GroovyShell();
groovyShell.evaluate("println 'Hello World!'");
// 执行groovy文件
GroovyShell groovyShell = new GroovyShell();
groovyShell.evaluate(new File("/path/to/groovy/test.groovy"));
// 先解析groovy文件,再运行其中的方法
GroovyShell groovyShell = new GroovyShell();
Script script = groovyShell.parse(new File("/path/to/groovy/test.groovy"));
Object[] args = {};
script.invokeMethod("run", args);
除了GroovyShell,groovy官方还提供了GroovyClassLoader和GroovyScriptEngine等其他执行groovy代码的入口
2,会有哪些危险的代码
因为groovy的强大,使得groovy可以非常方便地调用java的类和操作系统命令
// 1,直接退出Java进程
java.lang.System.exit(0);
// 2,执行Shell命令
java.lang.Runtime.getRuntime().exec("whoami");
// 3,调用java.io.File相关方法操作系统文件
def list = new java.io.File("/").list();
println list
// 4,直接调用shell命令
def cmd = "whoami"
println cmd.execute().text
// 5,使用AST来执行shell命令
@groovy.transform.ASTTest(value={
assert java.lang.Runtime.getRuntime().exec("whoami")
})
def x
// 6,无限循环
while(true) {
println "xxx"
}
// 7,长时间sleep
sleep 100000
// 8,申请大块内存空间
def b = new byte[1024*1024*1024]
println b.length
由于本人使用groovy时间尚短,不确定以上代码没有枚举完所有类型的危险代码.如果有groovy大佬发现其他类型的危险代码,欢迎在评论区留言
为了避免执行以上的危险代码,必须给强大的groovy来一次能力阉割
3,groovy自定义安全配置
groovy官方提供了自定义的安全配置功能,来限制groovy的能力
3.1,SecureASTCustomizer
GroovyShell在执行groovy代码前,会先将groovy代码编译成java class,然后在JVM中执行.
而groovy官方提供的SecureASTCustomizer可以在编译阶段对危险代码进行一次过滤,利用SecureASTCustomizer,我们可以
- 指定import的黑名单或白名单(二者只能选其一),支持星号(如 groovy.json.*)
- 语法关键词的黑名单或白名单(二者只能选其一)
- 其他语法开关,如是否允许闭包,是否允许定义方法等
因为很难枚举出所有有危险的java类,用户可以利用依赖引用来绕过import黑名单,所以使用import黑名单是不够安全的,例如我们禁止用户import java.io.File,但用户还是可以import其他类来间接操作系统文件
所以,利用SecureASTCustomizer的import白名单功能,并细心甄别可开放的java包,我们基本上可以避免执行第3种危险代码
private GroovyShell getShell() {
final SecureASTCustomizer secure = new SecureASTCustomizer();// 创建SecureASTCustomizer
secure.setClosuresAllowed(true);// 允许使用闭包
List<Integer> tokensBlacklist = new ArrayList<>();
tokensBlacklist.add(Types.KEYWORD_WHILE);// 添加关键字黑名单 while和goto
tokensBlacklist.add(Types.KEYWORD_GOTO);
secure.setDisallowedTokens(tokensBlacklist);
secure.setIndirectImportCheckEnabled(false);// 设置为false, 可以在代码中定义并直接使用class, 否则需要在白名单中指定
secure.setAllowedStarImports(Arrays.asList("org.codehaus.groovy.runtime.*", "groovy.json.*"));
final CompilerConfiguration config = new CompilerConfiguration();// 自定义CompilerConfiguration,设置AST
config.addCompilationCustomizers(secure);
GroovyShell shell = new GroovyShell(config);
return shell;
}
注意,使用了import白名单后,需要设置setIndirectImportCheckEnabled(false),否则在groovy中定义的class也要加入到白名单中才能使用
由于groovy会自动引入java.util,java.lang包,所以import白名单并不能阻止用户调用System.exit()方法
3.2,GroovyInterceptor
jenkins中使用了GroovyInterceptor拦截器来限制groovy的能力,我们也可以将groovy沙箱引入自己的项目中
<dependency>
<groupId>org.craftercms</groupId>
<artifactId>groovy-sandbox</artifactId>
<version>4.0.2</version>
</dependency>
GroovyInterceptor拦截器作用于运行阶段,为我们提供了多个介入代码运行的时机
static class GroovyNotSupportInterceptor extends GroovyInterceptor {
private final List<String> defaultMethodBlacklist = Arrays.asList("getClass", "class", "wait", "notify",
"notifyAll", "invokeMethod", "finalize", "execute");
/**
* 静态方法拦截
*/
@Override
public Object onStaticCall(GroovyInterceptor.Invoker invoker, @SuppressWarnings("rawtypes") Class receiver,
String method, Object... args) throws Throwable {
System.out.println("onStaticCall: " + method);
if (receiver == System.class && "exit".equals(method)) {
// System.exit(0)
throw new SecurityException("Don't call on System.exit() please");
} else if (receiver == Runtime.class) {
// 通过Java的Runtime.getRuntime().exec()方法执行shell, 操作服务器…
throw new SecurityException("Don't call on RunTime please");
} else if (receiver == Class.class && "forName".equals(method)) {
// Class.forName
throw new SecurityException("Don't call on Class.forName please");
}
return super.onStaticCall(invoker, receiver, method, args);
}
/**
* 普通方法拦截
*/
@Override
public Object onMethodCall(GroovyInterceptor.Invoker invoker, Object receiver, String method, Object... args)
throws Throwable {
System.out.println("onMethodCall: " + method);
if (defaultMethodBlacklist.contains(method)) {
// 方法列表黑名单
throw new SecurityException("Not support method: " + method);
}
return super.onMethodCall(invoker, receiver, method, args);
}
}
// 执行前在当前线程注册方法拦截
new GroovyNotSupportInterceptor().register();
GroovyShell shell = getShell();
shell.evaluate(script)
通过GroovyInterceptor拦截器和SecureASTCustomizer,我们基本上可以避免执行前4种危险代码了
3.3,禁用AST转换
groovy的AST转换发生在编译阶段之前,所以以上方法都无法阻止,目前没找到比较好的方法
只能使用最笨的方法,执行代码前用正则搜索全文,只要在引号或双引号外面发现@符号,则拒绝执行
3.4,自定义超时控制
思路是利用GroovyInterceptor,在每个拦截点判断当前是否超时,超时则中断执行
至于代码中的 sleep 10000000,思路也是利用GroovyInterceptor拦截sleep方法,当sleep方法入参大于5000毫秒时,将入参重置为5000
至此,前7种危险代码基本上能有效避免了
4,groovy线程的内存控制
控制groovy代码占用的内存,最有效的方案是将groovy代码放到容器中执行
其他方案不能准确控制线程的内存
delight的narshon沙箱使用了ThreadMXBean.getThreadAllocatedBytes(线程ID)方法来监控线程已分配的内存大小,当这个值超过预设值时,则会中断执行
但这个方法是返回指定线程自启动以来在堆上分配的内存总量,这个值只会增大不会减小(即使线程创建的对象已经被回收).实际上也没有方法能获取指定线程占用堆的内存总量,因为堆内存是线程共享的
总结,在Java中运行用户的groovy代码,最完美的方案就是放到容器中执行,也就是Faas方案,这样的话不需要限制groovy的能力,也可以限制其占用的CPU和内存资源.
如果采用非Faas方案,本文基于groovy官方的SecureASTCustomizer和jenkins的GroovyInterceptor给出了能有效避免大部分危险代码的方案,如果您有其他建议,欢迎评论区留言.