サンドボックス化された iframe で安全にプレイ

昨今のウェブでリッチ エクスペリエンスを構築するには、実際には制御できないコンポーネントやコンテンツを埋め込むことがほぼ避けられません。サードパーティのウィジェットはエンゲージメントを促進し、全体的なユーザー エクスペリエンスにおいて重要な役割を果たしますが、場合によっては、サイトのネイティブ コンテンツよりもユーザー作成コンテンツの方が重要な役割を果たします。どちらかを排除することは現実の選択肢ではありませんが、どちらもサイトで Nothing BadTM が発生するリスクを高めます。埋め込んだ各ウィジェット(すべての広告、すべてのソーシャル メディア ウィジェット)は、悪意を持ったユーザーの潜在的な攻撃ベクトルになります。

コンテンツ セキュリティ ポリシー(CSP)は、特に信頼できるスクリプトやその他のコンテンツのソースを許可リストに登録できるようにすることで、これらのタイプのコンテンツに関連するリスクを軽減できます。これは正しい方向への大きな一歩ですが、ほとんどの CSP ディレクティブが提供する保護はバイナリであり、リソースを許可するか許可しないことに注意してください。「このコンテンツのソースを実際に信頼しているかどうかわかりませんが、とても良い情報です。埋め込んでください」

最小権限

つまり、Google が求めているのは、その業務に必要な最小限のレベルの能力のみをコンテンツに埋め込むことができるメカニズムです。ウィジェットで新しいウィンドウをポップアップする必要がない場合は、window.open へのアクセス権を削除しても問題はありません。Flash が不要なのであれば、プラグインのサポートを無効にしても問題はありません。最小権限の原則に従い、使用する機能に直接関係のない機能をすべてブロックすることで、可能な限り安全性が高まります。その結果、使用すべきでない特権が、埋め込みコンテンツによって利用されないことをやみくもに信じる必要がなくなりました。そもそも、その機能にはアクセスできません。

iframe 要素は、そのようなソリューションに適したフレームワークを構築するための最初のステップです。信頼できないコンポーネントを iframe で読み込むことで、アプリと読み込むコンテンツを分離できます。フレーム内のコンテンツは、ページの DOM やローカルに保存したデータにはアクセスできず、ページ上の任意の位置に描画することもできません。スコープはフレームのアウトラインに制限されます。ただし、この分離は真に堅牢ではありません。含まれるページには、煩わしい動作や悪意のある動作のためのオプションがいくつかあります。動画、プラグイン、ポップアップの自動再生は氷山の一角です。

iframe 要素の sandbox 属性は、フレーム内のコンテンツの制限を強化するために必要な情報のみを提供します。特定のフレームのコンテンツを低権限環境で読み込むようにブラウザに指示することで、必要な処理を行うために必要な機能のサブセットのみを許可できます。

確認はするが、

Twitter の「ツイート」ボタンは、サンドボックスを使用してサイトに安全に埋め込める機能の好例です。Twitter では、次のコードを使って iframe 経由でボタンを埋め込むことができます。

<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
        style="border: 0; width:130px; height:20px;"></iframe>

何をロックダウンできるかを理解するため、ボタンに必要な機能を慎重に検討しましょう。フレームに読み込まれた HTML は、Twitter のサーバーから JavaScript の一部を実行し、クリックするとツイート インターフェースが表示されるポップアップを生成します。このインターフェースは、ツイートを正しいアカウントに結び付けるために Twitter の Cookie にアクセスし、ツイート フォームを送信する機能を必要とします。フレームはプラグインを読み込む必要はなく、トップレベル ウィンドウをナビゲートする必要もなく、その他のさまざまな機能を使用する必要もありません。これらの権限は必要ないので、フレームのコンテンツをサンドボックス化して権限を削除します。

サンドボックス化はホワイトリストに基づいて行われます。まず、使用可能なすべての権限を削除してから、サンドボックスの構成に特定のフラグを追加して個々の機能を有効にします。Twitter ウィジェットでは、JavaScript、ポップアップ、フォーム送信、twitter.com の Cookie を有効にすることにしました。そのためには、次の値を指定して sandbox 属性を iframe に追加します。

<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
    src="https://platform.twitter.com/widgets/tweet_button.html"
    style="border: 0; width:130px; height:20px;"></iframe>

これで完了です。フレームに必要なすべての機能を提供すると、ブラウザは sandbox 属性の値を介して明示的に付与しなかった権限へのアクセスを拒否します。

