沙箱

GraalVM 允许以 JVM 语言编写的宿主应用程序通过 多语言 API 执行以 Javascript 编写的客体代码。通过配置 沙箱策略,可以在宿主应用程序和客体代码之间建立一个安全边界。例如,宿主代码可以使用 UNTRUSTED 策略来执行不受信任的客体代码。宿主代码还可以执行多个相互不信任的客体代码实例,这些实例将相互隔离。以这种方式使用沙箱可以支持多租户场景。

Sandbox Security Boundary

引入安全边界将有益于以下用例

  • 使用第三方代码,即引入依赖项。第三方代码通常是受信任的,并在使用前会扫描漏洞,但对其进行沙箱处理是针对供应链攻击的额外预防措施。
  • 用户插件。复杂的应用程序可能允许用户安装社区编写的插件。传统上,这些插件被认为是受信任的,并且经常以完全权限运行,但理想情况下,它们不应该能够干扰应用程序,除非是预期的。
  • 服务器脚本。允许用户使用他们自己的逻辑(用通用脚本语言表达)来定制服务器应用程序,例如,在共享数据源上实现自定义数据处理。

沙箱策略 #

根据用例和相关的可接受安全风险,可以选择 SandboxPolicy,范围从 TRUSTEDUNTRUSTED,从而启用和配置越来越多的限制和缓解措施。SandboxPolicy 具有两个目的:预先配置和验证最终配置。它默认情况下会预先配置上下文和引擎以符合策略。如果配置被进一步定制,则策略验证将确保定制配置不会不可接受地削弱策略。

受信任策略 #

TRUSTED 沙箱策略适用于完全受信任的客体代码。这是默认模式。对上下文或引擎配置没有限制。

示例

try (Context context = Context.newBuilder("js")
                              .sandbox(SandboxPolicy.TRUSTED)
                              .build();) {
    context.eval("js", "print('Hello JavaScript!');");
}

受限策略 #

CONSTRAINED 沙箱策略适用于受信任的应用程序,这些应用程序应监管其对宿主资源的访问。CONSTRAINED 策略

示例

try (Context context = Context.newBuilder("js")
                              .sandbox(SandboxPolicy.CONSTRAINED)
                              .out(new ByteArrayOutputStream())
                              .err(new ByteArrayOutputStream())
                              .build()) {
    context.eval("js", "print('Hello JavaScript!');");
}

隔离策略 #

ISOLATED 沙箱策略建立在 CONSTRAINED 策略之上,适用于可能会因实现错误或处理不受信任的输入而出现错误行为的受信任应用程序。顾名思义,ISOLATED 策略在宿主代码和客体代码之间强制执行更深的隔离。特别是,在 ISOLATED 策略下运行的客体代码将在自己的虚拟机中执行,在一个单独的堆上。这意味着它们不再与宿主应用程序共享运行时元素(如 JIT 编译器或垃圾收集器),使宿主 VM 能够更好地抵御客体 VM 中的故障。

除了 CONSTRAINED 策略的限制之外,ISOLATED 策略还

  • 要求启用 方法作用域。这避免了宿主对象和客体对象之间的循环依赖。 HostAccess.ISOLATED 宿主访问策略已预先配置为满足 ISOLATED 沙箱策略的要求。
  • 要求设置最大隔离堆大小。这是客体 VM 将使用的堆大小。如果引擎由多个上下文共享,则这些上下文的执行将共享隔离堆。
  • 要求设置宿主调用堆栈空间。这可以防止宿主堆栈在向上调用宿主时被耗尽:如果剩余堆栈大小低于指定值,则客体将被禁止执行向上调用。
  • 要求设置最大 CPU 时间限制。这会限制工作负载在给定时间范围内执行。

示例

try (Context context = Context.newBuilder("js")
                              .sandbox(SandboxPolicy.ISOLATED)
                              .out(new ByteArrayOutputStream())
                              .err(new ByteArrayOutputStream())
                              .option("engine.MaxIsolateMemory", "256MB")
                              .option("sandbox.MaxCPUTime", "2s")
                              .build()) {
    context.eval("js", "print('Hello JavaScript!');");
}

从 Polyglot API 版本 23.1 开始,隔离策略和不受信任策略还要求在类路径或模块路径上指定语言的隔离映像。语言的隔离版本可以从 Maven 下载,使用以下依赖项

