GraalVM Polyglot 沙箱逃逸:跨语言上下文 RCE 风险分析与防护实践
摘要
GraalVM Polyglot 为 Java 应用提供了强大的跨语言嵌入能力,允许宿主应用在同一运行环境中执行 JavaScript、Python、Ruby、R、WebAssembly 等多种语言代码。这种能力在规则引擎、插件系统、在线代码执行、低代码平台、数据处理平台中非常有价值,但也带来了新的安全边界问题:当不可信脚本能够通过 Polyglot Context 访问 Java 宿主对象、文件系统、环境变量、进程创建接口或其他语言上下文时,所谓“脚本沙箱”就可能退化为“远程代码执行入口”。
本文围绕 GraalVM Polyglot 沙箱逃逸中的跨语言上下文 RCE 风险展开,重点分析 Polyglot Context、HostAccess、HostClassLookup、PolyglotAccess、IOAccess、Resource Limits 等关键配置点,解释常见误区和逃逸成因,并给出面向生产环境的安全加固方案。本文仅用于安全研究、防护建设和代码审计,不提供可直接用于攻击的利用链或 RCE payload。
关键词: GraalVM、Polyglot、沙箱逃逸、RCE、HostAccess、Java 安全、代码审计、插件安全
1. 背景:为什么 GraalVM Polyglot 容易被误用
GraalVM Polyglot API 的核心价值,是让 Java 宿主程序可以嵌入并执行其他语言代码。官方文档明确说明,Polyglot API 可以在 Java 宿主应用中嵌入和运行 guest language 代码,并支持不同语言之间的互操作。
这类能力非常适合以下场景:
- 业务规则动态执行,例如用户自定义 JavaScript 规则;
- 平台插件机制,例如允许第三方编写扩展脚本;
- 在线代码运行,例如数据分析平台中的表达式执行;
- 多语言算法集成,例如 Java 调用 Python 或 JavaScript 函数;
- 低代码、自动化编排、流程引擎中的动态逻辑执行。
问题在于,很多系统在接入 GraalVM 时,容易把“能执行脚本”理解为“脚本天然处于沙箱中”。实际上,GraalVM 的安全边界依赖宿主应用如何创建 Context、如何配置访问策略、是否限制 IO、是否允许查找 Java 类、是否允许跨上下文共享值、是否设置资源限制等。官方安全文档也强调,GraalVM 可以通过沙箱策略在宿主应用和 guest code 之间建立安全边界,但前提是正确配置 sandbox policy。
换句话说,GraalVM Polyglot 本身不是“默认安全的在线执行环境”。它是一套强大的多语言运行与互操作框架,安全性取决于嵌入方是否正确收敛权限。
2. Polyglot 沙箱的基本模型
在 GraalVM 中,最重要的概念是 Context。一个 Polyglot Context 代表一组可执行语言的运行时状态。官方 API 文档说明,Context 用于执行 guest language 代码,并且 permitted languages 会在首次使用时被延迟初始化。
可以把它理解为:
Java 宿主应用
|
| 创建 Context
|
Polyglot Context
|
|-- JavaScript guest code
|-- Python guest code
|-- WebAssembly guest code
|-- 其他受支持语言
真正的风险点不在于“执行 JavaScript”本身,而在于 guest code 是否能越过 Context 边界访问宿主能力。例如:
guest code
|
| 访问宿主对象?
| 查找 Java 类?
| 读写文件?
| 创建进程?
| 访问环境变量?
| 调用其他语言上下文?
| 共享跨 Context 对象?
v
宿主系统资源
如果这些权限没有被严格限制,脚本执行就可能从“表达式计算”升级为“宿主能力调用”,最终形成 RCE、敏感信息读取、横向调用、拒绝服务等安全问题。
3. RCE 风险的根源:不是单点漏洞,而是权限组合失控
很多人提到“沙箱逃逸”时,会下意识寻找某个具体 CVE 或某条神奇 payload。但在实际代码审计中,更常见的问题不是底层虚拟机漏洞,而是宿主程序把过多能力暴露给 guest code。
GraalVM 的 Context.Builder 中包含大量安全相关配置,例如:
allowHostAccessallowHostClassLookupallowPolyglotAccessallowIOallowNativeAccessallowCreateThreadallowCreateProcessallowEnvironmentAccessresourceLimitssandbox
官方 Context.Builder 文档说明,allowCreateProcess(true) 会允许 guest language 执行外部进程,默认值是 false;如果 allowAllAccess(true) 被启用,则进程创建也会被默认打开,除非显式拒绝。
这就解释了为什么很多 RCE 风险来自“组合配置”:
允许查找 Java 类
+ 允许访问宿主方法
+ 允许 IO 或进程创建
+ 不限制可访问类范围
= 高危执行环境
单独看某个开关,可能只是“为了方便调用 Java 类”;但多个开关叠加后,guest code 就可能拥有接近宿主应用的能力。
4. 高危配置一:allowAllAccess(true)
在代码审计中,最需要优先搜索的配置是:
Context.newBuilder("js")
.allowAllAccess(true)
.build();
这是最典型的危险写法。allowAllAccess(true) 相当于为 guest code 打开多类默认权限。官方 HostAccess 文档明确说明,HostAccess.ALL 允许访问宿主对象的 public 方法和字段,并允许不受限制的反射访问,不建议在 guest application 不完全可信的环境中使用。
危险点在于,很多开发人员为了快速实现“JavaScript 调 Java”,会直接使用 allowAllAccess(true)。在内部测试环境中,这种写法确实方便;但一旦脚本内容来自用户输入、第三方插件、数据库配置、远程接口、低代码表单,就会变成高风险入口。
更安全的原则是:
不可信脚本:绝不使用 allowAllAccess(true)
可信内部脚本:也应尽量避免全量开放
生产环境:使用显式白名单
推荐的思路是通过 HostAccess.EXPLICIT 或更严格的策略,只暴露被 @HostAccess.Export 标注的方法。官方文档说明,HostAccess.EXPLICIT 只允许访问 public 且被 @Export 标注的宿主方法或字段。
5. 高危配置二:allowHostClassLookup 过宽
第二个常见风险点是:
.allowHostClassLookup(className -> true)
这表示 guest code 可以查找所有 Java 类。官方 Java Interoperability 文档中给出的示例也说明,通过 allowHostClassLookup(className -> true) 可以允许访问所有 Java 类。
这类代码在教程中很常见,但在生产环境中非常危险。因为“允许查找类”本身不一定马上造成 RCE,但它为后续访问 Java 类型、反射、文件、网络、进程相关类创造了条件。
更安全的写法应该是白名单:
Context context = Context.newBuilder("js")
.allowHostAccess(HostAccess.EXPLICIT)
.allowHostClassLookup(name ->
name.equals("com.example.safe.RuleApi")
)
.build();
这段配置表达的是:guest code 只能访问明确允许的业务 API,而不是整个 JDK 或整个应用 classpath。
审计时可以重点搜索:
allowHostClassLookup(c -> true)
allowHostClassLookup(name -> true)
allowHostClassLookup(s -> s.startsWith("java."))
allowHostClassLookup(s -> !s.contains("xxx"))
尤其要警惕黑名单式过滤。黑名单很容易漏掉危险类、包装类、间接调用类、业务自定义工具类。安全沙箱应当采用白名单,而不是“禁止几个看起来危险的类”。
6. 高危配置三:HostAccess.ALL 与反射边界
HostAccess 决定 guest code 能访问宿主对象的哪些成员。官方文档列出了多种策略,包括 EXPLICIT、SCOPED、NONE、ALL、CONSTRAINED、ISOLATED、UNTRUSTED。其中 HostAccess.ALL 是最危险的,因为它允许对 public 宿主成员进行不受限访问,并且包含反射访问能力。
在安全设计中,应该优先考虑:
.allowHostAccess(HostAccess.NONE)
或:
.allowHostAccess(HostAccess.EXPLICIT)
如果业务必须向脚本暴露能力,建议单独定义一个最小 API:
public final class SafeRuleApi {
@HostAccess.Export
public boolean contains(String text, String keyword) {
if (text == null || keyword == null) {
return false;
}
return text.contains(keyword);
}
@HostAccess.Export
public int max(int a, int b) {
return Math.max(a, b);
}
}
然后只把这个对象传给 Context:
Context context = Context.newBuilder("js")
.allowHostAccess(HostAccess.EXPLICIT)
.allowHostClassLookup(name -> false)
.build();
context.getBindings("js").putMember("api", new SafeRuleApi());
这种方式的核心思想是:
guest code 不直接访问 Java 世界;
guest code 只访问宿主应用主动暴露的安全门面;
安全门面只提供无副作用、可验证、可审计的能力。
7. 高危配置四:IO、环境变量和进程权限
如果脚本能读写文件、访问环境变量、创建外部进程,那么它的风险等级会显著上升。
官方 Context.Builder 文档说明,allowIO(IOAccess) 用于配置 guest language 对宿主 IO 的访问,默认是 IOAccess.NONE;如果 allowAllAccess(true) 被启用,则默认会使用 IOAccess.ALL,除非显式设置。
同样,allowEnvironmentAccess 用于控制环境变量访问。当 allowAllAccess(true) 时,默认环境访问策略是 EnvironmentAccess.INHERIT,否则是 EnvironmentAccess.NONE。
在生产系统中,环境变量里经常存在数据库密码、云服务 AK/SK、Token、代理配置、内部服务地址等敏感信息。一旦 guest code 能访问环境变量,就可能造成凭据泄露。
推荐配置:
Context context = Context.newBuilder("js")
.allowHostAccess(HostAccess.EXPLICIT)
.allowHostClassLookup(name -> false)
.allowIO(IOAccess.NONE)
.allowCreateProcess(false)
.allowEnvironmentAccess(EnvironmentAccess.NONE)
.allowNativeAccess(false)
.build();
这里的目标不是“让脚本什么都不能做”,而是把脚本能力收敛为“只做业务计算”。如果确实需要读写文件,也应使用虚拟文件系统、临时目录、只读目录、文件大小限制、路径归一化校验等方式隔离。
8. 高危配置五:跨语言上下文与 PolyglotAccess
GraalVM 的特点是“多语言互操作”。这意味着 JavaScript 不一定只和 Java 交互,也可能通过 Polyglot 机制与其他 guest language 共享对象或调用函数。allowPolyglotAccess 就是一个关键控制点。
官方 Context.Builder 文档说明,allowPolyglotAccess 用于允许 polyglot access;当 allowAllAccess(true) 时,默认策略是 PolyglotAccess.ALL,否则是 PolyglotAccess.NONE。
跨语言上下文风险主要体现在:
JavaScript 本身受限
|
| 调用另一个语言上下文中的对象
v
另一个 guest language 暴露了更高权限能力
|
v
间接访问宿主资源
例如,某系统认为 JavaScript 环境是安全的,但同时把一个 Python、Ruby 或 WebAssembly 相关对象共享给了 JavaScript;如果另一个语言上下文拥有更宽的宿主访问能力,就可能出现“低权限语言借道高权限语言”的问题。
因此,跨语言调用要遵守两个原则:
- 不同可信级别的脚本不要放在同一个共享上下文中;
- 不同语言的权限模型必须一致,不能出现一个严格、一个宽松的情况。
对于不可信代码,应优先设置:
.allowPolyglotAccess(PolyglotAccess.NONE)
.allowValueSharing(false)
官方文档说明,allowValueSharing(false) 可以禁止不同 Context 之间传递值;当严格安全要求下,不希望一个 Context 的值传入另一个 Context 时,这个配置很有用。
9. 资源耗尽也是沙箱逃逸的一部分
很多人只把 RCE 当成安全问题,忽略了拒绝服务。实际上,脚本沙箱中最常见的问题之一就是资源耗尽,例如:
死循环
超大数组
深递归
大量输出
多线程创建
内存膨胀
高 CPU 占用
GraalVM 官方安全文档说明,ISOLATED 和 UNTRUSTED 沙箱策略要求为 Context 设置资源限制;当限制被超过时,代码执行会失败,Context 会被取消,并抛出 PolyglotException,此后该 Context 不能继续执行 guest code。
常见限制包括:
.option("sandbox.MaxHeapMemory", "128MB")
.option("sandbox.MaxCPUTime", "2s")
.option("sandbox.MaxStatements", "50000")
.option("sandbox.MaxStackFrames", "64")
.option("sandbox.MaxThreads", "1")
.option("sandbox.MaxOutputStreamSize", "64KB")
.option("sandbox.MaxErrorStreamSize", "64KB")
官方文档也给出了 SandboxPolicy.UNTRUSTED 与资源限制配合使用的示例,包括最大堆内存、最大 CPU 时间、最大语句数、最大线程数、输出流大小等限制。
在生产环境中,资源限制不能只靠业务超时控制。因为业务线程超时不一定能及时终止底层 guest code,也不能完整处理内存、输出流、线程、递归深度等问题。正确做法是:在 GraalVM Context 层面设置限制,同时在业务网关、线程池、容器层面设置第二道限制。
10. JSR-223 ScriptEngine 的迁移风险
很多老项目并不是直接使用 Context,而是通过 javax.script.ScriptEngine 执行脚本。GraalJS 提供过 JSR-223 兼容能力,但官方文档明确说明,这主要是为了兼容迁移场景,并强烈建议使用 org.graalvm.polyglot.Context,因为它能直接控制更细粒度的安全设置。
这类代码在审计时经常出现:
ScriptEngine engine = new ScriptEngineManager()
.getEngineByName("graal.js");
engine.eval(userScript);
问题在于,很多团队迁移 Nashorn 到 GraalJS 后,只替换了引擎名称,没有重新评估安全配置。结果是:
旧脚本执行入口仍然存在;
权限模型没有显式声明;
业务误以为“换成 GraalVM 后更安全”;
实际缺少 Context 级别的访问控制。
建议是:涉及不可信脚本时,优先改造为显式 Context 创建,并统一封装安全工厂类,例如:
public final class SafeContextFactory {
public static Context createJsSandbox() {
return Context.newBuilder("js")
.sandbox(SandboxPolicy.UNTRUSTED)
.allowHostAccess(HostAccess.UNTRUSTED)
.allowHostClassLookup(name -> false)
.allowPolyglotAccess(PolyglotAccess.NONE)
.allowValueSharing(false)
.allowIO(IOAccess.NONE)
.allowCreateProcess(false)
.allowCreateThread(false)
.allowNativeAccess(false)
.allowEnvironmentAccess(EnvironmentAccess.NONE)
.option("sandbox.MaxCPUTime", "2s")
.option("sandbox.MaxStatements", "50000")
.option("sandbox.MaxThreads", "1")
.option("sandbox.MaxOutputStreamSize", "64KB")
.option("sandbox.MaxErrorStreamSize", "64KB")
.build();
}
}
这段代码不是唯一标准答案,但体现了一个重要原则:不要让各业务模块自行创建 Context,而是统一通过安全工厂创建。
11. 代码审计思路:如何发现 Polyglot 沙箱风险
在实际项目中,可以按以下关键词进行全局检索:
Context.newBuilder
Context.create
allowAllAccess
allowHostAccess
HostAccess.ALL
allowHostClassLookup
allowPolyglotAccess
PolyglotAccess.ALL
allowIO
IOAccess.ALL
allowNativeAccess
allowCreateProcess
allowCreateThread
allowEnvironmentAccess
ScriptEngineManager
graal.js
Java.type
getBindings
putMember
重点关注以下问题:
11.1 脚本来源是否可信
需要确认脚本是否来自:
用户输入
数据库配置
接口参数
文件上传
第三方插件
低代码页面
消息队列
远程配置中心
只要脚本内容可以被非核心开发人员修改,就不能视为完全可信。
11.2 Context 是否复用
如果多个用户、多个租户、多个任务共用同一个 Context,就可能造成变量污染、对象残留、权限串扰。多租户场景下,应避免共享上下文,或者至少确保不同租户之间没有值共享和状态共享。
11.3 是否暴露了业务对象
很多系统会这样做:
context.getBindings("js").putMember("service", orderService);
如果 orderService 是完整 Spring Bean,里面可能包含数据库访问、HTTP 调用、文件处理、任务调度等能力。更安全的方式是暴露一个专门的安全门面,而不是暴露完整业务 Bean。
11.4 是否使用黑名单过滤
例如:
.allowHostClassLookup(name -> !name.contains("Runtime"))
这类写法几乎不可接受。安全策略应当是“默认拒绝,少量允许”,而不是“默认允许,排除几个危险项”。
11.5 是否设置资源限制
如果没有 CPU、内存、语句数、线程数、输出流大小限制,即使没有 RCE,也可能被恶意脚本拖垮服务。
12. 防护基线:生产环境推荐清单
可以将以下内容作为生产安全基线:
1. 禁止不可信脚本使用 allowAllAccess(true)。
2. 禁止 HostAccess.ALL 暴露给不可信 guest code。
3. allowHostClassLookup 必须使用白名单。
4. 默认关闭 IO、进程创建、线程创建、Native Access。
5. 默认关闭环境变量访问。
6. 默认关闭 PolyglotAccess.ALL。
7. 不同租户、不同可信级别脚本隔离 Context。
8. 不直接暴露 Spring Bean、DAO、HTTP Client、文件服务等对象。
9. 所有暴露给脚本的方法必须 @HostAccess.Export,并经过安全评审。
10. 暴露方法必须做参数校验、长度限制、类型限制和异常处理。
11. 必须设置 CPU、内存、语句数、线程数、输出流限制。
12. 禁止脚本执行结果直接参与 SQL、模板渲染、命令拼接等敏感操作。
13. 记录脚本来源、执行人、执行时间、脚本摘要、执行耗时和异常信息。
14. 高危脚本执行平台应接入审计、告警、熔断和限流。
15. GraalVM 大版本升级后重新验证 sandbox policy,因为官方说明 sandbox policy 在新大版本中可能出现不兼容变化。:contentReference[oaicite:15]{index=15}
13. 安全架构建议:把 Polyglot 当作“不可信计算单元”
如果业务确实需要执行用户脚本,建议从架构上将其视为“不可信计算单元”,而不是普通工具函数。
推荐分层:
入口层:
校验脚本来源、大小、语言类型、执行频率
编排层:
分配独立 Context
设置统一安全策略
设置超时和资源限制
能力层:
只暴露安全 API
不暴露完整业务 Bean
不暴露系统级能力
隔离层:
使用容器、独立进程、低权限账号
限制网络、文件系统、环境变量
审计层:
记录脚本执行日志
记录异常、超时、资源耗尽事件
对高风险行为告警
对于互联网暴露场景,不建议只依赖 GraalVM 内部配置。更稳妥的做法是结合容器隔离、Kubernetes 安全上下文、只读文件系统、seccomp、AppArmor、网络策略、低权限运行用户等外部隔离能力,实现多层防护。
14. 总结
GraalVM Polyglot 的价值在于跨语言互操作,但它的安全风险也来自跨语言互操作。所谓“沙箱逃逸”并不总是底层虚拟机漏洞,更多时候是宿主应用错误地开放了 HostAccess、HostClassLookup、IO、进程、环境变量、跨 Context 共享等能力。
对于研发团队来说,最重要的不是记住某条利用链,而是建立正确的安全模型:
不可信脚本默认没有任何宿主能力;
确实需要能力时,通过最小 API 显式暴露;
所有能力都要白名单、可审计、可限制;
所有执行都要有资源边界;
所有上下文都要按租户和可信级别隔离。
如果把 GraalVM Polyglot 当作普通脚本引擎使用,它可能成为 RCE 入口;如果把它当作不可信代码执行平台来设计,它可以成为安全、灵活、可控的多语言扩展能力。
更多推荐




所有评论(0)