機能をきめ細かく管理

上記の例では、考えられるサンドボックス フラグをいくつか確認しました。次に、属性の内部動作についてもう少し詳しく見てみましょう。

iframe で sandbox 属性が空の場合、フレームで囲まれているドキュメントは完全にサンドボックス化され、次の制限が適用されます。

  • フレーム内のドキュメントでは JavaScript は実行されません。これには、script タグを介して明示的に読み込まれる JavaScript だけでなく、インライン イベント ハンドラと JavaScript: URL も含まれます。つまり、noscript タグに含まれるコンテンツは、ユーザーが自分でスクリプトを無効にした場合と同じように表示されます。
  • フレーム内のドキュメントは一意のオリジンに読み込まれます。つまり、すべての同一オリジンのチェックは失敗します。一意のオリジンは、他のオリジンとは一致せず、それ自体とも一致しません。特に影響としては、ドキュメントが送信元の Cookie や他のストレージ メカニズム(DOM ストレージ、Indexed DB など)に保存されているデータにアクセスできないことです。
  • フレーム内のドキュメントは新しいウィンドウやダイアログを作成できません(window.opentarget="_blank" などを使用)。
  • フォームは送信できません。
  • プラグインは読み込まれません。
  • フレーム内のドキュメントは自身内のみを移動でき、最上位の親に移動することはできません。window.top.location を設定すると例外がスローされ、target="_top" を含むリンクをクリックしても効果はありません。
  • 自動的にトリガーされる機能(オートフォーカスされたフォーム要素、動画の自動再生など)はブロックされます。
  • ポインタのロックを取得できません。
  • フレーム内のドキュメントに含まれる iframes では、seamless 属性は無視されます。

これはきわめて複雑で、完全にサンドボックス化された iframe にドキュメントを読み込む場合、実際のリスクはほとんどありません。もちろん、それでも大きな価値はありません。一部の静的コンテンツについては完全なサンドボックスで対処できるかもしれませんが、ほとんどの場合、少しの緩やかな緩和が必要になります。

プラグインを除き、これらの各制限は、sandbox 属性の値にフラグを追加することで解除できます。サンドボックス化されたドキュメントでは、サンドボックス化されていないネイティブ コードであるため、プラグインを実行することはできませんが、それ以外はすべて妥当です。

  • allow-forms はフォームの送信を許可します。
  • allow-popups は(衝撃的な)ポップアップを許可します。
  • allow-pointer-lock を使用すると、ポインタのロックが可能になります。
  • allow-same-origin を使用すると、ドキュメントの生成元を維持できます。https://example.com/ から読み込まれたページは、その生成元のデータにアクセスすることになります。
  • allow-scripts を使用すると、JavaScript を実行できます。また、機能を自動的にトリガーすることもできます(JavaScript で実装するのは簡単であるため)。
  • allow-top-navigation を使用すると、最上位ウィンドウを移動して、ドキュメントをフレームの外に拡張できます。

これらを念頭に置いて、上記の Twitter の例でサンドボックス フラグの特定セットにした理由を正確に評価できます。

  • allow-scripts は、フレームに読み込まれたページでユーザー操作を扱う JavaScript が実行されるため必要です。
  • ボタンが新しいウィンドウにツイート フォームをポップアップするため、allow-popups が必要です。
  • ツイート フォームは送信可能であるため、allow-forms が必要です。
  • twitter.com の Cookie にアクセスできず、ユーザーがログインしてフォームを投稿できないため、allow-same-origin が必要です。

フレームに適用されるサンドボックス フラグは、サンドボックスで作成されたウィンドウまたはフレームにも適用されることに注意する必要があります。つまり、フォームはフレームがポップアップ表示されるウィンドウにのみ存在しますが、フレームのサンドボックスに allow-forms を追加する必要があります。

sandbox 属性を使用すると、ウィジェットは必要な権限のみを取得し、プラグイン、トップ ナビゲーション、ポインタロックなどの機能はブロックされます。ウィジェットが埋め込まれるリスクは低下し、悪影響はありません。 関係者全員にとってのメリットです。

権限の分離

サードパーティのコンテンツをサンドボックス化して、信頼できないコードを権限の低い環境で実行することは、明らかに有益です。では、独自のコードはどうでしょうか。自分を信じてるよね?では、サンドボックスについて心配する理由は何でしょうか。