<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>js-isolate</artifactId>
    <version>${graalvm.version}</version>
    <type>pom</type>
</dependency>

嵌入语言指南 包含有关使用多语言隔离依赖项的更多详细信息。

不受信任策略 #

UNTRUSTED 沙箱策略建立在 ISOLATED 策略之上,旨在减轻运行实际不受信任代码的风险。在运行不受信任的代码时,GraalVM 的攻击面包括执行代码的整个客体 VM,以及提供给客体代码的宿主入口点。

除了 ISOLATED 策略的限制之外,UNTRUSTED 策略还

  • 要求重定向标准 输入 流。
  • 要求设置客体代码的最大内存消耗。这是对最大隔离堆大小的额外限制,其背后的机制是跟踪客体代码在客体 VM 堆上分配的对象的大小。此限制可以认为是“软”内存限制,而隔离堆大小是“硬”限制。
  • 要求设置客体代码可以在堆栈上推送的最大堆栈帧数。此限制可以防止无界递归耗尽堆栈。
  • 要求设置客体代码的最大 AST 深度。与堆栈帧限制一起,这限制了客体代码消耗的堆栈空间。
  • 要求设置最大输出和错误流大小。由于输出和错误流必须被重定向,因此接收端位于宿主端。限制输出和错误流大小可以防止宿主端的可用性问题。
  • 要求启用不受信任代码缓解措施。不受信任的代码缓解措施可以解决 JIT 喷射和推测执行攻击的风险。它们包括常量混淆以及推测执行障碍的全面使用。
  • 进一步限制宿主访问,以确保没有宿主代码的隐式入口点。这意味着不允许客体代码访问宿主数组、列表、映射、缓冲区、可迭代对象和迭代器。原因是宿主端可能存在这些 API 的各种实现,从而导致隐式入口点。此外,不允许通过 HostAccess.Builder#allowImplementationsAnnotatedBy 将客体实现直接映射到宿主接口。 HostAccess.UNTRUSTED 宿主访问策略已预先配置为满足 UNTRUSTED 沙箱策略的要求。

示例

try (Context context = Context.newBuilder("js")
                              .sandbox(SandboxPolicy.UNTRUSTED)
                              .in(new ByteArrayInputStream("foobar".getBytes()))
                              .out(new ByteArrayOutputStream())
                              .err(new ByteArrayOutputStream())
                              .allowHostAccess(HostAccess.UNTRUSTED)
                              .option("engine.MaxIsolateMemory", "1024MB")
                              .option("sandbox.MaxHeapMemory", "128MB")
                              .option("sandbox.MaxCPUTime","2s")
                              .option("sandbox.MaxStatements","50000")
                              .option("sandbox.MaxStackFrames","2")
                              .option("sandbox.MaxThreads","1")
                              .option("sandbox.MaxASTDepth","10")
                              .option("sandbox.MaxOutputStreamSize","32B")
                              .option("sandbox.MaxErrorStreamSize","0B");
                              .build()) {
    context.eval("js", "print('Hello JavaScript!');");
}

有关如何设置资源限制的更多信息,请参阅相应的 指南

宿主访问 #

GraalVM 允许在宿主代码和客体代码之间交换对象,并将宿主方法公开给客体代码。在将宿主方法公开给权限较低的客体代码时,这些方法将成为权限较高的宿主代码的攻击面的一部分。因此,沙箱策略在 CONSTRAINED 策略中已经限制了宿主访问,以使宿主入口点变得明确。

HostAccess.CONSTRAINED 是 CONSTRAINED 沙箱策略的预定义宿主访问策略。要公开宿主类的某个方法,必须用 @HostAccess.Export 对其进行注释。此注释不会被继承。服务提供者(如 Polyglot API FileSystem 实现或标准输出和错误流重定向的输出流接收器)会被公开以供客体代码调用。

客体代码也可以实现用 @Implementable 进行注释的 Java 接口。使用此接口的宿主代码直接与客体代码交互。

