サンドボックス化

GraalVMは、JVMベースの言語で記述されたホストアプリケーションが、ポリグロットAPI を介してJavaScriptで記述されたゲストコードを実行できるようにします。サンドボックスポリシー を構成することで、ホストアプリケーションとゲストコード間のセキュリティ境界を確立できます。たとえば、ホストコードは、UNTRUSTED ポリシーを使用して、信頼できないゲストコードを実行できます。ホストコードは、互いに不信頼な複数のゲストコードインスタンスを実行し、それらを互いに保護することもできます。このように使用することで、サンドボックス化はマルチテナントシナリオをサポートします。

Sandbox Security Boundary

セキュリティ境界の導入から恩恵を受けるユースケースは次のとおりです。

  • サードパーティコードの使用、つまり依存関係の取り込み。サードパーティコードは通常、使用前に信頼され、脆弱性のスキャンが行われますが、サンドボックス化はサプライチェーン攻撃に対する追加の予防策となります。
  • ユーザープラグイン。複雑なアプリケーションでは、ユーザーがコミュニティが作成したプラグインをインストールできる場合があります。従来、これらのプラグインは信頼されており、多くの場合、完全な権限で実行されますが、理想的には、意図しない限り、アプリケーションに干渉してはならないはずです。
  • サーバースクリプティング。ユーザーが汎用スクリプト言語で独自のロジックを表現することで、サーバーアプリケーションをカスタマイズできるようにします(例:共有データソースでカスタムデータ処理を実装するため)。

サンドボックスポリシー #

ユースケースと関連する許容されるセキュリティリスクに応じて、SandboxPolicy を選択できます。TRUSTED からUNTRUSTED まで、制限と軽減策の範囲が広がります。SandboxPolicy は、最終的な構成の事前設定と検証の2つの目的を果たします。デフォルトでポリシーに準拠するようにコンテキストとエンジンを事前に設定します。構成がさらにカスタマイズされた場合、ポリシーの検証により、カスタム構成によってポリシーが許容できないほど弱体化されないことが保証されます。

信頼済みポリシー #

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 でアノテーションを付ける必要があります。このアノテーションは継承されません。ポリグロットAPIファイルシステム の実装や、標準出力とエラーストリームのリダイレクトの出力ストリーム受信者などのサービスプロバイダーは、ゲストコード呼び出しに公開されます。

ゲストコードは、@Implementable でアノテーションされたJavaインターフェースを実装することもできます。このようなインターフェースを使用するホストコードは、ゲストコードと直接やり取りします。

ゲストコードとやり取りするホストコードは、堅牢な方法で実装する必要があります。

  • 入力検証。ゲストから渡されたすべてのデータ(たとえば、公開されたメソッドのパラメーターを介して)は信頼できず、該当する場合はホストコードによって徹底的に検証する必要があります。
  • リエントランシー。公開されたホストコードは、ゲストコードがいつでも呼び出す可能性があるため、リエントラントである必要があります。コードブロックにsynchronizedキーワードを適用するだけでは、必ずしもリエントラントになるとは限りませんのでご注意ください。
  • スレッドセーフティ。公開されたホストコードは、ゲストコードが同時に複数のスレッドから呼び出す可能性があるため、スレッドセーフである必要があります。
  • リソース消費。公開されたホストコードは、そのリソース消費量を認識している必要があります。特に、信頼できない入力データに基づいて、直接的または間接的に(たとえば、再帰を通して)メモリを割り当てる構造は、完全に回避するか、または制限を実装する必要があります。
  • 特権機能。サンドボックスによって強制される制限は、制限された機能を提供するホストメソッドを公開することによって完全にバイパスされる可能性があります。たとえば、CONSTRAINEDサンドボックスポリシーを持つゲストコードは、ホストファイルのIO操作を実行できません。しかし、コンテキストに任意のファイルへの書き込みを許可するホストメソッドを公開すると、この制限を効果的にバイパスできます。
  • サイドチャネル。ゲスト言語によっては、ゲストコードがタイミング情報にアクセスできる場合があります。たとえば、Javascriptでは、Date()オブジェクトはきめ細かいタイミング情報を提供します。UNTRUSTEDサンドボックスポリシーでは、Javascriptタイマーの粒度は1秒に事前設定されており、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時間には、通常、同期またはIOを待機している時間は含まれません。すべてのスレッドのCPU時間が追加され、CPU時間制限に対してチェックされます。これは、2つのスレッドが同じコンテキストを実行する場合、時間制限が2倍の速さで超過することを意味する可能性があります。

