Salesforce Developers Japan Blog

ShadowRealmの概要

オリジナル記事

Introducing ShadowRealm

 

SalesforceのStandardsおよびWeb Platformチームは、Webブラウザーでネイティブに実行できる新機能の構築を支援しています。

2022年4月27日

 

SalesforceのStandardsおよびWeb Platformチームは、Webブラウザーでネイティブに実行できる新機能の構築を支援しています。今回のブログでは、EcmaScriptの新しいShadowRealm APIがSalesforceのセキュリティと整合性のメカニズムをいかに改善するか、また、このAPIがLightning Web Securityなどの仮想化フレームワークを構成する方法についてご説明します。

他のプラットフォームと同様に、Webも多くのアプリケーションをさまざまな方法で構成することで、多様なソースから創造的なエクスペリエンスを実現する可能性を創出しようとしています。Salesforce PlatformにおけるWeb活用にあたっては、集合的な構成という概念が中核にあります。これは、Salesforceのお客様がSalesforceに非常に創造的なカスタマイズを施し、エクスペリエンスを他のお客様と共有しようとする際に起こります。このような構成物は最終的には複数のソースによるプログラムへと行き着きます。ソースはさまざまなチームやベンダーの可能性があり、環境による要件もさまざまですが、これらすべてが同時にSalesforce Platformに接続されます。

Webアプリケーションでは、すべての要素が単一ルートのグローバル環境の中で共有されますが、これはメインのWindowオブジェクトに表示されます。Salesforceアプリケーションのコアはブラウザーのここで実行され、顧客によってカスタマイズされたものも含め、多くのコンポーネントと接続されます。これが全体として実行する際の整合性を確保する基礎であり、お客様の構成に使用される各部においてセキュリティを維持します。

アプリケーションのフットプリント設定においては、グローバルなスコープを超えたり、利用可能なビルトインオブジェクトで実施したりすることができます。こうした修正は、名前の追加からグローバル(jQueryの$など)、一般的な方法のパッチ(Array.prototype.sortへのカスタムの動作の追加など)まで、さまざまに実施される場合もあります。これにより、多くのコンポーネントやライブラリで構成されたアプリケーションの整合性に、影響が生じる可能性があります。

 

ShadowRealmとは

ShadowRealmは、新しいGlobalオブジェクトや一連のビルトインオブジェクトの新たに進化したコンテキストを作成できるよう設計されており、グローバルリソースをアプリケーション内の他の部分と共有または汚染することなくJavaScriptを実行するための仕組みを提供します。

このような仕組みにより、ShadowRealmコンテキストが作成された周囲のコンテキストと同じヒープでJavaScriptコードを実行できます。コードは同期して実行されるため、ShadowRealmインスタンス内部で実行されるこのコードによりアクセスされるDOM APIを仮想化することができます。仮想化のフレームワークがShadowRealmとWindow要素の間で通信する際には、この仕組みに依存しています。

const sr = new ShadowRealm();

// Sets a new global within the ShadowRealm only
sr.evaluate('globalThis.x = "my shadowRealm"');

globalThis.x = "root"; //

const srx = sr.evaluate('globalThis.x');

srx; // "my shadowRealm"
x; // "root"

ShadowRealmインスタンスは、JavaScriptのプリミティブ値(String、Number、BigInt、Symbol、Boolean、undefined、null)のやり取りのみ可能で、レルムの境界をまたいだオブジェクトのやり取りはすべて禁止されています。これが重要なのは、オブジェクトは作成したレルムのIDのリファレンスを伝達するため、情報の漏えいに使用されたり、IDの不連続性の問題を引き起こしたりすることがあるからです。たとえば、メインWindowのArrayコンストラクターは、ShadowRealmを含む他のレルムのArrayコンストラクターとは異なります。Arrayオブジェクトはそれぞれのレルムのコンストラクターのみのインスタンスです。

しかしShadowRealmインスタンスは、関数の値をラップし、「共有」することができます。これにより、ShadowRealm間でのやり取りのために、堅牢な通信チャネルを構築できます。