与客体代码交互的宿主代码必须以稳健的方式实现

  • 输入验证。从客体传递的所有数据(例如,通过参数传递给公开的方法)都是不受信任的,并且应该在适用的情况下由宿主代码进行彻底验证。
  • 可重入性。暴露的宿主代码应具有可重入性,因为客体代码可以在任何时间调用它。请注意,仅对代码块应用 `synchronized` 关键字并不一定使其可重入。
  • 线程安全。暴露的宿主代码应具有线程安全性,因为客体代码可以从多个线程同时调用它们。
  • 资源消耗。暴露的宿主代码应注意其资源消耗。特别是,基于不可信输入数据分配内存的构造,无论直接或间接,例如通过递归,都应完全避免或实现限制。
  • 特权功能。沙箱实施的限制可以通过暴露提供受限功能的宿主方法来完全绕过。例如,具有 CONSTRAINED 沙箱策略的客体代码不能执行宿主文件 I/O 操作。但是,将允许写入任意文件的宿主方法暴露给上下文,有效地绕过了此限制。
  • 侧信道。根据客体语言,客体代码可能能够访问计时信息。例如,在 Javascript 中,`Date()` 对象提供细粒度的计时信息。在 UNTRUSTED 沙箱策略中,Javascript 定时器的粒度预先配置为一秒,可以降低到 100 毫秒。但是,宿主代码应该意识到客体代码可能会计时其执行,如果宿主代码执行依赖于秘密的处理,可能会发现秘密信息。

不知道与不可信客体代码交互的宿主代码,在不考虑上述方面的情况下,永远不应该直接暴露给客体代码。例如,一个反模式是实现第三方接口并将所有方法调用转发给客体代码。

资源限制 #

ISOLATED 和 UNTRUSTED 沙箱策略要求为上下文设置资源限制。可以为每个上下文提供不同的配置。如果超过限制,代码的评估将失败,上下文将被取消,并出现一个 PolyglotException,该异常对于 `isResourceExhausted()` 返回 `true`。此时,上下文将不再执行客体代码。

--sandbox.TraceLimits 选项允许您跟踪客体代码并记录最大资源利用率。这可用于估计沙箱的参数。例如,可以通过启用此选项并对服务器进行压力测试或在高峰使用期间让服务器运行来获取 Web 服务器的沙箱参数。启用此选项后,工作负载完成后,报告将保存到日志文件。用户可以使用语言启动器中的 `--log.file=<path>` 或使用 `java` 启动器时使用 `-Dpolyglot.log.file=<path>` 来更改日志文件的位置。报告中的每个资源限制都可以直接传递给沙箱选项以强制执行限制。

例如,请参阅如何跟踪 Python 工作负载的限制

graalpy --log.file=limits.log --sandbox.TraceLimits=true workload.py

limits.log:
Traced Limits:
Maximum Heap Memory:                                        12MB
CPU Time:                                                     7s
Number of statements executed:                           9441565
Maximum active stack frames:                                  29
Maximum number of threads:                                     1
Maximum AST Depth:                                            15
Size written to standard output:                              4B
Size written to standard error output:                        0B

Recommended Programmatic Limits:
Context.newBuilder()
            .option("sandbox.MaxHeapMemory", "2MB")
            .option("sandbox.MaxCPUTime","10ms")
            .option("sandbox.MaxStatements","1000")
            .option("sandbox.MaxStackFrames","64")
            .option("sandbox.MaxThreads","1")
            .option("sandbox.MaxASTDepth","64")
            .option("sandbox.MaxOutputStreamSize","1024KB")
            .option("sandbox.MaxErrorStreamSize","1024KB")
            .build();

Recommended Command Line Limits:
--sandbox.MaxHeapMemory=12MB --sandbox.MaxCPUTime=7s --sandbox.MaxStatements=9441565 --sandbox.MaxStackFrames=64 --sandbox.MaxThreads=1 --sandbox.MaxASTDepth=64 --sandbox.MaxOutputStreamSize=1024KB --sandbox.MaxErrorStreamSize=1024KB

如果工作负载发生变化或切换到不同的主要 GraalVM 版本,可能需要重新分析。

某些限制可以在执行过程中的任何时间点 重置

限制活动 CPU 时间 #

sandbox.MaxCPUTime 选项允许您指定运行客体代码花费的最大 CPU 时间。CPU 时间消耗取决于底层硬件。最大 CPU 时间 指定上下文可以处于活动状态多长时间,直到它被自动取消并且上下文被关闭。默认情况下,时间限制每 10 毫秒检查一次。这可以使用 `sandbox.MaxCPUTimeCheckInterval` 选项进行自定义。

只要时间限制被触发,就不能使用此上下文执行更多客体代码。它将持续为将被调用的多语言上下文中的任何方法抛出 `PolyglotException`。

