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

現在のウェブでリッチなエクスペリエンスを構築するには、ほとんどの場合、制御できないコンポーネントやコンテンツを埋め込む必要があります。サードパーティ製ウィジェットはエンゲージメントを促進し、全体的なユーザー エクスペリエンスにおいて重要な役割を果たします。ユーザー作成コンテンツは、サイトのネイティブ コンテンツよりも重要になることもあります。どちらも使用しないという選択肢はありませんが、どちらもサイトに Something Bad™ が発生するリスクを高めます。埋め込む各ウィジェット(すべての広告、ソーシャル メディア ウィジェット)は、悪意を持った者にとって次のような攻撃ベクトルになる可能性があります。

コンテンツ セキュリティ ポリシー(CSP)を使用すると、スクリプトなどのコンテンツの信頼できるソースをホワイトリストに登録できるため、これらの両方の種類のコンテンツに関連するリスクを軽減できます。これは正しい方向への大きな一歩ですが、ほとんどの CSP ディレクティブが提供する保護はバイナリ(リソースが許可されるか、許可されないか)であることに注意が必要です。「このコンテンツ ソースを実際に信頼するのかはわからないが、とってもきれいだ」と言うと効果的な場合もあります。ブラウザさん、埋め込んでください。でも、サイトを壊さないでください。」

最小権限

本質的には、埋め込むコンテンツに、その機能の実行に必要な最小限のレベルの機能のみを付与できるメカニズムを探しています。ウィジェットで新しいウィンドウをポップアップする必要がない場合は、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 の場合、フレーム内のドキュメントは完全にサンドボックス化され、次の制限が適用されます。

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

これは非常に厳格で、完全にサンドボックス化された iframe に読み込まれたドキュメントは、ほとんどリスクがありません。もちろん、あまり意味がないこともあります。一部の静的コンテンツでは完全なサンドボックスで済むかもしれませんが、ほとんどの場合、少し緩める必要があります。

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

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

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

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

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

sandbox 属性が設定されている場合、ウィジェットは必要な権限のみを取得し、プラグイン、トップ ナビゲーション、ポインタ ロックなどの機能はブロックされたままになります。ウィジェットを埋め込むリスクを軽減し、悪影響は発生していません。 これは、すべての関係者にとってメリットがあります。

特権の分離

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

逆に、コードにプラグインが不要な場合は、なぜプラグインにアクセスできるようにするのでしょうか?最善の場合、これは使用しない権限ですが、最悪の場合、攻撃者が侵入する可能性のあるベクトルになります。どのコードにもバグがあり、実質的にすべてのアプリケーションは、何らかの形で悪用に対して脆弱です。独自のコードをサンドボックス化すると、攻撃者がアプリを不正使用しても、アプリのオリジンへの完全なアクセス権は付与されず、アプリが実行できる操作のみを実行できるようになります。それでも悪いことですが、最悪の状況に比べればましです。

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

eval() を安全にサンドボックス化

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

Evalbox は、文字列を受け取って JavaScript として評価するエキサイティングなアプリケーションです。そうですか。長い間お待ちいただいておりましたもちろん、これはかなり危険なアプリケーションです。任意の JavaScript の実行を許可すると、オリジンが提供するすべてのデータが取得されることになります。コードがサンドボックス内で実行されるようにすることで、Bad Things™ が発生するリスクを軽減し、安全性を大幅に高めます。フレームの内容から順にコードを見ていきましょう。

<!-- 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 を取得して、スクリプトの実行のみを許可します。

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

次に、実行できるように接続します。まず、iframe からのレスポンスをリッスンし、ユーザーに alert() します。実際のアプリケーションでは、より煩わしくない処理が行われます。

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 フラグの両方を含むサンドボックスを使用して、同じオリジンの別のページをフレーム化している場合、フレーム化されたページが親に到達して、サンドボックス属性を完全に削除する可能性があります。

サンドボックスでプレイする

サンドボックス化は、執筆時点で Firefox 17 以降、IE10 以降、Chrome など、さまざまなブラウザで利用可能です(もちろん、caniuse の最新のサポート表もあります)。iframessandbox 属性を適用すると、表示するコンテンツに対する特定の権限(コンテンツが正しく機能するために必要な権限のみ)を付与できるようになります。これにより、コンテンツ セキュリティ ポリシーですでに可能なことを超えて、サードパーティ コンテンツの組み込みに関連するリスクを軽減できます。

さらに、サンドボックス化は、巧妙な攻撃者が独自のコード内の穴を悪用するリスクを軽減するための強力な手法です。モノリシック アプリケーションをサンドボックス化された一連のサービスに分割し、それぞれが自己完結型の機能の小さなチャンクを担当することで、攻撃者は特定のフレームのコンテンツだけでなく、そのコントローラも侵害しなければなりません。これは、コントローラのスコープを大幅に縮小できるため、はるかに困難なタスクです。残りの部分についてはブラウザに依頼し、セキュリティ関連の作業はそのコードの監査に集中できます。

ただし、サンドボックス化がインターネット上のセキュリティの問題を完全に解決するわけではありません。多層防御を提供するため、ユーザーのクライアントを制御しない限り、すべてのユーザーのブラウザ サポートを利用することはまだできません(エンタープライズ環境などのユーザー クライアントを管理している場合、どうでしょうか)。いつか…ですが、現時点ではサンドボックスは防御を強化するためのもう 1 つの保護レイヤであり、単独で頼れる完全な防御ではありません。それでもレイヤは優れています。これを活用することをおすすめします

関連情報

  • HTML5 アプリケーションでの権限分離」は、小さなフレームワークの設計と、そのフレームワークを 3 つの既存の HTML5 アプリに適用する方法について説明している興味深い論文です。

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

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