そこで、私は、コードにプラグインが必要ない場合は、なぜプラグインにアクセスできるようにするのか、という質問を変えてみましょう。最悪でも攻撃者に足を踏み入れる可能性のある手段にもなります。すべてのコードにはバグがあり、実質的にどのアプリケーションも、なんらかの形で悪用に対して脆弱です。独自のコードをサンドボックス化すると、たとえ攻撃者がアプリの無効化に成功しても、アプリのオリジンへのフルアクセスは許可されず、アプリで可能なことしか実行できなくなります。それでも悪質ですが、それほど悪質ではありません。

このリスクをさらに軽減するには、アプリケーションを論理的な断片に分割し、各部分を最小限の権限でサンドボックス化します。この手法はネイティブ コードでは非常に一般的です。たとえば、Chrome は、ローカル ハードドライブにアクセスしてネットワーク接続できる、権限の高いブラウザ プロセスと、信頼できないコンテンツを解析する手間のかかる多くの権限の低いレンダラ プロセスに分割します。レンダラはディスクにアクセスする必要はなく、ページのレンダリングに必要な情報はすべてブラウザが提供します。巧妙なハッカーがレンダラを破壊する方法を見つけたとしても、そこまで至ったわけではありません。レンダラ自体が重要なことを実行することができないからです。権限の高いアクセスはすべて、ブラウザのプロセスを介してルーティングする必要があります。攻撃者が損害を与えるには、システムのさまざまな部分にいくつかの穴を見つける必要があるため、pwnage が成功するリスクが大幅に軽減されます。

eval() を安全にサンドボックス化しています

サンドボックス化と postMessage API を使用すると、このモデルをウェブに簡単に適用できます。アプリケーションの一部をサンドボックス化された iframe に配置し、親ドキュメントがメッセージを投稿してレスポンスをリッスンすることで、それらの間の通信を仲介できます。このような構造により、アプリのどの部分についても、エクスプロイトによる被害を最小限に抑えることができます。また、明確な統合ポイントの作成を強制されるため、入出力の検証で注意が必要な場所を正確に把握できるという利点もあります。簡単な例を使って その仕組みを見てみましょう

Evalbox は、文字列を受け取って JavaScript として評価する便利なアプリケーションです。なるほど。長い年月が待たされたことを 嬉しく思います任意の JavaScript を実行できると、オリジンが提供するあらゆるデータが取得されるようになるため、これは非常に危険なアプリケーションです。コードをサンドボックス内で実行することにより、Bad ThingsTM のリスクを軽減し、サンドボックスの安全性を大幅に高めます。フレームのコンテンツから、コードを内側から順に説明します。

<!-- frame.html -->
<!DOCTYPE html>
<html>
    <head>
    <title>Evalbox's Frame</title>
    <script>
        window.addEventListener('message', function (e) {
        var mainWindow = e.source;
        var result = '';
        try {
            result = eval(e.data);
        } catch (e) {
            result = 'eval() threw an exception.';
        }
        mainWindow.postMessage(result, event.origin);
        });
    </script>
    </head>
</html>

フレーム内には、window オブジェクトの message イベントにフックすることで親からのメッセージをリッスンする最小限のドキュメントがあります。親が iframe のコンテンツで postMessage を実行するたびに、このイベントがトリガーされ、親が実行したい文字列にアクセスできるようになります。

ハンドラでは、イベントの source 属性(親ウィンドウ)を取得します。作業が完了したら、これを使用して結果を送り返します。次に、与えられたデータを eval() に渡すことで、面倒な作業を行います。この呼び出しは try ブロックでラップされています。サンドボックス化された iframe 内で禁止されたオペレーションによって DOM 例外が生成されることが多く、そのような例外をキャッチして、わかりやすいエラー メッセージを報告します。最後に 結果を親ウィンドウにポストしますこれは非常にシンプルなものです。

親も同様に複雑ではありません。コードの textarea と実行用の button を持つ小さな UI を作成し、サンドボックス化された iframe を介して frame.html を pull し、スクリプトの実行のみを許可します。

<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
        id='sandboxed'
        src='frame.html'></iframe>

それでは、実行に向けて接続します。まず、iframealert() からユーザーに対するレスポンスをリッスンします。実際のアプリケーションであれば、それほど面倒な処理は行われないでしょう。

window.addEventListener('message',
    function (e) {
        // Sandboxed iframes which lack the 'allow-same-origin'
        // header have "null" rather than a valid origin. This means you still
        // have to be careful about accepting data via the messaging API you
        // create. Check that source, and validate those inputs!
        var frame = document.getElementById('sandboxed');
        if (e.origin === "null" &amp;&amp; e.source === frame.contentWindow)
        alert('Result: ' + e.data);
    });