上下文的已用 CPU 时间包括花费在回调宿主代码的时间。

上下文的已用 CPU 时间通常不包括花费在等待同步或 I/O 的时间。所有线程的 CPU 时间将被添加并与 CPU 时间限制进行检查。这意味着如果两个线程执行同一个上下文,那么时间限制将快一倍。

时间限制由一个独立的高优先级线程强制执行,该线程将定期唤醒。不能保证上下文将在指定的精度内被取消。例如,如果宿主 VM 导致完全垃圾回收,则精度可能会严重错过。如果时间限制从未超过,则客体上下文的吞吐量不会受到影响。如果一个上下文的时间限制被超过,那么它可能会暂时减慢具有相同显式引擎的其他上下文的吞吐量。

用于指定时间持续时间的可用单位是 `ms`(毫秒)、`s`(秒)、`m`(分钟)、`h`(小时)和 `d`(天)。最大 CPU 时间限制和检查间隔必须为正数,后跟时间单位。

try (Context context = Context.newBuilder("js")
                           .option("sandbox.MaxCPUTime", "500ms")
                       .build();) {
    context.eval("js", "while(true);");
    assert false;
} catch (PolyglotException e) {
    // triggered after 500ms;
    // context is closed and can no longer be used
    // error message: Maximum CPU time limit of 500ms exceeded.
    assert e.isCancelled();
    assert e.isResourceExhausted();
}

限制已执行语句的数量 #

指定上下文可以执行的最大语句数量,直到它被取消。在上下文的语句限制被触发后,它将不再可用,任何对上下文的使用都将抛出 `PolyglotException`,该异常对于 `PolyglotException.isCancelled()` 返回 `true`。语句限制与执行的线程数量无关。

限制可以设置为负数以禁用它。可以使用 `sandbox.MaxStatementsIncludeInternal` 配置是否仅对内部源应用此限制。默认情况下,该限制不包括标记为内部的源的语句。如果使用共享引擎,则所有引擎的上下文必须使用相同的内部配置。

单个语句的复杂性可能不是恒定时间,具体取决于客体语言。例如,执行 Javascript 内置函数的语句(例如 `Array.sort`)可能占一个语句,但其执行时间取决于数组的大小。

try (Context context = Context.newBuilder("js")
                           .option("sandbox.MaxStatements", "2")
                           .option("sandbox.MaxStatementsIncludeInternal", "false")
                       .build();) {
    context.eval("js", "purpose = 41");
    context.eval("js", "purpose++");
    context.eval("js", "purpose++"); // triggers max statements
    assert false;
} catch (PolyglotException e) {
    // context is closed and can no longer be used
    // error message: Maximum statements limit of 2 exceeded.
    assert e.isCancelled();
    assert e.isResourceExhausted();
}

AST 深度限制 #

对客体语言函数的最大表达式深度的限制。只有可仪表化节点才计入限制。

AST 深度可以估计函数的复杂度及其堆栈帧大小。

限制堆栈帧的数量 #

指定上下文可以在堆栈上推送的最大帧数。线程本地堆栈帧计数器在函数进入时递增,在函数返回时递减。

堆栈帧限制本身可以作为防止无限递归的保护措施。它与 AST 深度限制一起可以限制总堆栈空间使用量。

限制活动线程的数量 #

限制上下文在同一时间点可以使用线程的数量。UNTRUSTED 沙箱策略不支持多线程。

堆内存限制 #

sandbox.MaxHeapMemory 选项指定客体代码在其运行期间允许保留的最大堆内存。只有驻留在客体代码中的对象才计入限制——在回调宿主代码期间分配的内存不计入限制。这不是一个硬限制,因为此选项的效力(也)取决于使用的垃圾收集器。这意味着客体代码可能会超过限制。

try (Context context = Context.newBuilder("js")
                           .option("sandbox.MaxHeapMemory", "100MB")
                       .build()) {
    context.eval("js", "var r = {}; var o = r; while(true) { o.o = {}; o = o.o; };");
    assert false;
} catch (PolyglotException e) {
    // triggered after the retained size is greater than 100MB;
    // context is closed and can no longer be used
    // error message: Maximum heap memory limit of 104857600 bytes exceeded. Current memory at least...
    assert e.isCancelled();
    assert e.isResourceExhausted();
}

该限制通过保留大小计算来检查,该计算要么基于 分配 的字节,要么基于 低内存通知

