ポリグロット・サンドボックス化
GraalVMでは、JVMベースの言語で記述されたホスト・アプリケーションが、ポリグロット埋込みAPIを介してJavascriptで記述されたゲスト・コードを実行できます。サンドボックス・ポリシーを構成すると、ホスト・アプリケーションとゲスト・コードの間のセキュリティ境界を確立できます。たとえば、ホスト・コードは、UNTRUSTEDポリシーを使用して信頼できないゲスト・コードを実行できます。ホスト・コードは、相互に保護されるゲスト・コードの相互に信頼されないインスタンスを複数実行できます。このようにして、ポリグロット・サンドボックス化でマルチテナント・シナリオがサポートされます:
セキュリティ境界を導入することでメリットを得るユース・ケースは、次のとおりです:
- サード・パーティ・コードの使用(依存関係の取得)。サード・パーティ・コードは通常信頼され、使用前に脆弱性をスキャンされますが、サンドボックス化はサプライ・チェーン攻撃に対する追加の予防措置です。
- ユーザー・プラグイン。複雑なアプリケーションでは、ユーザーがコミュニティ作成のプラグインをインストールできる場合があります。従来、これらのプラグインは信頼できるとみなされ、多くの場合、完全な権限で実行されますが、理想的には、意図した場合を除いてアプリケーションに干渉できないようにする必要があります。
- サーバー・スクリプト。たとえば、共有データ・ソースにカスタム・データ処理を実装するなど、汎用スクリプト言語で表された独自のロジックを使用して、ユーザーがサーバー・アプリケーションをカスタマイズできるようにします。
サンドボックス・ポリシー
ユース・ケースおよび関連する許容可能なセキュリティ・リスクに応じて、SandboxPolicy(TRUSTEDからUNTRUSTEDまで)を選択し、より広範囲の制限と軽減策を有効にして構成できます。SandboxPolicy
は、事前構成と最終構成の検証の2つの目的を果たします。デフォルトでは、ポリシーに準拠するようにコンテキストおよびエンジンを事前構成します。構成をさらにカスタマイズする場合、ポリシーの検証により、カスタム構成が許容できないほどポリシーを弱めないことが保証されます。
TRUSTEDポリシー
TRUSTEDサンドボックス・ポリシーは、完全に信頼できるゲスト・コードを対象としています。これがデフォルト・モードです。コンテキストまたはエンジン構成に制限はありません。
例:
try (Context context = Context.newBuilder("js")
.sandbox(SandboxPolicy.TRUSTED)
.build();) {
context.eval("js", "print('Hello JavaScript!');");
}
CONSTRAINEDポリシー
CONSTRAINEDサンドボックス・ポリシーは、ホスト・リソースへのアクセスを制御する必要がある信頼できるアプリケーションを対象としています。CONSTRAINEDポリシーでは:
- コンテキストを設定する言語が必要です。
- ネイティブ・アクセスを許可しません。
- プロセス作成を許可しません。
- システムの終了を許可せず、これが言語でサポートされているVM全体の終了をゲスト・コードが行うことを禁止します。
- 標準出力およびエラー・ストリームのリダイレクションが必要です。これは、ゲスト・コードによる出力ストリームへの予期しない書込みによって外部コンポーネント(ログ処理など)が混乱するリスクを軽減するためです。
- ホスト・ファイルまたは ソケットへのアクセスを許可しません。カスタムのポリグロット・ファイル・システム実装のみが許可されます。
- 環境アクセスを許可しません。
- ホスト・アクセスを制限します:
- ホスト・クラスのロードを許可しません。
- デフォルトでは、すべてのパブリック・ホスト・クラスおよびメソッドを許可しません。
- アクセス継承を許可しません。
- 任意のホスト・クラスおよびインタフェースの実装を許可しません。
java.lang.FunctionalInterface
の実装を許可しません。- 変更可能なターゲット・タイプのホスト・オブジェクト・マッピングを許可しません。HostAccess.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ポリシー
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!');");
}
UNTRUSTEDポリシー
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", "8MB")
.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
で注釈を付ける必要があります。この注釈は継承されません。ポリグロット・ファイル・システム実装や、標準出力およびエラー・ストリーム・リダイレクション用の出力ストリーム受信者などのサービス・プロバイダは、ゲスト・コード呼出しに公開されます。
ゲスト・コードは、@Implementable
の注釈が付いたJavaインタフェースを実装することもできます。このようなインタフェースを使用するホスト・コードは、ゲスト・コードと直接対話します。
ゲスト・コードと対話するホスト・コードは、堅牢な方法で実装する必要があります:
- 入力検証。ゲストから渡されるすべてのデータ(たとえば、パラメータを介して公開されるメソッドに渡される)は信頼できないため、該当する場合はホスト・コードによって徹底的に検証する必要があります。
- 再入可能性。公開されたホスト・コードは、ゲスト・コードがいつでも起動できるため、再入可能である必要があります。コード・ブロックに
synchronized
キーワードを適用するだけでは、必ずしも再入可能になるとはかぎりません。 - スレッドセーフティ。ゲスト・コードが複数のスレッドから同時に呼び出す可能性があるため、公開されたホスト・コードはスレッドセーフである必要があります。
- リソース消費。公開されたホスト・コードは、リソースの消費を認識する必要があります。特に、信頼できない入力データに基づいてメモリーを割り当てるような構造体は、直接的にせよ、再帰など間接的にせよ、完全に回避するか、制限を実装する必要があります。
- 権限機能。サンドボックスによって適用される制限は、制限された機能を提供するホスト・メソッドを公開することによって完全にバイパスできます。たとえば、CONSTRAINEDサンドボックス・ポリシーを持つゲスト・コードは、ホスト・ファイルのIO操作を実行できません。ただし、任意のファイルへの書込みを許可するコンテキストにホスト・メソッドを公開すると、この制限が効果的に回避されます。
- サイド・チャネル。ゲスト言語によっては、ゲスト・コードがタイミング情報にアクセスできる場合があります。たとえば、Javascriptでは、
Date()
オブジェクトは詳細なタイミング情報を提供します。UNTRUSTEDサンドボックス・ポリシーでは、Javascriptタイマーの粒度は1秒に事前構成されており、100ミリ秒に短縮できます。ただし、ホスト・コードは、ゲスト・コードが実行のタイミングを計る可能性があり、ホスト・コードがシークレットに依存する処理を実行した場合にシークレット情報を検出する可能性があることに注意してください。
信頼できないゲスト・コードと対話することを認識していないホスト・コードは、前述の側面を考慮せずにゲスト・コードに直接公開しないでください。たとえば、アンチパターンは、サード・パーティ・インタフェースを実装し、すべてのメソッド呼出しをゲスト・コードに転送することです。
リソース制限
ISOLATEDおよびUNTRUSTEDサンドボックス・ポリシーでは、コンテキストのリソース制限を設定する必要があります。コンテキストごとに異なる構成を指定できます。制限を超えた場合、コードの評価は失敗し、isResourceExhausted()
に対してtrue
を返すPolyglotException
でコンテキストは取り消されます。この時点で、そのコンテキストでゲスト・コードを実行することはできなくなります
--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時間には、通常、同期またはIOの待機に費やされた時間は含まれません。すべてのスレッドのCPU時間が合算され、CPU時間制限と照合されます。これは、2つのスレッドで同じコンテキストが実行された場合、時間制限を2倍速く超えることを意味します。
時間制限は、定期的に呼び出される別の優先度の高いスレッドによって実施されます。指定した精度でコンテキストが取り消される保証はありません。ホスト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.isCancelled()
に対してtrue
を返すPolyglotException
がスローされます。文の制限は、実行されているスレッドの数とは無関係です。
この制限を負の数に設定して無効にできます。この制限が適用されるかどうかに関係なく、内部ソースはsandbox.MaxStatementsIncludeInternal
を使用してのみ構成できます。デフォルトでは、この制限には内部とマークされたソースの文は含まれません。共有エンジンを使用する場合は、1つのエンジンのすべてのコンテキストで同じ内部構成を使用する必要があります。
ゲスト言語によっては、単一の文の複雑度が一定時間ではない場合があります。たとえば、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();
}
この制限は、割当て済みバイトまたは低メモリー通知に基づいてトリガーされた保持サイズ計算によってチェックされます。
割り当てられたバイトは、定期的に呼び出される別の優先度の高いスレッドによってチェックされます。メモリー制限されたコンテキストごとにこのようなスレッドが1つ(sandbox.MaxHeapMemory
が設定されたスレッド)あります。保持バイトの計算は、必要に応じて、割り当てられたバイト・チェック・スレッドから起動されるもう1つの優先度の高いスレッドによってさらに行われます。ヒープ・メモリー制限を超えると、保持バイトの計算スレッドもコンテキストを取り消します。さらに、低メモリー・トリガーが呼び出されると、メモリー制限されたコンテキストが少なくとも1つあるエンジン上のすべてのコンテキストが、割当てチェッカとともに一時停止されます。すべての個々の保持サイズ計算は取り消されます。メモリー制限されたコンテキストごとのヒープ内の保持バイトは、単一の優先順位の高いスレッドによって計算されます。
ヒープ・メモリー制限では、コンテキストで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ミリ秒と解釈されます。
同じコンテキストの2つの保持サイズ計算の開始は、デフォルトでは少なくとも10ミリ秒離れている必要があります。これは、sandbox.RetainedBytesCheckInterval
オプションで構成できます。間隔は正数である必要があります。
コンテキストに割り当てられたバイトのチェックは、sandbox.AllocatedBytesCheckEnabled
オプションによって無効にできます。デフォルトでは、有効になっています(true)。無効にすると(false)、コンテキストの保持サイズ・チェックは、低メモリー・トリガーによってのみトリガーできます。
ホストVM全体のヒープに割り当てられたバイトの合計数がVMのヒープ・メモリーの合計の特定の係数を超えると、低メモリー通知が起動され、次のプロセスが開始されます。sandbox.MaxHeapMemory
オプションが設定されている実行コンテキストが1つ以上あるすべてのエンジンの実行は一時停止され、メモリー制限されたコンテキストごとにヒープ内の保持バイトが計算され、その制限を超えるコンテキストは取り消されて、実行が再開されます。デフォルトの係数は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サンドボックス・ポリシーによって適用される主な防御は、ポリグロット・エンジンが専用のnative-image
分離で動作し、ゲスト・コードの実行を、独自のヒープ、ガベージ・コレクタ、およびJITコンパイラを使用する、ホスト・アプリケーションとは別のVMレベルのフォルト・ドメインに移動することです。
ゲストのヒープ・サイズによってゲスト・コードのメモリー消費にハード制限を設定する以外に、ゲスト・コードのみに実行時防御を集中させることができ、ホスト・コードのパフォーマンスの低下は発生しません。実行時防御は、engine.UntrustedCodeMitigation
オプションによって有効になります。
定数ブラインディング
JITコンパイラでは、ユーザーがソース・コードを提供し、ソース・コードが有効であればマシン・コードにコンパイルします。攻撃者の視点から見ると、JITコンパイラは、攻撃者が制御する入力を実行可能メモリー内の予測可能なバイトにコンパイルします。JITスプレーと呼ばれる攻撃では、攻撃者が悪意のある入力プログラムをJITコンパイラにフィードすることによって予測可能なコンパイルを活用し、Return-Oriented Programming (ROP)ガジェットを含むコードを強制的に生成させます。
入力プログラム内の定数は、JITコンパイラがマシン・コードにそのまま含めることが多いため、このような攻撃のターゲットとして特に魅力的です。定数ブラインディングは、コンパイル・プロセスにランダム性を導入することで、攻撃者の予測を無効にすることを目指しています。具体的には、定数ブラインディングは、コンパイル時にランダムなキーで定数を暗号化し、実行時に出現するたびに復号化します。マシン・コードには、暗号化されたバージョンの定数のみがそのまま出現します。ランダム・キーの知識がないと、攻撃者は暗号化された定数値を予測できないため、実行可能メモリー内の結果のバイトを予測できなくなります。
GraalVMは、実行時にコンパイルされたゲスト・コードのコード・ページに埋め込まれたすべての即時値とデータを、4バイトのサイズまでブラインドします。
投機的実行攻撃の軽減
Spectreなどの投機的実行攻撃は、CPUが分岐予測情報に基づいて命令を一時的に実行する可能性があるという事実を悪用します。予測が誤っている場合、これらの命令の結果は破棄されます。ただし、実行によって、CPUのマイクロアーキテクチャ状態に副作用が生じる可能性があります。たとえば、一時的な実行中にデータがキャッシュに取り込まれている場合があります。これは、データ・アクセスのタイミングを合せることで読み取ることができるサイド・チャネルです。
GraalVMは、実行時にコンパイルされたゲスト・コードに投機的実行バリア命令を挿入し、攻撃者が投機的実行ガジェットを作成できないようにすることでSpectre攻撃から保護します。投機的実行バリアは、パターン履歴表に基づいた投機的実行(Spectre V1)を停止するために、条件分岐の各ターゲットに配置されます。投機的実行バリアは、分岐先バッファに基づいた投機的実行(Spectre V2)を停止するために、間接分岐の可能性がある各分岐先にも配置されます。
実行エンジンの共有
異なるトラスト・ドメインのゲスト・コードは、ポリグロット・エンジン・レベルで分離する必要があります。つまり、同じトラスト・ドメインのゲスト・コードのみがエンジンを共有する必要があります。複数のコンテキストで1つのエンジンを共有する場合、それらのすべてに同じサンドボックス・ポリシー(エンジンのサンドボックス・ポリシー)が必要です。アプリケーション開発者は、パフォーマンス上の理由から、実行コンテキスト間で実行エンジンを共有することを選択できます。コンテキストは実行されたコードの状態を保持しますが、エンジンはコード自体を保持します。複数のコンテキスト間で実行エンジンを共有することは明示的に設定する必要があり、多数のコンテキストが同じコードを実行するシナリオではパフォーマンスを向上させることができます。共通コードの実行エンジンを共有するコンテキストが機密(つまり、プライベート)コードも実行するシナリオでは、対応するソース・オブジェクトをコード共有からオプトアウトできます:
Source.newBuilder(…).cached(false).build()
互換性と制限
ポリグロット・サンドボックス化は、GraalVM Community Editionでは使用できません。
サンドボックス化ポリシーに応じて、Truffle言語、インストゥルメントおよびオプションのサブセットのみを使用できます。特に、サンドボックス化は現在、ランタイムのデフォルト・バージョンのECMAScript (ECMAScript 2022)でのみサポートされています。サンドボックス化は、GraalVMのNode.js内からもサポートされていません。
ポリグロット・サンドボックス化は、VMの動作を変更するシステム・プロパティなどによるVM設定への変更と互換性がありません。
サンドボックス化ポリシーでは、セキュア・バイ・デフォルトの状態を維持するために、GraalVMのメジャー・リリース間で互換性のない変更が行われることがあります。
ポリグロット・サンドボックス化では、オペレーティング・システムや基礎となるハードウェアの脆弱性など、その動作環境の脆弱性から保護することはできません。対応するリスクから保護するために、適切な外部分離プリミティブを採用することをお薦めします。
Javaセキュリティ・マネージャとの差別化
Javaセキュリティ・マネージャは、JEP-411のJava 17で非推奨になりました。セキュリティ・マネージャの目的は次のように述べられています: 「セキュリティ・マネージャを使用すると、安全でない可能性がある操作や機密性の高い操作を実行する前に、その操作が何であるか、その操作が実行可能なセキュリティ・コンテキストで試行されているかどうかをアプリケーションが判断できるようになります。」
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にレポートを送信してください。セキュアな電子メールの公開暗号化キーなどの詳細は、脆弱性のレポートを参照してください。報告については、プロジェクトのコントリビュータに直接連絡したり、他のルートを通じて連絡しないようお願いします。