今日のウェブでリッチなエクスペリエンスを構築するには、実質的に制御できないコンポーネントやコンテンツを埋め込むことがほぼ不可避です。サードパーティのウィジェットは、エンゲージメントを促進し、ユーザー エクスペリエンス全体で重要な役割を果たすことができます。また、ユーザー作成コンテンツは、サイトのネイティブ コンテンツよりも重要になることもあります。どちらかを控えることは現実的な選択肢ではありませんが、どちらもサイトで問題が発生するリスクを高めます。埋め込むウィジェット(すべての広告、すべてのソーシャル メディア ウィジェット)は、悪意のあるユーザーにとって攻撃ベクトルとなる可能性があります。
コンテンツ セキュリティ ポリシー(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 属性の値で明示的に付与されていない権限へのアクセスはブラウザによって拒否されます。
機能のきめ細かい制御
上記の例では、サンドボックス化フラグの例をいくつか確認しました。ここでは、属性の内部動作をもう少し詳しく見ていきましょう。
空の sandbox 属性を持つ iframe がある場合、フレーム化されたドキュメントは完全にサンドボックス化され、次の制限が適用されます。
- JavaScript はフレーム化されたドキュメントでは実行されません。これには、スクリプトタグを介して明示的に読み込まれた JavaScript だけでなく、インライン イベント ハンドラや javascript: URL も含まれます。また、ユーザーがスクリプトを無効にした場合と同様に、noscript タグに含まれるコンテンツが表示されます。
- フレーム化されたドキュメントは一意のオリジンに読み込まれます。つまり、同一オリジン チェックはすべて失敗します。一意のオリジンは、他のオリジンと一致しません。特に、ドキュメントは、オリジンの Cookie やその他のストレージ メカニズム(DOM ストレージ、Indexed DB など)に保存されているデータにアクセスできません。
- フレーム化されたドキュメントは、新しいウィンドウやダイアログを作成できません(
window.openやtarget="_blank"など)。 - フォームを送信できません。
- プラグインが読み込まれない。
- フレーム化されたドキュメントは、それ自体のみをナビゲートでき、最上位の親はナビゲートできません。
window.top.locationを設定すると例外がスローされ、target="_top"を含むリンクをクリックしても効果はありません。 - 自動的にトリガーされる機能(自動フォーカスされるフォーム要素、自動再生される動画など)はブロックされます。
- ポインタロックを取得できません。
seamless属性は、フレーム化されたドキュメントに含まれるiframesでは無視されます。
これは非常に厳格な措置であり、完全にサンドボックス化された 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は、ツイート フォームを送信可能にするために必要です。allow-same-originが必要です。これがないと、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() に渡して、面倒な処理を行います。サンドボックス化された iframe 内で禁止されているオペレーションは DOM 例外を頻繁に生成するため、この呼び出しは try ブロックでラップされています。これらの例外をキャッチして、わかりやすいエラー メッセージを報告します。最後に、結果を親ウィンドウにポストバックします。これはとても簡単なことです。
親も同様に複雑ではありません。コード用の 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" && 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 には、最新のサポート表が掲載されています)。sandbox 属性を iframes に適用すると、表示されるコンテンツに特定の権限を付与できます。これは、コンテンツが正しく機能するために必要な権限のみです。これにより、コンテンツ セキュリティ ポリシーですでに可能な範囲を超えて、第三者のコンテンツの挿入に関連するリスクを軽減できます。
さらに、サンドボックス化は、巧妙な攻撃者が独自のコードの脆弱性を悪用するリスクを軽減するための強力な手法です。モノリシック アプリケーションをサンドボックス化された一連のサービスに分割し、それぞれが自己完結型の機能の小さなチャンクを担当するようにすることで、攻撃者は特定のフレームのコンテンツだけでなく、そのコントローラも侵害せざるを得なくなります。これははるかに困難なタスクです。特に、コントローラのスコープを大幅に縮小できるためです。残りの部分についてはブラウザに支援を求めることで、セキュリティ関連の作業をそのコードの監査に費やすことができます。
サンドボックスがインターネット上のセキュリティ問題に対する完全なソリューションであるとは言えません。多層防御を提供します。ユーザーのクライアントを制御できない限り、すべてのユーザーに対してブラウザのサポートに依存することはできません(ユーザーのクライアントを制御できる場合、たとえば、エンタープライズ環境では、素晴らしいことです)。いつかはそうなるかもしれませんが、現時点では、サンドボックス化は防御を強化するためのもう 1 つの保護レイヤであり、それだけに頼ることができる完全な防御ではありません。それでも、レイヤは優れています。この機能を使用することをおすすめします。
関連情報
「Privilege Separation in HTML5 Applications」は、小規模なフレームワークの設計と、3 つの既存の HTML5 アプリへの適用について説明している興味深い論文です。
サンドボックス化は、他の 2 つの新しい iframe 属性(
srcdocとseamless)と組み合わせることで、さらに柔軟になります。前者は、HTTP リクエストのオーバーヘッドなしでフレームにコンテンツを入力できます。後者は、フレーム化されたコンテンツにスタイルを適用できます。現時点では、どちらもブラウザのサポートがかなり不十分ですが(Chrome と WebKit のナイトリー)、今後は興味深い組み合わせになるでしょう。たとえば、次のコードを使用して、記事のコメントをサンドボックス化できます。<iframe sandbox seamless srcdoc="<p>This is a user's comment! It can't execute script! Hooray for safety!</p>"></iframe>