searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

如何在Java中安全运行用户的groovy代码

2023-08-23 10:06:30
772
0

在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给出了能有效避免大部分危险代码的方案,如果您有其他建议,欢迎评论区留言.

 

0条评论
0 / 1000
白龙马
14文章数
0粉丝数
白龙马
14 文章 | 0 粉丝
原创

如何在Java中安全运行用户的groovy代码

2023-08-23 10:06:30
772
0

在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给出了能有效避免大部分危险代码的方案,如果您有其他建议,欢迎评论区留言.

 

文章来自个人专栏
Java
14 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0