時間制限は、定期的にウェイクアップされる別の高優先度スレッドによって強制されます。コンテキストが指定された精度内でキャンセルされる保証はありません。たとえば、ホストVMがフルガベージコレクションを引き起こす場合、精度は大幅にずれる可能性があります。時間制限が決して超過しない場合、ゲストコンテキストのスループットは影響を受けません。1つのコンテキストで時間制限を超過した場合、同じ明示的なエンジンを持つ他のコンテキストのスループットが一時的に低下する可能性があります。

時間の長さを指定するための利用可能な単位は、ミリ秒の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を使用して構成できます。デフォルトでは、制限には内部としてマークされたソースのステートメントは含まれません。共有エンジンを使用する場合は、エンジンのすべてのコンテキストで同じ内部構成を使用する必要があります。

単一ステートメントの複雑さは、ゲスト言語によっては一定時間ではない可能性があります。たとえば、Array.sortなどのJavascriptビルトインを実行するステートメントは、1つのステートメントを占める可能性がありますが、その実行時間は配列のサイズに依存します。

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が設定されているコンテキスト)ごとに、このようなスレッドが1つあります。保持バイトの計算は、必要に応じて割り当てられたバイトチェックスレッドから開始される別の高優先度スレッドによって実行されます。保持バイト計算スレッドは、ヒープメモリ制限を超えた場合にもコンテキストをキャンセルします。さらに、低メモリトリガーが呼び出されると、メモリ制限付きコンテキストが少なくとも1つあるエンジン上のすべてのコンテキストは、それらの割り当てチェッカーとともに一時停止されます。個々の保持サイズ計算はすべてキャンセルされます。メモリ制限付きコンテキストごとにヒープ内の保持バイトは、単一の高優先度スレッドによって計算されます。

ヒープメモリ制限は、コンテキストがOutOfMemoryエラーを引き起こすのを防ぎません。短時間で多くのオブジェクトを割り当てるゲストコードは、めったにオブジェクトを割り当てないコードと比較して、精度が低くなります。

コンテキストの保持サイズ計算は、以下の専門家向けオプションsandbox.AllocatedBytesCheckIntervalsandbox.AllocatedBytesCheckEnabledsandbox.AllocatedBytesCheckFactorsandbox.RetainedBytesCheckIntervalsandbox.RetainedBytesCheckFactor、およびsandbox.UseLowMemoryTriggerを使用してカスタマイズできます。

コンテキストの保持サイズ計算は、保持バイトの推定値が指定されたsandbox.MaxHeapMemoryの特定の係数を超えたときにトリガーされます。この推定値は、コンテキストがアクティブであったスレッドによってヒープメモリに割り当てられたに基づいています。より正確には、この推定値は、利用可能な場合、以前の保持バイト計算の結果と、以前の計算の開始以降に割り当てられたバイトの合計です。デフォルトでは、sandbox.MaxHeapMemoryの係数は1.0であり、sandbox.AllocatedBytesCheckFactorオプションでカスタマイズできます。係数は正の数値である必要があります。たとえば、sandbox.MaxHeapMemoryが100MBで、sandbox.AllocatedBytesCheckFactorが0.5の場合、割り当てられたバイトが50MBに達すると、保持サイズ計算が最初にトリガーされます。計算された保持サイズが25MBの場合、次の保持サイズ計算はさらに25MBが割り当てられるとトリガーされます。

デフォルトでは、割り当てられたバイトは10ミリ秒ごとにチェックされます。これは、sandbox.AllocatedBytesCheckIntervalで構成できます。可能な最小間隔は1msです。それより小さい値は1msとして解釈されます。

同じコンテキストの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サンドボックスポリシーによって適用される主な防御は、Polyglotエンジンが専用のnative-imageアイソレートで実行されることであり、ゲストコードの実行をホストアプリケーションとは別のVMレベルのフォールトドメイン(独自のヒープ、ガベージコレクタ、JITコンパイラを持つ)に移行します。

