Network Timing
October 1, 2020
ネットワーク タイミングのサイドチャネルは、ウェブが誕生したときから存在していました。これらの攻撃は、ブラウザがperformance.now()のような高精度のタイマーを出荷し始めたときに、新しい注目を集め、時とともに異なるレベルの影響を持つようになりました。
タイミング測定値を得るために、攻撃者は暗黙的または明示的なクロックを使用する必要があります。これらのクロックは、XS-Leaksの目的では通常交換可能であり、精度と利用可能性が異なるだけです。簡単のために、この記事では、すべてのモダンブラウザに存在する明示的なクロックであるperformance.now()
APIを使用することを想定しています。
このサイドチャネルにより、攻撃者はクロスサイトリクエストが完了するまでにかかった時間から情報を推測することができます。ネットワークのタイミング測定は、ユーザーの状態によって変化する可能性があり、通常は下記の要素に連動します。
- リソースサイズ
- バックエンドでの計算時間
- サブリソースの数とサイズ
- キャッシュの状態.
tip
クロックの種類については、クロックの記事で詳しく説明しています。
モダンなWebのタイミング攻撃 #
performance.now()は、リクエストの実行にかかる時間を測定するために使用できます。
// クロックを開始します。
var start = performance.now()
// fetchリクエストが完了するまでの時間を計測します。
fetch('https://example.org', {
mode: 'no-cors',
credentials: 'include'
}).then(() => {
// fetch終了した時点で時間差を計算します。
var time = performance.now() - start;
console.log("The request took %d ms.", time);
});
Onloadイベント #
同じような処理で、リソースを取得するのにかかる時間を測定するには、単に onload
イベントを監視することで可能です。
// 時間が欲しいページを指すscript要素を作成します。
var script = document.createElement('script');
script.src = "https://example.org";
document.body.appendChild(script);
// クロックを開始します。
var start = performance.now();
// スクリプトがロードされたら、リクエストが完了するまでの時間を計算します。
script.onload = () => {
var time = performance.now() - start;
console.log("The request took %d ms.", time)
}
tip
同様の手法は他の HTML 要素、例えば<img>
,<link>
,<iframe>
にも使うことができ、他の手法が失敗するシナリオで使用することができます。例えば、Fetch Metadataが script タグへのリソースの読み込みをブロックする場合、image タグへの読み込みを許可することがあります。
tip
別の方法として、image.complete
プロパティを使用することもできます。詳しくはこちら
Cross-windowなタイミング攻撃 #
攻撃者は、window.open
で新しいウィンドウを開き、window
の読み込みが始まるのを待つことで、ページのネットワークタイミングを測定することも可能です。以下のスニペットは、この測定の方法を示しています。
// 新しいウィンドウを開き、iframeの読み込みを開始するタイミングを測定します。
var win = window.open('https://example.org');
// 最初の時間を計測します。
var start = performance.now();
// ループを定義します。
function measure(){
try{
// ページがロードされた場合、そのページは異なるオリジンになるので、
// `win.origin`は例外をスローします。
win.origin;
// ウィンドウがsame-originのままであれば、すぐにループを繰り返しますが、
// イベントループはブロックしません。
setTimeout(measure, 0);
}catch(e){
// ウィンドウが読み込まれたら、時間差を計算します。
var time = performance.now() - start;
console.log('It took %d ms to load the window', time);
}
}
// ウィンドウのオリジンが切り替わった時点で抜けるループを開始する
measure();
note
このPOCではsetTimeout
を使って、while(true)
ループに相当する部分を大まかに作成していることに注意してください。JSのイベントループがブロックされるのを避けるために、このような方法で実装する必要があります。
tip
この手法は、イベントループをビジー状態にすることで、ページの実行タイミングを測定することにも応用できます。
イベントのアンロード #
unload
とbeforeunload
イベントは、リソースを取得するのにかかる時間を測定するために使用することができます。これは、ブラウザが新しいナビゲーションを要求したときにbeforeunload
がトリガーされ、一方、そのナビゲーションが実際に発生したときにunloadがトリガーされるためです。この動作により、これら2つのイベント間の時間差を計算し、ブラウザがリソースの取得を完了するまでにかかった時間を測定することが可能です。
info
unload
とbeforeunload
の時間差はx-frame-options
(XFO)ヘッダーの影響を受けません。なぜなら、このイベントはブラウザがレスポンスヘッダーを認識する前に起動されるからです。
以下のスニペットでは、SharedArrayBufferクロックを使用していますが、このスニペットを実行する前に、クロックを開始する必要があります。
// WebWorkerが使用するShared bufferの作成
var sharedBuffer = new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT);
var sharedArray = new Uint32Array(sharedBuffer);
// WebWorkerを起動し、呼び出します。
worker.postMessage(sharedBuffer);
var start;
iframe.contentWindow.onbeforeunload = () => {
// ナビゲーション中の「時間」を取得します
start = Atomics.load(sharedArray, 0);
}
iframe.contentWindow.onpagehide = () => {
// ナビゲーション後の「時間」を取得します
var end = Atomics.load(sharedArray, 0);
console.log('The difference between events was %d iterations', end - start);
};
tip
SharedArrayBufferのクロックは高解像度のタイマーを作成するために使用されました。しかし、iframeのbeforeunload
とunload
イベント間の時間差は、他のクロック、例えば*performance.now()*でも測定できます。
tip
このスニペットでは、iframeを利用して計測しています。この攻撃のバリエーションは、ウィンドウの参照を使用することもできますが、これに対する防御はより困難です。
サンドボックス化されたフレームのタイミング攻撃 #
もしページにFraming Protectionsが実装されていなければ、攻撃者はページとすべてのサブリソースがネットワーク上でロードされるまでの時間を計ることができます。デフォルトでは、iframeの onload
ハンドラはすべてのリソースがロードされ、すべてのJavaScriptの実行が終了した後に呼び出されます。しかし、攻撃者は <iframe>
に sandbox
属性を含めることで、スクリプト実行時のノイズを除去することができます。この属性はJavaScriptの実行を含む多くの機能をブロックし、その結果、ほとんど純粋なネットワーク計測を行うことができます。
var iframe = document.createElement('iframe');
// 対象のWebサイトのURLを設定する
iframe.src = "https://example.org";
// スクリプトの実行をブロックするsandbox属性を設定する
iframe.sandbox = "";
document.body.appendChild(iframe);
// 要求が開始されるまでの時間を測定する
var start = performance.now();
iframe.onload = () => {
// iframeが読み込まれたら、時間差を計算する
var time = performance.now() - start;
console.log("The iframe and subresources took %d ms to load.", time)
}
タイムレスタイミング攻撃 #
この他に、タイミング攻撃を実行するために時間の概念を考慮しないタイプの攻撃もあります。このタイムレス攻撃は、2つのHTTPリクエスト(baseline request及びattacked request)を1つのパケットにまとめ、それらをサーバーに同時に到着させることで成立します。サーバーはリクエストを同時に処理し、その実行時間に基づいたレスポンスを可能な限り最速で返します。2つのリクエストのうちどちらかが先に到着することになり、攻撃者はリクエストの到着順序を比較することで時間差を推測することができます。
この手法の利点は、他の手法では常に存在する、ネットワークのジッターや不確定な遅延から独立していることです。
この攻撃は HTTP の特定のバージョンと共同シナリオに限定されます。それは特定の仮定をし、サーバーの動作に関する要件を持っています。
他のタイプの攻撃は、タイミング攻撃を実行する時間の概念を考慮しません 1。 タイムレスな攻撃は、2 つの「HTTP」リクエスト (ベースラインと攻撃リクエスト) を 1 つのパケットに合わせて、サーバーに同時に到着することを保証することで構成されます。 サーバーはリクエストを同時に処理し、できるだけ早く実行時間に基づいて応答を返します。 2 つのリクエストのうちの 1 つが最初に到着するため、攻撃者はリクエストが到着した順序を比較することで時間差を推測できます。
この手法の利点は、ネットワークのジッターや不確実な遅延から独立していることです。これは、残りの手法に常に存在するものです。
important
この攻撃は、HTTPの特定のバージョンと共同シナリオに限定されています。特定の前提や、サーバの動作に関して満たさなくてはならないことがあります。
対策 #
Attack Alternative | SameSite Cookies (Lax) | COOP | Framing Protections | Isolation Policies |
---|---|---|---|---|
Modern Timing Attacks | ✔️ | ❌ | ❌ | RIP 🔗 NIP |
Frame Timing (Network) | ✔️ | ❌ | ❌ | FIP |
Frame Timing (Sandbox) | ✔️ | ❌ | ❌ | FIP |
Cross-window Timing | ❌ | ✔️ | ❌ | NIP |
Timeless Timing | ✔️ | ✔️ | ❌ | ❓ |
🔗 – Defense mechanisms must be combined to be effective against different scenarios.