次に、button をクリックするためのイベント ハンドラを接続します。ユーザーがクリックすると、textarea の現在の内容を取得し、実行用のフレームに渡します。

function evaluate() {
    var frame = document.getElementById('sandboxed');
    var code = document.getElementById('code').value;
    // Note that we're sending the message to "*", rather than some specific
    // origin. Sandboxed iframes which lack the 'allow-same-origin' header
    // don't have an origin which you can target: you'll have to send to any
    // origin, which might alow some esoteric attacks. Validate your output!
    frame.contentWindow.postMessage(code, '*');
}

document.getElementById('safe').addEventListener('click', evaluate);

単に、非常にシンプルな評価 API が用意されており、評価されたコードが Cookie や DOM ストレージなどの機密情報にアクセスしないようにしています。同様に、評価されたコードでは、プラグインの読み込みや、新しいウィンドウのポップアップなど、煩わしいアクティビティや悪意のあるアクティビティを読み込めません。

モノリシック アプリケーションを単一目的のコンポーネントに分割することで、独自のコードでも同じことができます。前述したように それぞれをシンプルなメッセージング API でラップできます。高い権限を持つ親ウィンドウは、コントローラおよびディスパッチャとして機能し、ジョブの実行に必要な最小限の権限しか持たない特定のモジュールにメッセージを送信し、結果をリッスンして、各モジュールに必要な情報のみが十分に供給されるようにします。

ただし、親と同じオリジンからフレーム化されたコンテンツを処理する場合は注意が必要です。https://example.com/ 上のページが、同じオリジンの別のページを、allow-same-origin フラグと allow-scripts フラグの両方を含むサンドボックスでフレーム処理する場合、そのフレーム内のページが親に到達し、sandbox 属性を完全に削除できます。

サンドボックスでプレイ

サンドボックス化は現在、Firefox 17 以降、IE10 以降、本稿執筆時点で Chrome などのさまざまなブラウザで利用可能です(Caniuse にも最新のサポート表があります)。指定する iframessandbox 属性を適用すると、表示するコンテンツに対して、コンテンツが正しく機能するために必要な権限のみを付与できます。これにより、第三者のコンテンツの取り込みに関連するリスクを、コンテンツ セキュリティ ポリシーですでに可能な以上のリスクで低減できます。

さらに、サンドボックスは、巧妙な攻撃者がコードの穴を悪用できるリスクを軽減する強力な手法です。モノリシック アプリを、それぞれが自己完結型の機能の小さな部分を担うサンドボックス サービスに分離することで、攻撃者は特定のフレームのコンテンツだけでなく、コントローラも侵害せざるを得なくなります。特にコントローラの範囲を大幅に縮小できるため、これはかなり難しいタスクです。ブラウザのサポートを依頼すれば、そのコードの監査にセキュリティ関連の労力を費やすことができます。

サンドボックス化がインターネット上のセキュリティ問題に対する完全なソリューションというわけではありません。多層防御が提供され、ユーザーのクライアントを管理しない限り、すべてのユーザーのブラウザ サポートに依存することはできません(企業環境など、ユーザー クライアントを管理している場合)。いつの日か、サンドボックス化は防御を強化するもう 1 つの保護層になるかもしれませんが、サンドボックス化は単独では信頼できる完全な防御ではありません。それでも、レイヤは非常に優れています。これを活用することをおすすめします

関連情報

  • Privilege Separation in HTML5 Applications」は、小さなフレームワークの設計と、3 つの既存の HTML5 アプリへの適用に関する興味深い論文です。

  • 他の 2 つの新しい iframe 属性(srcdocseamless)と組み合わせると、サンドボックス化がさらに柔軟になります。前者では HTTP リクエストのオーバーヘッドなしでフレームにコンテンツを入力できます。後者ではフレーム内のコンテンツにスタイルを流すことができます。現時点ではどちらのブラウザもサポートが非常に少ない(Chrome と WebKit ナイトリー)が、将来的には興味深い組み合わせとなるでしょう。たとえば、次のコードを使用して、記事に対するコメントをサンドボックス化できます。

        <iframe sandbox seamless
                srcdoc="<p>This is a user's comment!
                           It can't execute script!
                           Hooray for safety!</p>"></iframe>