分配的字节由一个独立的高优先级线程检查,该线程将定期唤醒。每个内存受限上下文(一个设置了 `sandbox.MaxHeapMemory` 的上下文)都有一个这样的线程。保留字节计算由另一个从分配字节检查线程启动的高优先级线程完成,需要时启动。保留字节计算线程在堆内存限制被超过时也会取消上下文。此外,当低内存触发器被调用时,所有引擎上的上下文(这些引擎至少有一个内存受限上下文)将与其分配检查器一起暂停。所有单独的保留大小计算都被取消。每个内存受限上下文的堆中的保留字节由一个单独的高优先级线程计算。

堆内存限制不会阻止上下文导致 `OutOfMemory` 错误。与很少分配对象的代码相比,快速连续分配大量对象的客体代码的准确性较低。

可以使用专家选项 `sandbox.AllocatedBytesCheckInterval`、`sandbox.AllocatedBytesCheckEnabled`、`sandbox.AllocatedBytesCheckFactor`、`sandbox.RetainedBytesCheckInterval`、`sandbox.RetainedBytesCheckFactor` 和 `sandbox.UseLowMemoryTrigger` 来自定义上下文的保留大小计算,如下所述。

当保留字节估计超过指定的 `sandbox.MaxHeapMemory` 的一定因素时,将触发上下文的保留大小计算。该估计基于上下文处于活动状态的线程 分配 的堆内存。更准确地说,该估计是先前保留字节计算的结果(如果有)加上自上次计算开始以来分配的字节。默认情况下,`sandbox.MaxHeapMemory` 的因子为 1.0,可以通过 `sandbox.AllocatedBytesCheckFactor` 选项进行自定义。该因子必须为正数。例如,假设 `sandbox.MaxHeapMemory` 为 100MB,`sandbox.AllocatedBytesCheckFactor` 为 0.5。当分配的字节达到 50MB 时,将首次触发保留大小计算。假设计算出的保留大小为 25MB,那么当分配额外的 25MB 时,将触发下一次保留大小计算,等等。

默认情况下,分配的字节每 10 毫秒检查一次。这可以通过 `sandbox.AllocatedBytesCheckInterval` 进行配置。最小的间隔为 1 毫秒。任何更小的值都将被解释为 1 毫秒。

同一个上下文的两次保留大小计算的开始时间默认情况下至少相隔 10 毫秒。这可以通过 `sandbox.RetainedBytesCheckInterval` 选项进行配置。该间隔必须为正数。

可以通过 `sandbox.AllocatedBytesCheckEnabled` 选项禁用上下文的分配字节检查。默认情况下它是启用的(“true”)。如果禁用(“false”),则只有低内存触发器才能触发上下文的保留大小检查。

当整个宿主 VM 的堆中分配的字节总数超过 VM 的总堆内存的一定因素时,将调用 低内存通知,并启动以下过程。设置了 `sandbox.MaxHeapMemory` 选项的至少一个执行上下文的引擎的执行将被暂停,将计算每个内存受限上下文的堆中的保留字节,超过其限制的上下文将被取消,然后执行将恢复。默认因子为 0.7。这可以通过 `sandbox.RetainedBytesCheckFactor` 选项进行配置。该因子必须介于 0.0 到 1.0 之间。使用 `sandbox.MaxHeapMemory` 选项的所有上下文必须对 `sandbox.RetainedBytesCheckFactor` 使用相同的值。

如果任何堆内存池的已使用阈值或集合已使用阈值已经设置,则默认情况下无法使用低内存触发器,因为无法实现由sandbox.RetainedBytesCheckFactor指定的限制。但是,当sandbox.ReuseLowMemoryTriggerThreshold设置为 true 并且堆内存池的已使用阈值或集合已使用阈值已经设置时,将忽略该内存池的sandbox.RetainedBytesCheckFactor值,并使用已设置的任何限制。这样,低内存触发器就可以与也设置了堆内存池的已使用阈值或集合已使用阈值的库一起使用。

可以通过sandbox.UseLowMemoryTrigger选项禁用所述低内存触发器。默认情况下,它是启用的(“true”)。如果禁用(“false”),则只能通过已分配字节检查器触发执行上下文的保留大小检查。使用sandbox.MaxHeapMemory选项的所有上下文必须为sandbox.UseLowMemoryTrigger使用相同的值。