const sr = new ShadowRealm();

const wrappedFn = sr.evaluate('(x) => globalThis.foo = x');

wrappedFn(42);

globalThis.foo; // undefined

sr.evaluate('globalThis.foo'); // 42

ラップされた関数は他の関数を送信し、受信したレルム内でラップさせることが可能です。

const sr = new ShadowRealm();

const wrappedFn = sr.evaluate('(x) => globalThis.foo = x');

// The wrapped function received by the shadowRealm will chain the call
// to the arrow function created in this root realm
wrappedFn((y) => globalThis.bar * y);

// The shadow realm just wrapped the arrow function above and
// set it as the value of its respective globalThis.foo

globalThis.bar = 3;
sr.evaluate('globalThis.bar = 0');

// When the sr's `foo` is called, it will call the arrow function 
// in this realm and reflect its return vaue.
sr.evaluate('globalThis.foo(2)'); // 6

このような通信は同期的で、これを活用してページ内の要素のステータスをただちに確認したり、アプリケーションの正確な状態を把握したりできます。

グローバルオブジェクトは共有されないため、複数のコンポーネントが標準のビルトインオブジェクト上にカスタムの予測しない値を設定する問題が低減されます。

const sr = new ShadowRealm();

// Removes the Array constructor from the global object within this instance
sr.evaluate('delete globalThis.Array;'); // true

// The current Array constructor is not affected
typeof Array; // "function"

 

文字列を評価せずにShadowRealmを使用する

ShadowRealmはJavaScriptコードを評価するための新しい仕組みを導入しているわけではありません。ShadowRealm.prototype.evaluateは、コードがそれぞれのShadowRealmインスタンスで実行された状態で、indirect eval()に対して同様に動作します。すなわち、コードの評価はメインページと同様、既存のコンテンツセキュリティポリシー(CSP)に従う必要があるということです。たとえば、unsafe-evalShadowRealm.prototype.evaluateの使用を制限します。

コードの文字列を評価できない場合は、ShadowRealmインスタンスにコードを挿入する別の方法があります。ShadowRealm.prototype.importValueにより、動的なモジュールのインポート(import()表現内として)でモジュールの読み込みとラップされた関数を含むエクスポート値のキャプチャを実現できます。

const sr = new ShadowRealm();

const specifier = './foo.js';
const name = 'sum';

// importValue returns a promise that will eventually be resolved with
// the value specified in the given module name.
const shadowSum = await sr.importValue(specifier, name);

shadowSum(1); // runs an operation within the shadowRealm and captures the result

モジュールはレルムごとに評価されます。すなわち、修正により値が共有されることはなく、さまざまなレルムからグローバルセットを監視します。たとえば、./foo.jsのモジュールは以下のようなコードとなります。

globalThis.total = 0;

export function sum(n) {
  return globalThis.total += n;
}

export function getTotal() {
  return globalThis.total;
}

この場合、shadowSumがShadowRealm内部で読み込まれたsumをラップし、呼び出された場合にはそれぞれのShadowRealm内部からのglobalThis.totalにのみ影響を及ぼします。同様に、モジュール./foo.jsがページレルムなど他のレルムで読み込まれた場合は、sum関数は読み込まれた各モジュールの監視を行います。

const sr = new ShadowRealm();

const specifier = './foo.js';
const name = 'sum';

const [ shadowSum, shadowGetTotal ] = await Promise.all([
    sr.importValue(specifier, name),
    sr.importValue(specifier, 'getTotal')
]);

globalThis.total = 0;

shadowSum(10); // 10
shadowSum(20); // 30
shadowSum(30); // 60

globalThis.total; // 0
shadowGetTotal(); // 60

const { sum, getTotal } = await import(specifier);

sum(42); // 42
globalThis.total; // 42

// The value from the shadow realm is preserved
shadowGetTotal(); // 60

