Navigations
October 1, 2020
クロスサイトのページで画面遷移がトリガーされたか(または、そうでないか)を検出することは攻撃者にとって有用です。例えば、ウェブサイトはユーザの状態に依存(#case-scenarios)して、あるエンドポイントで画面遷移をトリガーする可能性があります。
どのような画面遷移が発生したかを検出することで攻撃者以下の様なことが可能になります。
iframeを使用してonloadイベントがトリガーされた回数を数える。- ウィンドウへの参照を通じてアクセス可能な
history.lengthの値をチェックする。この値は被害者のhistory.pushStateや通常の画面遷移によって変更された履歴の数を提供しています。攻撃者はhistory.lengthの値を取得するためにウィンドウへの参照のlocationをターゲットのウェブサイトに変更し、そしてSame-Originに戻すことによって最後に値を読み取ります。
ダウンロードトリガー #
エンドポイントがContent-Disposition: attachment ヘッダを設定すると、ブラウザにレスポンスをナビゲートさせるのではなくファイルとしてダウンロードすることを指示します。この挙動が発生したかを検出すると、結果が被害者の状態に依存する場合に攻撃者に機密情報をリークできる可能性があります。
ダウンロードバー #
Chromeベースのブラウザではファイルをダウンロードする際に、ブラウザのウィンドウ下部にダウンロードの進捗を示すバーがウィンドウと一体化して表示されます。攻撃者ウィンドウの高さを監視することでダウンロードバーが開いているかどうかを検出することができます。
// ウィンドウの現在の高さを読み取る
var screenHeight = window.innerHeight;
// ダウンロードのトリガーとなるページを読み込む
window.open('https://example.org');
// タブの読み込みを待つ
setTimeout(() => {
// ダウンロードバーが表示された場合、すべてのタブの高さが小さくなります
if (window.innerHeight < screenHeight) {
console.log('Download bar detected');
} else {
console.log('Download bar not detected');
}
}, 2000);
important
この攻撃は、自動ダウンロード機能が有効になっているChromeベースのブラウザでのみ有効です。加えてこの攻撃はユーザがダウンロードばーばを能動的に閉じないと再検出できないため、繰り返し行うことはできません。
iframeを利用したダウンロード遷移 #
Content-Disposition: attachment ヘッダをテストするもう一つの方法は遷移が発生したかどうかをチェックすることです。仮にページの読み込みによってダウンロードが発生した場合、遷移は発生せずウィンドウは同一オリジン内に留まります。
以下のコードは、そのような遷移が発生したかを検出しダウンロードが試行されたかを検出します。
// ダウンロード試行時のテスト先URLを設定する
var url = 'https://example.org/';
// onloadイベントを計測するための外側のiframeを作成する
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
// ダウンロードの試行をテストするために、内側のiframeを作成
iframe.srcdoc = `<iframe src="${url}" ></iframe>`;
iframe.onload = () => {
try {
// ナビゲーションが発生した場合、iframeはクロスオリジンになるため、"inner.origin "にアクセスすると例外が発生します。
iframe.contentWindow.frames[0].origin;
console.log('Download attempt detected');
} catch(e) {
console.log('No download attempt detected');
}
}
info
ダウンロードを試行することによりiframe内で画面遷移が発生しない場合、iframeはonloadイベントを直接トリガーしません。そのため、上記の例では代わりに外側にiframeを使用し、その中のiframeを含むサブリソースの読み込みが完了した際にトリガーされるonloadイベントを待ち受けます。
important
この攻撃はどのような[Framing Protections](https://webapppentestguidelines.github.io/xs-leaks/docs/defenses/opt-in/xfo/にかかわらず動作します。Content-Disposition: attachmentが指定されるとX-Frame-OptionsとContent-Security-Policyヘッダは無視されるからです。
ダウンロード遷移(iframeなし) #
前項で紹介した手法はwindowオブジェクトを利用しても効果的にテストすることができます。
// 送信先URLをセット
var url = 'https://example.org';
// windowへの参照を取得
var win = window.open(url);
// windowが読み込まれるまで待機
setTimeout(() => {
try {
// ナビゲーションが発生した場合、iframeはクロスオリジンになるため、"win.origin" にアクセスすると例外が発生する
win.origin;
parent.console.log('Download attempt detected');
} catch(e) {
parent.console.log('No download attempt detected');
}
}, 2000);
サーバサイドリダイレクト #
拡張 #
サーバサイドリダイレクトは、宛先URLのサイズと攻撃者によって制御されている入力値(クエリ文字列またはパスのいずれか)が増加する場合、クロスオリジンのページから検出することができます。以下の手法は大きなクエリ文字列やパスを生成する子男でほとんどのWEBサーバでエラーを誘発することが可能であるという事実に依存しています。リダイレクトはURLのサイズを増加させるため、サーバが処理可能なURLの最大長より正確に1文字減らすることで検出することができます。これによりサイズが大きくなった場合、サーバはクロスオリジンのページから検出可能なエラーを応答します。(例えばエラーイベント経由)
example
攻撃の例はこちらで確認することができます。 here.
クロスオリジンリダイレクト #
CSP 侵害 #
Content-Security-Policy (CSP) はクロスサイトスクリプティングやデータインジェクションに対する綿密な防御機構です。CSPが侵害されるとSecurityPolicyViolationEvent がトリガーされます。攻撃者はconnect-src ディレクティブ を利用してCSPを設定することができます。このディレクティブはfetch がCSPディレクティブで設定されていないURLを追うたびにViolationイベントがトリガーされます。これによって攻撃者は他のオリジンへのリダイレクトが発生したかを検出することができます。
以下の例では、fetch APIで設定(6行目)したウェブサイトがhttps://example.com以外のウェブサイトにリダイレクトされるとSecurityPolicyViolationEvent がトリガーされます。
| |
攻撃対象のリダイレクトがクロスサイトであることと、SameSite=Laxと設定されたクッキーの存在が条件である場合、fetchはトップレベルの遷移としてカウントされないため上記の手法は動作しません。このような場合、攻撃者は別のCSPディレクティブであるform-actionと、GETを使ってHTMLフォームを送信するとトップレベル遷移としてカウントされるという事実を利用することができます。
以下の例では、フォームのアクション(3行目)がhttps://example.org以外にリダイレクトされるとSecurityPolicyViolationEventがトリガーされます。
| |
FireFoxではChromeベースのブラウザとは異なりform-actionがform送信後にリダイレクトをブロックしないため、この手法は実行できないことに注意してください。
攻撃シナリオ #
とあるオンラインバンクでは富裕層ユーザが口座の残高を確認した際に、ウェブサイト上の予約ページへナビゲーションをトリガーすることにより魅力的な株の投資機会へリダイレクトすることになっています。このように特定のユーザグループに対してのみ行われている場合、攻撃者に顧客ステータスがリークされます。
分割されたHTTPキャッシュの回避 #
もし、サイトexample.com が *.example.com/resource からのリソースを含む場合、sのリソースはトップレベルナビゲーションを通して直接リクエストされた場合と同じキャッシュキーを持つことになります。これは、キャッシュキーがトップレベルの eTLD+1 と フレームの eTLD+1 で構成されるからです。1
リソースがキャッシュされているかどうかは window.stop() が実行される前にオリジンが変更されたかどうかをチェックすることによって検出することができるのは、ウィンドウはwindow.stop() で異なるオリジンへのナビゲーションを防ぐことができ、デバイス上のキャッシュはネットワークより高速であるためです。
async function ifCached_window(url) {
return new Promise(resolve => {
checker.location = url;
// キャッシュのみ
setTimeout(() => {
checker.stop();
}, 20);
// 結果を取得
setTimeout(() => {
try {
let origin = checker.origin;
// タイムアウト前にOriginが変更されていない
resolve(false);
} catch {
// Originが変更された
resolve(true);
checker.location = "about:blank";
}
}, 50);
});
}
windowを作成(チェック成功後に戻ることが可能になる)
let checker = window.open("about:blank");
使い方
await ifCached_window("https://example.org");
info
分割されたHTTPキャッシュを回避するにはヘッダVary.Sec-Fetch-Siteを使って防ぐことができます。Sec-Fetch-Siteはキャッシュを開始者によって分割するため、Cache Protections を参照してください。この攻撃は同じサイトのリソースにのみ適用されるため、Sec-Fetch-Siteヘッダーはウェブサイトのsame-siteやsame-originに対して、攻撃者にとってはcross-siteになるため、うまくいきます。
対策 #
| Attack Alternative | SameSite Cookies (Lax) | COOP | Framing Protections | Isolation Policies |
|---|---|---|---|---|
| history.length (iframes) | ✔️ | ❌ | ✔️ | FIP |
| history.length (windows) | ❌ | ✔️ | ❌ | NIP |
| onload event inside an iframe | ✔️ | ❌ | ✔️ | FIP |
| Download bar | ✔️ | ❌ | ❌ \(^{1}\) | NIP |
| Download Navigation (iframes) | ✔️ | ❌ | ❌ \(^{1}\) | FIP |
| Download Navigation (windows) | ❌ | ❌ \(^{1}\) | ❌ | NIP |
| Inflation | ✔️ | ❌ | ❌ | RIP |
| CSP Violations | ❌ \(^{2}\) | ❌ | ❌ | RIP 🔗 NIP |
🔗 – Defense mechanisms must be combined to be effective against different scenarios.
- COOP と Framing Protections のどちらも、
Content-Dispositionヘッダが存在すると他のヘッダが無視されてしまい、リダイレクトリークを緩和することはできません。 - LaxモードのSameSite Cookieは、ウェブサイトのiframingから保護することができますが、Strictモードとは対照的に、ウィンドウへの参照やサーバー側のリダイレクトを含むリークには役立ちません。
Real-World Examples #
Twitterに報告された脆弱性は、この手法を利用してXS-Searchによる非公開ツイートの内容を漏洩させるというものでした。この攻撃は、ユーザーのクエリ2に対する結果がある場合にのみ、ページがナビゲーションを開始するため、可能でした。