WebアプリケーションのSandboxを実現するLockerServiceの概要とその課題

本記事では、Salesforceが提供する「[LockerService](https://developer.salesforce.com/blogs/developer-relations/2016/04/introducing-lockerservice-lightning-components.html)」というWebアプリケーションにおけるSandboxを実現する機構について、それが産まれた背景と技術的な概要、および抱えている課題についてとりあげます。

## 背景

### エンタープライズシステムにおけるWebアプリケーションとコンポーネント流通

コンポーネントを利用した開発をWebアプリケーションに導入しようという試みは、特にユーザインターフェースの構築における設計と開発の効率化やコードの再利用などを目的として、かなり古くからなされてきました。特に最近ではアプリケーションをSPAで開発するのが市民権を得てきており、ユーザインターフェースをコンポーネントベースで組み立てるライブラリ／フレームワークがもてはやされるようになっています。

もちろん、このようなコンポーネント指向の開発は、あくまでアプリケーション開発者の側でのコードの再利用がメインの話です。一方、開発におけるコンポーネント化の話とは別のレイヤーの話として、ユーザに対して提供するアプリケーションをコンポーネント化して、もっとユーザに近いレベルでアプリケーション機能の再利用を実現しよう、という動きもあります。

エンタープライズでの情報システムにおいて、導入にかかるコストを抑制するためには、一から組み立てるのではなく既存パッケージを活用するべき、というのは異論のないところでしょうが、組織のビジネスをユニークたらしめるためにはある程度のカスタマイズは必要になってきます。そのため、いくつかのエンタープライズ向けパッケージアプリケーションでは、モノリシックな形ではなく、あとでコンポーネントとして再構成することが可能なように細分化して設計され、それらを設定やカスタムコードによって組み合わせられるような仕組みを備えていたりします。

アプリケーションがコンポーネントとして提供され、再構成して動作をカスタマイズできるのであれば、そもそも単一のベンダー提供するコンポーネントだけではなく、多彩なベンダーが提供するコンポーネントも同時に組み合わせることはできないでしょうか。オープンにすることでアプリケーションはプラットフォームとなり、可能性は大きく広がることになります。これが成功した暁には、再利用可能なコンポーネントをマーケットプレイスで提供して流通させる、といったコンポーネント流通の仕組みまで構想にはいってきます。

ただ、残念ながら、こういったコンポーネント流通の仕組みのうち、完全にプラットフォーム非依存でかつちゃんとした規模のあるビジネスに発展しているものは、おそらく今現在皆無といえます。

### 複数ベンダーによるコンポーネントとWebアプリケーションのセキュリティ

こういったコンポーネントの流通が実現した世の中を仮定した時、コンポーネントの中でも特にユーザインターフェースに関するコンポーネントを扱うには、プラットフォームがWebアプリケーションであるということを強く意識しておかなければなりません。

Webアプリケーションでは、ユーザーインターフェースは最終的にHTMLで表現されることになります。また、その表示制御に関わる部分は、JavaScriptで記述されます。このような環境でユーザインターフェースを構成するコンポーネントが複数のベンダーによって提供されることになると、以下のような問題が発生することが予想されます。

- あるコンポーネントが他のコンポーネントの利用しているリソースとコンフリクトしてしまい、予想外の動作を引き起こす（e.g. グローバル変数名、DOM要素のidやクラス名がバッティングする、など）
- コンポーネントが必要とする以上のアクセス権限がコンポーネントに許可されてしまい、悪意のあるコンポーネントのアクセスを許してしまう（e.g. document.cookieを参照できる、など）

上記のような問題は、特にエンドユーザが自身でコンポーネントを選択してアセンブルすることをプラットフォームが許している場合に顕著になります。

本来、複数ベンダーのプログラムが入り交じるようなプラットフォームでは、プラットフォームがSandboxとなるコンテナを持っているのが理想です。たとえばスマートフォンなどのモバイルOSでは、インストールしたアプリケーションごとのデータ領域は隔離され、OSが管理するリソースにも限定的にしかアクセスできないようになっています。

Webアプリケーションでユーザ主導で組み立て可能なコンポーネントの仕組みを取り入れるには、まずこのようなSandboxをどのように実現するかを解決しなければいけません。

なお、古典的には、WebアプリケーションでのSandboxの実現にはよくIFRAMEを使った方法が利用されます。例えばOpenSocialなどでは、IFRAME内に区切ったコンポーネントを「ガジェット」として、ドメインを分けたIFRAMEの中に分離して表示し、コンポーネントからアクセス可能なリソースを分離しています。この方式の利点は、ブラウザのセキュリティ機構（=Same Origin Policy）によって確実にアクセス制御が保証されているという点です。さらに現在ではフレーム間でのメッセージ通信も[規格化されており](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)、コンポーネント間／コンポーネント=コンテナ間の制御された協調動作も可能になりました。

## LockerServiceの概要

### Salesforceの「Lightning」

さて、本題のLockerServiceに入る前に、まずLightningの話をしなければなりません。

Lightningを一言で言おうとすると、Salesforceが提唱するWebアプリケーションコンポーネントの仕組みになります。ただし、独自の仕様かつ実質上プロプライエタリな彼らのクラウド環境での動作のみを念頭においているものです。

一応断っておきますと、このことはそれ単独では本記事の批判対象ではありませんし、先に述べたように完全に技術的に中立でかつビジネス的に成功したWebアプリケーションコンポーネントのプラットフォームは、少なくとも現時点まですべて試みは失敗してきています。ゆえに彼らのこの選択についてはある程度の理解を個人的には持っています。

さて、このようなLightningですが、容易に予想されるようにやはり再利用可能なコンポーネント流通によるエコシステムの実現を構想として掲げています。すでにSalesforceの持つアプリケーションマーケットプレイスの仕組みであるAppExchangeは（コンシューマ市場におけるAppleのAppStoreとかGoogle Playほどではないですが）ある程度のビジネス規模になっていますので、これは極めて自然でしょう。そのため、彼らも複数ベンダーのコンポーネントが同一ページで組み合わされることによって発生しうるセキュリティ上の問題に対してはちゃんと考える必要がありました。

しかし、そこで彼らは何を考えたのか（いや何を考えたかはまあ分かるのですが）王道とも言えるIFRAMEによるコンポーネント隔離を**選択しませんでした**。その代わり彼らが発表したのが、表題の「LockerService」という機構です。

### なぜIFRAMEではだめだったのか

LightningにおいてIFRAMEによるコンポーネント隔離を良しとしなかった理由は、おそらく以下のような理由になります。

#### IFRAMEではリソースが多く消費される

IFRAMEを使う場合、内部でWindowオブジェクトを生成しますが、これは一般的にメモリ確保の面で初期化コストが高い処理とされています。特にモバイルブラウザなどではメモリ消費が馬鹿になりません。あまり多用するのは避けたいところです。

#### IFRAMEではLook and Feelの統一ができない

フレーム内では、CSSなどは別個に適用され、親フレームの影響はありません。これはコンポーネントの隔離という意味では望ましいことではありますが、一方でプラットフォームで統一したLook and Feelを提供しようとするときにはネックになります。

#### IFRAMEでは矩形内に表示が固定される

IFRAMEはその名の通りフレーム（枠）として描かれます。つまり、矩形枠からはみ出して描画するということができません。
たとえば日付カレンダーをHTMLで表示するドロップダウンメニューをコンポーネントとして作成したとすると、これがIFRAME内で表示されると以下のようになってしまいます。

<img width="168" alt="React_Storybook.png" src="https://qiita-image-store.s3.amazonaws.com/0/6495/589c4abd-6855-7ad4-89fa-48b540b5f834.png">
↓
<img width="88" alt="React_Storybook.png" src="https://qiita-image-store.s3.amazonaws.com/0/6495/40ce25e6-92b1-05b9-1463-e94787968808.png">

フレームに隠れるところを表示できるようにするだけであれば、フレームの高さを動的に伸長したりといった対策はできなくもないですが、オーバーレイなど本質的に難しい課題もあり、ちょっときびしいところです。

### LockerServiceのSandboxアプローチ

これらの点から彼らがIFRAMEではないもうひとつのSandbox方式として提唱したのが「LockerService」ということになります。

LockerServiceが達成するものは以下の機構になります。

- コンポーネントは、特定の範囲内のDOMにしかアクセスできないように制御される。
- コンポーネントは、JavaScriptのグローバル変数なども含めて、他のコンポーネントから隔離される。
- すべてのコンポーネントは、最終的に同一DOMツリー内に描画される。IFRAMEは使わない。

ではこれをどうやって実現するのか、という話になります。以下、詳細を解説します。

LockerService下のコンポーネントは、Secure DOMとよばれる仮想DOM（＝React等のそれとは全く意味が異なるので注意）を介してのみDOMにアクセス可能になります。このDOMはコンポーネント内のコードからは透過的に、つまりあたかも普通のDOMであるかのように上書きされてしまいます。つまり、グローバル定義されている`document`変数は書き換えられた状態でコンポーネントのコードがロードされるようになります。

このSecure DOMを介して利用可能なタグおよび属性値は制限されます。ホワイトリストに入っていないものについては、コンポーネント内のコードで設定されても無視されることになります。DOMに対するイベントリスナもWrapされたイベントオブジェクトを返すように付け替えられます。

DOMのみならず、Windowオブジェクトも同様に上書きされてしまいます。そもそも`window`がそのままアクセスできるなら、`window.document` でオリジナルの生DOMが辿れてしまいますので、こちらもSecure Windowとよばれるオブジェクトに置き換えられます。

そして、すべてのグローバル変数は、ホワイトリストにはいっているもの以外、すべて遮断されます。ホワイトリストに含まれるものについても、Wrapされたものに置き換えられ、直接アクセスできないようになります。

こうしてコンポーネントの中のコードは、それぞれの隔離された空間の中に定義された、ネイティブのDOM/BOMに対するプロクシとなるオブジェクトを介してのみアクセスが許された状態で、格納されます。

### LockerServiceの仕組みとセキュリティ

一体どのようにしてLockerServiceがそのような制御をしているのか、実際のものとは少し違いますが、LockerServiceが行っていることをざっくりとコードで解説します。

コンポーネントが持つすべてのJavaScriptコードは、アプリケーションにロードされる前に、LockerServiceによって事前に書き換えられます。ただし、構文解析してコード変換しているようなことをしているわけではなく、単に以下のようなコードを追加してwrapしているだけです（実際のものはもう少し複雑です）。

```js
var secwin = new SecureWindow();
var secdoc = new SecureDocument();
with({ window: secwin, document: secdoc, ... }) {
  (function() {
    "use strict";

// ...ここに元のJavaScriptコードが入る...

  })();
}
```

with句で囲まれているので、ブロック内のコードが指し示す`window`および`document`へのアクセスは、オリジナルのwindowやdocumentではなく、冒頭で生成された`secwin`および`secdoc`に置き換わります。

即時関数内がstrictモードに強制されているのは、オリジナルのwindowへの直接コードアクセスを防ぐためです。もしstrictモードでなかったとすると、以下のコードで簡単にグローバルオブジェクトの取得ができてしまいます。

```js
var global = (function() { return this; })();
// strictモードではglobalはundefinedだが、
// 非strictモードではグローバルオブジェクト(=オリジナルのwindow)を返す
```

ちなみにwith句はstrictモードでは使えないのですが、withの中の即時関数内で"use strict"しているので、こちらのコードは問題なく動くというわけです。大変ですね。

しかし、本当にこれでSandboxにできているのでしょうか？特に、グローバルのwindowオブジェクトに絶対アクセスできない保証が本当にあるのでしょうか？

先ほど、strictモードを使ってグローバルオブジェクトへの直接のアクセスを防いでいるということを解説しましたが、strictモードであってもたとえばFunctionコンストラクタ内ではthisはグローバルオブジェクトとして扱われるため、以下のコードで取得できてしまいそうです。

```js
var global = new Function("return this")();
```

もし事前にグローバル変数である`Function`へのアクセスを制限していたとしても、これはさすがに防げそうにありません。

```js
c='constructor',(1)[c][c]('return this')();
```

ここでLockerServiceは破綻しそうに見えますが、実はLockerServiceの動作環境にはさらなる制限が加えられています。それは、コンポーネントを表示するページにCSPが適用されているということです。CSPのポリシーで、`unsafe-eval`を設定されているので、上記のようなeval型のコードはすべてポリシー違反となります。まるで綱渡りのような形ですね。

ここで、素人レベルでぱっと思いつく攻撃方法はとりあえず対策されていることがわかりました。とりあえず感想としては、まさかCSPを強制するためにCSP対応が不十分なIE11をサポートから切り捨てるとは思い切ったな、と言った感じでしょうか。


## 課題

以上でざっくりとLockerServiceの正体について解説しました。このあたりは[ソースコードも公開されて](https://github.com/forcedotcom/aura/tree/master/aura-impl/src/main/resources/aura/locker)おり、まあそれほど情報としてはクローズドではないのですが、実際にはセキュリティの設計から仕様、実装まで単一ベンダー（＝Salesforce）が行っているものですし、特にコンパチビリティの実現については不安が多く残ります。

実際、DOMおよびWindowオブジェクトをアプリケーションから透過的にできるように目指した割には、制限や互換性の問題が多く、既存ライブラリがLockerService上でまともに動くのは期待できない状況が多く続いています。当初はAngularやReactもそのまま動作すると言っていましたが、つい最近、これらのライブラリを使う場合は[IFRAMEで表示する方法に結局落ち着くことになりました](http://releasenotes.docs.salesforce.com/en-us/spring17/release-notes/rn_lightning_container.htm)。さらに、全くお笑いではないですが、Salesforce自身が開発提供している標準コンポーネントの一部もLockerServiceの影響を受けてしまい、動作できなくなってしまっているという状況です。

正直現時点ではこの取り組みがどのような結末を迎えるかはわかりません。Salesforce本体はこの仕組みの実現にそれなりのリソースをかけてるので、まあなるようになるのかもしれないし、ただその事自体、もしこれがポシャってしまったら、と、さらなる不安を掻き立てる要素にもなってしまいます。

まあSalesforceがポシャるだけならその界隈が涙を流すだけでいいのかもしれませんが、中途半端なまま継続してしまってWebの分断を引き起こすようなことにならないことを祈ります。

最後に、LockerServiceのセキュリティについて、僕自身がLockerServiceのブログにコメントした文章をあげておきます。

https://developer.salesforce.com/blogs/developer-relations/2016/04/introducing-lockerservice-lightning-components.html

> It seems very fragile access control system relying on CSP behavior. There is no standard spec assuring that LockerService can always prevent to access to the global. Even if CSP becomes valid, Function prototype overriding or some other access hook can steal references which LS wanted to hide. Additionally, as it has no standard spec background, there still remains a risk that browser vendors will introduce a bypass feature in future even if you could close all the possible path today.