importValueは、ShadowRealmインスタンスとの通信チャネルを設定する開始点として、所定のモジュールから値のインポートを要求するよう、意図的に設計されています。オブジェクトは現在ShadowRealmの境界をまたぐことが認められていないため、import()の正規表現など、モジュールのネームスペースオブジェクトを返すことはありません。

evaluateメソッドはunsafe-evalなどのCSPの規制に従うため、importValueメソッドはdefault-srcなどのページに設定されたCSPの規制に従います。こうした指示がある場合は、APIはこのようにコードを読み込むことはできません。

 

ローレベルコードと仮想化

ShadowRealm APIに含まれるメソッドは2つのみで、ローレベルコードでの使用のために設計されています。ShadowRealmは仮想化の基盤やコードをサンドボックス化するシステムとして使用されることがよくあります。ShadowRealm APIは、最終ユーザが直接使用するようには設計されていません。Lightning Web Security(LWS)などのフレームワークレイヤーが最上部に必要です。LWSはメンブレンフレームワークを使って、ShadowRealmsとメインアプリケーションWindowをまたぐ値の実行や状態を通信します。

公開されたビルトインAPIのサーフェスも、メインページやiFrameで見られるトップレベルのWindowオブジェクトと比較すると、小規模になる傾向があります。ShadowRealmインスタンスのグローバルオブジェクトはすべてのプロパティが設定可能な普通のオブジェクトです。大雑把に言えば、これはあらゆるグローバルプロパティを削除できるということで、window.topwindow.locationなど、iFrameの評判の悪い偽造不可能性を持つグローバルプロパティとは対照的です。これらのプロパティはiFrameのグローバルに常に表示され、削除することはできません。ShadowRealmは偽造不可能性を持つ値を追加しないだけではなく、効果的に削除できるようホストに提供されたすべての値を要求するルールを設定することで、これを防止します。

新しいAPIはモジュールに対応するだけではなく、低フットプリントからフットプリントがない状態までで実行されるDOM操作の仮想化のためにWebで適切に使用されるよう、コードの実行によりクリーンなキャンバスのメリットを利用するため、境界を設定します。これは、クリーンなページにおける整合性の懸念がなく実行されるコードとは区別されます。ShadowRealm APIは、メンブレンフレームワークや、Webで利用できるその他のカプセル化の要素に対する、有用かつ強力な基盤となり得るのです。

 

ShadowRealmのロードマップ

このような議論では、メンブレンフレームワークを追加する基準の引き下げや、メンブレンフレームワークの直接的な標準化といった方向性に向かうことが多く、ShadowRealmには改善の余地が多く残されています。また、Web Workersなど、Webで現在すでに存在するスマートなソリューションを提供するため、オブジェクトのシリアル化を実現するというアイデアに重点が置かれることがあります。さらに、ShadowRealmの境界をまたいでPromise、イテレーター、非同期イテレーターのラップや展開を行うための仕組みの構築について触れることもあります。

こうしたアイデアはすべて重要で、Webのエコシステムの標準的なプロセスとして欠かすことはできない機能です。SalesforceのStandardsおよびWeb Platformチームは、JavaScript言語の重要な要素として、ShadowRealmのさらなる機能強化に継続的に取り組んでいます。TC39のGitHubリポジトリでは、ShadowRealms API、解説問題についてのスレッド、仕様の現在のレンダリングされたバージョンが公開されています。

 

著者紹介

Leo Balter(写真左)は、SalesforceのStandardsおよびWeb Platformを推進する、シニア製品マネージャーです。TC39の代表者であるLeoは、現在ShadowRealm APIがECMAScriptに組み込まれるようサポートを提供しています。Leoの趣味は、何本も所有するギターで、ジャズのスタンダードナンバーを演奏することです。

 

Rick Waldron(写真右)はLightning Web Securityのリードソフトウェアエンジニアで、ShadowRealm APIのもう1人の推進者でもあります。RickはJavaScriptの公式テストスイートであるTest262を維持する中心人物として、すべてのJavaScriptの標準に対する充分な検証を実施しています。彼も、さまざまなギターを演奏することを趣味としています。

トピック:

コメント

ShadowRealmの概要