ゲストコードのメモリ消費量にゲストのヒープサイズを介してハード制限を設定することに加えて、実行時防御をゲストコードのみに集中させ、ホストコードのパフォーマンス低下を引き起こさないようにすることもできます。実行時防御は、engine.UntrustedCodeMitigationオプションによって有効になります。

定数ブラインディング #

JITコンパイラを使用すると、ユーザーはソースコードを提供し、ソースコードが有効であれば、それをマシンコードにコンパイルできます。攻撃者の観点から見ると、JITコンパイラは攻撃者によって制御された入力を、実行可能メモリ内の予測可能なバイトにコンパイルします。JITスプレーと呼ばれる攻撃では、攻撃者は予測可能なコンパイルを利用して、悪意のある入力プログラムをJITコンパイラに供給し、Return-Oriented Programming(ROP)ガジェットを含むコードを出力するように強制します。

入力プログラム内の定数は、このような攻撃の特に魅力的なターゲットです。JITコンパイラは、多くの場合、それらをマシンコードにそのまま含むためです。定数ブラインディングは、コンパイルプロセスにランダム性を導入することで、攻撃者の予測を無効にすることを目的としています。具体的には、定数ブラインディングは、コンパイル時にランダムキーで定数を暗号化し、実行時に各出現時に復号化します。マシンコードには、定数の暗号化されたバージョンのみがそのまま表示されます。ランダムキーを知らない限り、攻撃者は暗号化された定数値を予測できず、そのため、実行可能メモリ内の結果のバイトを予測できなくなります。

GraalVMは、実行時にコンパイルされたゲストコードのコードページに埋め込まれたすべての即値とデータを、4バイトのサイズまでブラインド化します。

ランダム化された関数エントリポイント #

予測可能なコードレイアウトにより、攻撃者は、前述のJITスプレー攻撃などによって導入されたガジェットを簡単に検出できます。実行時にコンパイルされたメソッドは、既にオペレーティングシステムによってアドレス空間配置ランダム化(ASLR)の対象となるメモリに配置されていますが、GraalVMはさらに、関数の開始オフセットにランダムな数のトラップ命令を追加してパディングします。

投機的実行攻撃の軽減策 #

Spectreなどの投機的実行攻撃は、CPUが分岐予測情報に基づいて一時的に命令を実行する可能性があるという事実を利用しています。誤予測の場合、これらの命令の結果は破棄されます。ただし、実行によってCPUのマイクロアーキテクチャの状態に副作用が発生している可能性があります。たとえば、一時的な実行中にデータがキャッシュに読み込まれている可能性があり、これはデータアクセスをタイミング測定することで読み取ることができるサイドチャネルです。

GraalVMは、実行時にコンパイルされたゲストコードに投機的実行バリア命令を挿入することで、攻撃者が投機的実行ガジェットを作成するのを防ぎ、Spectre攻撃から保護します。Javaのメモリ安全性に関連する条件付き分岐の各ターゲットに投機的実行バリアが配置され、投機的実行が停止されます。

実行エンジンの共有 #

異なる信頼ドメインのゲストコードは、ポリグロットエンジンレベルで分離する必要があります。つまり、同じ信頼ドメインのゲストコードのみがエンジンを共有する必要があります。複数のコンテキストがエンジンを共有する場合、すべてが同じサンドボックスポリシー(エンジンのサンドボックスポリシー)を持っている必要があります。アプリケーション開発者は、パフォーマンス上の理由から、実行コンテキスト間で実行エンジンを共有することを選択できます。コンテキストは実行されたコードの状態を保持しますが、エンジンはコード自体を保持します。複数のコンテキスト間での実行エンジンの共有は明示的に設定する必要があり、多くのコンテキストが同じコードを実行するシナリオではパフォーマンスを向上させることができます。実行エンジンを共通コードのために共有するコンテキストが機密(プライベート)コードも実行するシナリオでは、対応するソースオブジェクトは、以下を使用してコード共有からオプトアウトできます。

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にレポートを送信してください。安全なメールのための公開暗号化キーを含む追加情報については、脆弱性の報告を参照してください。レポートについて、プロジェクトの貢献者に直接、またはその他のチャネルを通じて連絡しないようお願いします。

お問い合わせ