沙箱機制

GraalVM 允許以 JVM 為基礎的語言撰寫的主機應用程式,透過 Polyglot 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>

嵌入語言指南包含有關使用 polyglot 隔離相依性的更多詳細資訊。

不信任原則 #

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 沙箱策略的客端程式碼無法執行主機檔案 IO 操作。然而,向內容公開允許寫入任意檔案的主機方法,實際上會繞過此限制。
  • 側通道。根據客端語言,客端程式碼可能可以存取時間資訊。例如,在 Javascript 中,Date() 物件提供精細的時間資訊。在 UNTRUSTED 沙箱策略中,Javascript 計時器的粒度預先設定為一秒,並可以降低至 100 毫秒。然而,主機程式碼應注意,客端程式碼可能會計時其執行,如果主機程式碼執行依賴秘密的處理,則可能會發現秘密資訊。

不了解與不受信任的客端程式碼互動的主機程式碼,在未考慮上述各個方面的情況下,絕不應直接暴露給客端程式碼。例如,一個反模式是實作第三方介面並將所有方法調用轉發給客端程式碼。

資源限制 #

ISOLATED 和 UNTRUSTED 沙箱策略需要為內容設定資源限制。每個內容可以提供不同的設定。如果超出限制,程式碼的評估會失敗,並且內容會被 PolyglotException 取消,該例外會為 isResourceExhausted() 傳回 true。此時,無法在內容中執行任何更多的客端程式碼。

--sandbox.TraceLimits 選項允許您追蹤客端程式碼並記錄最大資源利用率。這可以用於估計沙箱的參數。例如,網頁伺服器的沙箱參數可以透過啟用此選項並壓力測試伺服器,或讓伺服器在高峰使用期間執行來獲得。啟用此選項時,報告會在工作負載完成後儲存到記錄檔。使用者可以使用語言啟動器搭配 --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 選項進行自訂。

一旦觸發時間限制,此內容將無法執行任何其他客端程式碼。對於將調用的 polyglot 內容的任何方法,它將不斷拋出 PolyglotException

內容的已使用 CPU 時間包括回呼主機程式碼所花費的時間。

內容的已使用 CPU 時間通常不包括等待同步或 IO 所花費的時間。所有執行緒的 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.AllocatedBytesCheckIntervalsandbox.AllocatedBytesCheckEnabledsandbox.AllocatedBytesCheckFactorsandbox.RetainedBytesCheckIntervalsandbox.RetainedBytesCheckFactorsandbox.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();
}

執行階段防禦 #

透過 engine.SpawnIsolate 選項,ISOLATED 和 UNTRUSTED 沙箱原則強制執行的主要防禦措施是,Polyglot 引擎在專用的 native-image 隔離區中執行,將訪客程式碼的執行移至與主機應用程式分離的 VM 層級錯誤網域,它有自己的堆、垃圾收集器和 JIT 編譯器。

除了透過訪客的堆大小為訪客程式碼的記憶體消耗設定硬性限制外,它還可以將執行階段防禦集中在訪客程式碼上,而不會導致主機程式碼的效能下降。執行階段防禦由 engine.UntrustedCodeMitigation 選項啟用。

常數盲化 #

JIT 編譯器允許使用者提供原始程式碼,並且在原始程式碼有效的情況下,將其編譯為機器碼。從攻擊者的角度來看,JIT 編譯器將攻擊者控制的輸入編譯為可執行記憶體中可預測的位元組。在稱為 JIT 噴射的攻擊中,攻擊者會利用可預測的編譯,將惡意輸入程式饋送到 JIT 編譯器中,從而迫使它發出包含返回導向程式設計 (ROP) 小工具的程式碼。

輸入程式中的常數是此類攻擊的特別有吸引力的目標,因為 JIT 編譯器通常將它們逐字包含在機器碼中。常數盲化的目的是透過在編譯過程中引入隨機性來使攻擊者的預測失效。具體來說,常數盲化會在編譯時使用隨機金鑰加密常數,並在每次出現時在執行時解密它們。只有常數的加密版本會逐字出現在機器碼中。在沒有隨機金鑰的知識的情況下,攻擊者無法預測加密的常數值,因此無法再預測可執行記憶體中產生的位元組。

GraalVM 會盲化執行階段編譯的訪客程式碼程式碼頁面中嵌入的所有立即值和資料,直到四個位元組的大小。

隨機化函式進入點 #

可預測的程式碼配置使攻擊者更容易找到透過上述 JIT 噴射攻擊引入的小工具。雖然執行階段編譯的方法已經放置在受作業系統位址空間配置隨機化 (ASLR) 影響的記憶體中,但 GraalVM 還會使用隨機數量的陷阱指令來填充函式的起始偏移。

推測性執行攻擊緩解 #

推測性執行攻擊(例如 Spectre)利用了 CPU 可能會根據分支預測資訊瞬時執行指令的事實。在預測錯誤的情況下,這些指令的結果將被丟棄。但是,執行可能已在 CPU 的微架構狀態中造成副作用。例如,在瞬時執行期間,資料可能已提取到快取中 - 一種可以透過計時資料存取來讀取的側通道。

