摘要

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 代码,并支持不同语言之间的互操作。

这类能力非常适合以下场景:

  1. 业务规则动态执行,例如用户自定义 JavaScript 规则;
  2. 平台插件机制,例如允许第三方编写扩展脚本;
  3. 在线代码运行,例如数据分析平台中的表达式执行;
  4. 多语言算法集成,例如 Java 调用 Python 或 JavaScript 函数;
  5. 低代码、自动化编排、流程引擎中的动态逻辑执行。

问题在于,很多系统在接入 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 中包含大量安全相关配置,例如:

  • allowHostAccess
  • allowHostClassLookup
  • allowPolyglotAccess
  • allowIO
  • allowNativeAccess
  • allowCreateThread
  • allowCreateProcess
  • allowEnvironmentAccess
  • resourceLimits
  • sandbox

官方 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 能访问宿主对象的哪些成员。官方文档列出了多种策略,包括 EXPLICITSCOPEDNONEALLCONSTRAINEDISOLATEDUNTRUSTED。其中 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;如果另一个语言上下文拥有更宽的宿主访问能力,就可能出现“低权限语言借道高权限语言”的问题。

因此,跨语言调用要遵守两个原则:

  1. 不同可信级别的脚本不要放在同一个共享上下文中;
  2. 不同语言的权限模型必须一致,不能出现一个严格、一个宽松的情况。

对于不可信代码,应优先设置:


.allowPolyglotAccess(PolyglotAccess.NONE)
.allowValueSharing(false)

官方文档说明,allowValueSharing(false) 可以禁止不同 Context 之间传递值;当严格安全要求下,不希望一个 Context 的值传入另一个 Context 时,这个配置很有用。


9. 资源耗尽也是沙箱逃逸的一部分

很多人只把 RCE 当成安全问题,忽略了拒绝服务。实际上,脚本沙箱中最常见的问题之一就是资源耗尽,例如:


死循环
超大数组
深递归
大量输出
多线程创建
内存膨胀
高 CPU 占用

GraalVM 官方安全文档说明,ISOLATEDUNTRUSTED 沙箱策略要求为 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 入口;如果把它当作不可信代码执行平台来设计,它可以成为安全、灵活、可控的多语言扩展能力。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