限制写入标准输出和错误流的数据量 #

限制访客代码在运行时写入标准输出或标准错误输出的输出大小。限制输出大小可以作为防止拒绝服务攻击的保护措施,这些攻击会淹没输出。

try (Context context = Context.newBuilder("js")
                           .option("sandbox.MaxOutputStreamSize", "100KB")
                       .build()) {
    context.eval("js", "while(true) { console.log('Log message') };");
    assert false;
} catch (PolyglotException e) {
    // triggered after writing more than 100KB to stdout
    // context is closed and can no longer be used
    // error message: Maximum output stream size of 102400 exceeded. Bytes written 102408.
    assert e.isCancelled();
    assert e.isResourceExhausted();
}
try (Context context = Context.newBuilder("js")
                           .option("sandbox.MaxErrorStreamSize", "100KB")
                       .build()) {
    context.eval("js", "while(true) { console.error('Error message') };");
    assert false;
} catch (PolyglotException e) {
    // triggered after writing more than 100KB to stderr
    // context is closed and can no longer be used
    // error message: Maximum error stream size of 102400 exceeded. Bytes written 102410.
    assert e.isCancelled();
    assert e.isResourceExhausted();
}

重置资源限制 #

可以使用Context.resetLimits方法在任何时间点重置限制。如果已知且可信的初始化脚本应从限制中排除,这将很有用。只有语句、CPU 时间和输出/错误流限制可以重置。

try (Context context = Context.newBuilder("js")
                           .option("sandbox.MaxCPUTime", "500ms")
                       .build();) {
    context.eval("js", /*... initialization script ...*/);
    context.resetLimits();
    context.eval("js", /*... user script ...*/);
    assert false;
} catch (PolyglotException e) {
    assert e.isCancelled();
    assert e.isResourceExhausted();
}

运行时防御 #

ISOLATED 和 UNTRUSTED 沙箱策略通过engine.SpawnIsolate选项实施的主要防御是,Polyglot 引擎在专用native-image隔离区中运行,将访客代码的执行移至与主机应用程序分离的 VM 级故障域,并具有自己的堆、垃圾收集器和 JIT 编译器。

除了通过访客的堆大小为访客代码的内存消耗设置硬性限制之外,它还允许将运行时防御重点放在访客代码上,而不会导致主机代码的性能下降。运行时防御由engine.UntrustedCodeMitigation选项启用。

常量屏蔽 #

JIT 编译器允许用户提供源代码,并且在源代码有效的情况下,将其编译为机器代码。从攻击者的角度来看,JIT 编译器会将攻击者控制的输入编译为可执行内存中的可预测字节。在称为 JIT 喷射的攻击中,攻击者利用可预测的编译,将恶意输入程序馈送到 JIT 编译器,从而迫使它发出包含面向返回编程 (ROP) 小工具的代码。

输入程序中的常量是此类攻击的特别有吸引力的目标,因为 JIT 编译器通常会将它们逐字包含在机器代码中。常量屏蔽旨在通过在编译过程中引入随机性来使攻击者的预测无效。具体而言,常量屏蔽使用随机密钥在编译时对常量进行加密,并在运行时在每次出现时对其进行解密。只有常量的加密版本会逐字出现在机器代码中。在没有随机密钥的情况下,攻击者无法预测加密的常量值,因此也无法预测可执行内存中的结果字节。

GraalVM 会将运行时编译的访客代码的代码页中嵌入的所有立即值和数据屏蔽到四字节大小。

随机化函数入口点 #

可预测的代码布局使攻击者更容易找到已引入的小工具,例如,通过前面提到的 JIT 喷射攻击。虽然运行时编译的方法已经放置在受操作系统地址空间布局随机化 (ASLR) 影响的内存中,但 GraalVM 还会使用随机数量的陷阱指令填充函数的起始偏移量。

推测执行攻击缓解措施 #

幽灵之类的推测执行攻击利用了 CPU 可能基于分支预测信息暂时执行指令的事实。如果发生错误预测,则这些指令的结果将被丢弃。但是,执行可能会在 CPU 的微体系结构状态中造成副作用。例如,数据可能已在瞬态执行期间被拉入缓存 - 这是一种可以通过计时数据访问读取的侧信道。

GraalVM 通过在运行时编译的访客代码中插入推测执行屏障指令来防止幽灵攻击,以防止攻击者构建推测执行小工具。在与 Java 内存安全相关的每个条件分支的目标处都会放置一个推测执行屏障,以停止推测执行。

