サンドボックス化された 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 が必要です。
  • allow-same-origin は必須です。この Cookie がないと、twitter.com の Cookie にアクセスできず、ユーザーはログインしてフォームを送信できません。

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

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

特権の分離

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

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

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

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

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

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>