GraalVM 透過在執行階段編譯的訪客程式碼中插入推測性執行屏障指令來防止 Spectre 攻擊,以防止攻擊者製作推測性執行小工具。推測性執行屏障放置在與 Java 記憶體安全相關的條件分支的每個目標處,以停止推測性執行。

共用執行引擎 #

不同信任網域的訪客程式碼必須在 polyglot 引擎層級分隔開來,也就是說,只有相同信任網域的訪客程式碼應該共用一個引擎。當多個內容共用一個引擎時,它們都必須具有相同的沙箱原則(引擎的沙箱原則)。應用程式開發人員可能會選擇在執行內容之間共用執行引擎以提高效能。雖然內容保留了執行程式碼的狀態,但引擎保留了程式碼本身。需要在多個內容之間顯式設定共用執行引擎,並且可以在多個內容執行相同程式碼的情況下提高效能。在多個內容共用執行引擎以執行通用程式碼的情況下,也會執行敏感(私有)程式碼,對應的來源物件可以選擇不與

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

相容性和限制 #

沙箱在 GraalVM 社群版中不可用。

根據沙箱原則,只有 Truffle 語言、工具和選項的子集可用。特別是,目前僅在執行階段的 ECMAScript 預設版本 (ECMAScript 2022) 中支援沙箱。GraalVM 的 Node.js 也不支援沙箱。

沙箱與透過(例如)變更 VM 行為的系統屬性修改 VM 設定不相容。

沙箱原則在主要的 GraalVM 版本之間可能會發生不相容的變更,以維持預設安全態勢。

沙箱無法防範其作業環境中的漏洞,例如作業系統或底層硬體中的漏洞。我們建議採用適當的外部隔離基本元素來防範相關的風險。

與 Java Security Manager 的區別 #

Java Security Manager 已在 Java 17 中透過 JEP-411 棄用。安全管理員的用途如下所述:「它允許應用程式在執行可能不安全或敏感的操作之前,確定操作是什麼,以及是否在允許執行操作的安全內容中嘗試執行操作。」

GraalVM 沙箱的目標是以安全的方式允許執行不受信任的訪客程式碼,這表示不受信任的訪客程式碼不應能夠損害主機程式碼及其環境的機密性、完整性或可用性。

GraalVM 沙箱與 Security Managers 在以下方面有所不同

  • 安全邊界:Java Security Manager 具有彈性的安全邊界,取決於方法的實際呼叫內容。這使得「劃定界限」變得複雜且容易出錯。安全關鍵程式碼區塊首先需要檢查目前的呼叫堆疊,以判斷堆疊上的所有框架是否都具有調用程式碼的權限。在 GraalVM 沙箱中,有一個簡單、清晰的安全邊界:主機和訪客程式碼之間的邊界,訪客程式碼在 Truffle 架構之上執行,類似於典型的電腦架構如何區分使用者模式和(特權)核心模式。
  • 隔離:對於 Java Security Manager,特權程式碼在語言和執行階段方面幾乎與不受信任的程式碼「處於平等地位」
  • 共用語言:使用 Java Security Manager 時,不受信任的程式碼以與特權程式碼相同的語言編寫,其優點是兩者之間的互操作性簡單。相反地,在 GraalVM 沙箱中,以 Truffle 語言編寫的訪客應用程式需要通過顯式邊界才能進入以 Java 編寫的主機程式碼。
  • 共用執行環境:在 Java 安全管理員 (Java Security Manager) 中,不受信任的程式碼會與受信任的程式碼在相同的 JVM 環境中執行,共用 JDK 類別和執行時間服務,例如垃圾收集器或編譯器。在 GraalVM 沙箱中,不受信任的程式碼會在專用的 VM 執行個體(GraalVM 隔離區)中執行,從設計上將主機和客體的服務和 JDK 類別分開。
  • 資源限制:Java 安全管理員無法限制運算資源的使用,例如 CPU 時間或記憶體,這會導致不受信任的程式碼對 JVM 進行阻斷服務 (DoS) 攻擊。GraalVM 沙箱提供控制項來設定數個運算資源(CPU 時間、記憶體、執行緒、程序)的限制,客體程式碼可能會使用這些資源來解決可用性問題。
  • 設定:制定 Java 安全管理員原則通常被認為是一項複雜且容易出錯的任務,需要一位了解程式的哪些部分需要哪些存取權限的專業人員。設定 GraalVM 沙箱提供了安全設定檔,重點在於常見的沙箱使用案例和威脅模型。

回報漏洞 #

如果您認為您發現了安全性漏洞,請將報告(最好附上概念驗證)提交至 secalert_us@oracle.com。請參閱回報漏洞以取得更多資訊,包括我們用於安全電子郵件的公開加密金鑰。我們要求您不要直接或通過其他管道聯絡專案貢獻者以報告問題。

與我們聯繫