共享执行引擎 #

不同信任域的访客代码必须在 polyglot 引擎级别进行分离,也就是说,只有相同信任域的访客代码才能共享引擎。当多个上下文共享一个引擎时,所有上下文必须具有相同的沙箱策略(引擎的沙箱策略)。应用程序开发人员可以选择出于性能原因在执行上下文之间共享执行引擎。虽然上下文保存已执行代码的状态,但引擎保存代码本身。在多个上下文之间共享执行引擎需要显式设置,并且可以在多个上下文执行相同代码的情况下提高性能。在上下文共享执行引擎以进行公共代码同时还执行敏感(私有)代码的情况下,相应的源对象可以选择退出与

Source.newBuilder(…).cached(false).build()

兼容性和限制 #

GraalVM 社区版中不提供沙箱。

根据沙箱策略,只有一部分 Truffle 语言、工具和选项可用。特别是,沙箱目前仅支持运行时的默认版本 ECMAScript(ECMAScript 2022)。从 GraalVM 的 Node.js 内部也不支持沙箱。

沙箱与通过(例如)更改 VM 行为的系统属性对 VM 设置的修改不兼容。

沙箱策略可能会在 GraalVM 主要版本之间发生不兼容的变化,以保持默认的安全姿势。

沙箱无法防止其操作环境中的漏洞,例如操作系统或底层硬件中的漏洞。我们建议采用适当的外部隔离原语来防止相应的风险。

与 Java 安全管理器的区别 #

Java 安全管理器在 Java 17 中已通过 JEP-411 弃用。安全管理器的目的是如下所述:“它允许应用程序在执行可能不安全或敏感的操作之前,确定操作是什么以及是否在允许执行操作的安全上下文中尝试执行操作。”

GraalVM 沙箱的目标是允许以安全的方式执行不受信任的访客代码,这意味着不受信任的访客代码不应能够损害主机代码及其环境的机密性、完整性或可用性。

GraalVM 沙箱在以下方面与安全管理器不同

  • 安全边界:Java 安全管理器具有灵活的安全边界,该边界取决于方法的实际调用上下文。这使得“划清界限”变得复杂且容易出错。安全关键代码块首先需要检查当前调用堆栈以确定堆栈上的所有帧是否都有权调用代码。在 GraalVM 沙箱中,存在一个直接、明确的安全边界:主机代码和访客代码之间的边界,访客代码在 Truffle 框架之上运行,类似于典型计算机体系结构如何区分用户模式和(特权)内核模式。
  • 隔离:使用 Java 安全管理器,特权代码与不受信任的代码在语言和运行时方面几乎处于“平等地位”。
  • 共享语言:使用 Java 安全管理器,不受信任的代码使用与特权代码相同的语言编写,具有两种代码之间直接互操作的优势。相比之下,GraalVM 沙箱中,使用 Truffle 语言编写的访客应用程序需要通过显式边界传递到用 Java 编写的主机代码。
  • 共享运行时:使用 Java 安全管理器,不受信任的代码在与可信代码相同的 JVM 环境中执行,共享 JDK 类和运行时服务,例如垃圾收集器或编译器。在 GraalVM 沙箱中,不受信任的代码在专用 VM 实例(GraalVM 隔离区)中运行,通过设计将主机和访客的服务和 JDK 类分开。
  • 资源限制:Java 安全管理器无法限制对 CPU 时间或内存等计算资源的使用,这使得不受信任的代码可以使 JVM 出现 DoS。GraalVM 沙箱提供控制措施以设置访客代码可以消耗的几种计算资源(CPU 时间、内存、线程、进程)的限制,以解决可用性问题。
  • 配置:创建 Java 安全管理器策略通常被认为是一项复杂且容易出错的任务,需要了解哪些程序部分需要哪种访问级别的主题专家。配置 GraalVM 沙箱提供安全配置文件,这些配置文件侧重于常见的沙箱用例和威胁模型。

报告漏洞 #

如果您认为发现了安全漏洞,请将报告提交至 secalert_us@oracle.com,最好附上概念证明。有关更多信息,包括我们的公共加密密钥以进行安全电子邮件,请参阅报告漏洞。我们要求您不要直接或通过其他渠道联系项目贡献者以报告此问题。

联系我们