Proxyオブジェクトによる計装でPostMessage XSSの検出を強化する
JavaScriptのProxyオブジェクトを使ってPostMessageクロスサイトスクリプティング(XSS)脆弱性を検出し、従来の動的ファジング手法を強化する新たな方法を紹介します。
はじめに
クロスサイトスクリプティング(XSS)脆弱性は、根強い脅威であり続けています。XSSは、Webアプリケーションの動的な性質を悪用して悪意あるスクリプトを実行します。XSS攻撃を可能にする無数の経路の中でも、HTML5のpostMessage APIは、クロスオリジン通信を実現する手段として広く使われていることから際立っています。
しかし、この有用性は、特に厳密に検証されていない場合、脆弱性のパンドラの箱を開けることにもなります。従来の検出手法はある程度は有効であるものの、特に動的に生成される複雑なWeb環境では、こうした脆弱性を正確に特定して緩和するには力不足であることが少なくありません。
本記事では、PostMessage XSS脆弱性の検出を強化する新たなアプローチを紹介します。このアプローチは、JavaScriptのProxyオブジェクトによる計装の能力を活用し、動的ファジングに先立つプロファイリングのフェーズと組み合わせます。
Webアプリケーションの事前プロファイリングにProxyオブジェクトによる計装を組み込むことで、より的を絞った効率的なファジングプロセスの土台を整えます。このアプローチにより、微妙なXSSの攻撃ベクトルの検出が効率化されます。
以降のセクションでは、このアプローチとその実装の技術的な詳細を掘り下げます。この探究を通じて、開発者、セキュリティの専門家、研究者に、常に存在するXSS攻撃の脅威からWebアプリケーションを守るための強力なツールセットを提供することを目指します。
背景
HTML5に不可欠なpostMessage APIは、クロスオリジンのメッセージ受け渡しを可能にすることで、Webにおける通信に革命をもたらしました。異なるオリジンのドキュメント間のやり取りを容易にするために設計されたこのAPIは、サードパーティのウィジェットから複雑なシングルページアプリケーションまで、あらゆるものを支えており、現代のWebで重要な役割を果たしています。しかし、その柔軟性と強力さゆえに、主にクロスサイトスクリプティング(XSS)攻撃による悪用の標的となっています。
XSS攻撃では、ユーザーが特定のサイトに対して抱いている信頼を悪用し、他のユーザーが閲覧するWebページに悪意あるスクリプトを注入します。従来のXSS脆弱性は、適切にサニタイズされていないユーザー入力がWebページのコンテンツに直接含まれることで発生します。postMessage APIは、クロスオリジンでメッセージを送信することで、こうした攻撃に新たなベクトルをもたらします。そのメッセージには、受信側のページのコンテキストで実行され得る悪意あるコンテンツが含まれる可能性があります。
よくある脆弱性と落とし穴
オリジンの検証
postMessageに関連する主な脆弱性の一つは、オリジンの検証の欠如です。メッセージを受信したときにメッセージのオリジンを確認しなかったり、その確認を誤って実装したりすると、悪意ある送信元からのメッセージを受け入れてしまう可能性があります。次の例は脆弱な実装を示しています。
window.addEventListener('message', (event) => {
// Dangerous: No check for `event.origin`
eval(event.data);
});
このスニペットでは、メッセージイベントのリスナーが、オリジンを検証せずに、受信したあらゆるメッセージに含まれるコードを無差別に実行しています。このアプローチは任意のJavaScriptコードの実行に道を開き、XSS攻撃の格好の標的となります。
オリジンの検証を行うコードの例です。
window.addEventListener('message', (event) => {
// Securely checking the origin of the message
if (event.origin === 'https://trusted-origin.com') {
// Assuming the content is safe, further validation can also be implemented here
eval(event.data);
} else {
console.error('Untrusted origin:', event.origin);
}
});
クロスサイトスクリプティング
PostMessage XSSは、アプリケーションがpostMessage APIを通じて受信したデータを不適切に扱い、信頼できない、潜在的に悪意のあるコードが実行されることで発生します。この脆弱性は、多くの場合、よくある2つの見落としから生じます。
- オリジンの検証の欠如:受信したメッセージのオリジンを検証しないと、攻撃者が信頼できない送信元から悪意あるメッセージを送れてしまう可能性があります。
- メッセージ内容の不適切なサニタイズ:受信したメッセージの内容を、十分にサニタイズせずに信頼できる入力として扱うと、任意のJavaScriptコードの実行につながる可能性があります。
postMessageを通じて受信したデータに基づいてコンテンツを動的に更新するため、メッセージを待ち受けるWebアプリケーションを考えてみましょう。
window.addEventListener('message', (event) => {
// Assume the content of the message is a URL to be navigated to
if (event.data.url) {
window.location.href = event.data.url; // Potential for exploitation
}
});
このシナリオでは、攻撃者はJavaScript URL(javascript:)を含むメッセージを作成でき、任意のコード実行につながります。
parent.postMessage({url: "javascript:alert('XSS')"}, "*");
JavaScriptのProxyオブジェクト
JavaScriptのProxyオブジェクトは、ECMAScript 2015(ES6)で導入された強力な機能で、別のオブジェクトに対するプロキシを作成できます。これにより、そのオブジェクトに対するプロパティの参照、代入、列挙、関数呼び出しといった基本的な操作を傍受して再定義できます。この能力は、オブジェクトの仮想化、ロギング、プロファイリング、データ検証など、さまざまな高度なシナリオで特に有用です。
Proxyオブジェクトによる計装の基本
Proxyオブジェクトは、2つのパラメーターで作成されます。カプセル化する対象のオブジェクトと、さまざまな操作に対するトラップを定義するハンドラーオブジェクトです。傍受のロジックはハンドラーオブジェクトに定義します。基本的な例を示します。
let target = {};
let handler = {
get: function(obj, prop) {
console.log(`Accessing property ${prop}`);
return prop in obj ? obj[prop] : 37; // Default value
}
};
let proxy = new Proxy(target, handler);
console.log(proxy.a); // Output: Accessing property a
// 37 (since 'a' is not a property of target)
この例では、getトラップがプロパティへのアクセスをログに記録し、対象のオブジェクトにプロパティが存在しない場合はデフォルト値を返します。
Proxyオブジェクトの制限
JavaScriptのProxyは、セキュリティの計装やその他の高度な操作のための強力な機能を提供する一方で、いくつかの制限もあります。開発者がアプリケーションでProxyを効果的に活用するには、これらの制限を理解することが不可欠です。
- ネイティブ型に対する操作を傍受できない:JavaScriptのProxyは、文字列、数値、ブール値などのネイティブ型に対して行われる操作を直接傍受できません。これらの型はオブジェクトではないため、それらに対する操作をProxyで傍受することはできません。たとえば、Proxyを使って文字列の比較や変換を直接傍受することはできません。この制限は、プリミティブ値を含む操作を監視・検証する能力に影響する可能性があります。
let proxy = new Proxy('example string', handler); // This will throw an error
- 一部の組み込みオブジェクトの操作を監視できない:Proxyは、配列のlengthを直接変更したり、関数にプロパティを設定したりするなど、組み込みオブジェクトに対する特定の操作を傍受できません。メソッド呼び出しやプロパティへのアクセスは傍受できますが、setter/getterによるアクセスを発生させない内部プロパティの変更は、Proxyの手の届かないところにあります。
let numbers = new Proxy([1, 2, 3], handler);
numbers.length = 2; // This operation cannot be intercepted directly
- 一部の組み込み関数に対して透過的でない:JavaScriptの一部の組み込み関数やメソッドは、引数がProxyオブジェクトの場合に異なる挙動を示すことがあります。たとえば、
Array.isArray(proxyObject)は、プロキシが配列をカプセル化していてもfalseを返します。この非透過的な挙動により、型チェックやネイティブの挙動に依存するコードで予期しない結果が生じる可能性があります。
Proxyオブジェクトによる計装を用いたPostMessage XSSの検出
メッセージのプロファイリング
ファジングの前にプロファイリングのフェーズを組み込むことで、特にpostMessageハンドラーとストレージへのアクセスパターンに焦点を当てて、JavaScriptアプリケーション内の脆弱性を特定する有効性と効率が高まります。
プロファイリングのステップでは、postMessageハンドラーとメッセージ、そしてWebストレージ(localStorageやsessionStorageなど)とのやり取りの取得に焦点を当て、アプリケーションの挙動に関する詳細な知見を収集することを目指します。この情報には、主に2つの目的があります。
- ファジングの対象の再設定:アプリケーションのやり取りとデータフローに関する詳細な情報を収集することで、ファジングの入力を調整し、より多くのコードパスをより効果的に実行させることができ、そうしなければ隠れたままになっていたかもしれない脆弱性の発見につながります。
- パフォーマンスの向上:プロファイリングによって、アプリケーションが利用する関連性のある入力を特定でき、後続のファジングのフェーズをそれらの領域に集中させられます。この的を絞ったアプローチにより、無関係な入力に時間を浪費することを避け、ファジングプロセスの効率を最大化できます。
postMessageハンドラーを取得するために、addEventListenerメソッドをオーバーライドして、登録されるすべてのイベントリスナーをログに記録します。これは、ファジングの潜在的な対象となり得るメッセージハンドラーを特定するのに特に有用です。
// Object to store event listeners
const registeredEventListeners = [];
// Save the original addEventListener method
const originalAddEventListener = EventTarget.prototype.addEventListener;
// Override the addEventListener method
EventTarget.prototype.addEventListener = function(type, listener, options) {
// Store the event details
registeredEventListeners.push({ element: this, type, listener, options });
// Call the original addEventListener method
originalAddEventListener.call(this, type, listener, options);
};
このコードスニペットは、登録された各イベントリスナーを配列に格納するため、後からイベントリスナー、特にメッセージイベントに関連するものを容易にレビュー・解析できます。
postMessageの活動を監視してログに記録するために、効率とシンプルさを重視してコンソールを使い、送信されるメッセージをログに記録するカスタムハンドラーを追加します。
function handleMessage(event) {
console.error("magic_post_message", JSON.stringify({data: event.data, origin: event.origin}));
}
window.addEventListener('message', handleMessage, false);
このハンドラーは受信メッセージを取得し、その内容とオリジンをログに記録します。これにより、アプリケーション内でpostMessageがどのように使われているかについての貴重な知見が得られ、ファジングの際にさらに調査すべき潜在的な領域が浮かび上がります。
プロファイリングは、Proxyを使ってアクセスをログに記録することで、ネストしたプロパティへのアクセスを含め、アプリケーションがオブジェクトとどのようにやり取りするかの追跡にまで及びます。
/**
* This function creates a proxy around the provided obj that tracks access to its properties. If a property of the object
* (or a nested object) is accessed, the accessHandler function is called with the object, the property name, and the path
* to the property.
*/
function createAccessTrackingProxy(obj, accessHandler) {
// A recursive function to create a proxy for an object and its nested objects
const createProxy = (target, path) => {
return new Proxy(target, {
get(target, property, receiver) {
// Trigger the access handler function
accessHandler(obj, property, path);
// Check if the property accessed is an object and not null for recursion
if (target[property] !== null && typeof target[property] === 'object') {
return createProxy(target[property], path.concat(property));
}
// Return the actual property value
const value = Reflect.get(target, property, receiver);
if (isObject(value)) {
return createProxy(value, path.concat(property));
} else {
if (coinFlip()) {
return createProxy({}, path.concat(property));
} else {
return value;
}
}
}
});
};
return createProxy(obj, []);
}
/**
* Logs path of object accesses
*/
function accessHandler(obj, property, path) {
try {
console.log('magic_post_message', JSON.stringify({
data: obj,
origin: null,
path: [...path, property],
}));
} catch (e) {
// Pass.
}
}
このアプローチでは、オブジェクト(またはネストしたオブジェクト)の周りにProxyを作成し、そのプロパティへの各アクセスをログに記録します。accessHandler関数はプロパティがアクセスされるたびに呼び出され、アクセスされたプロパティへのパスをログに記録します。この詳細な追跡は、アプリケーションがデータとどのようにやり取りするかを理解するのに役立ち、ファジングプロセスにさらなる情報を与えます。
この文脈でコイントスを使うのは、プロファイリングのフェーズに変動性を持ち込むシンプルな方法です。実際の値を返すか、新しいオブジェクトへのプロキシを返すかを動的に決めることで、隠れたパスや挙動を明らかにできる可能性があります。
PostMessageの動的ファジング
ファジングのステップでは、プロファイリングのフェーズで収集したメッセージのコーパスを、発見したアクセスパスで拡充します。この拡張プロセスは、ファジングがJavaScriptアプリケーション内のより広範なコードパスを確実に実行するようにすることで、より入り組んだ脆弱性を明らかにするための鍵となります。拡張の後、メッセージセットを最小化して冗長な入力や無関係な入力を取り除き、ファジングプロセスの効率を最適化します。
Ostorlabは、ネストしたエンコード方式を理解する挿入ポイント生成器を採用しています。この能力は、base64、XML、HTTPクエリ、JSONのエンコードを含むネストした構造を持ち得るSAMLメッセージのような、複雑なオブジェクトを扱うアプリケーションのテストに不可欠です。
以下のコードは、ファザーが収集したpostMessageイベントを反復処理し、収集したアクセスパスに基づいてファジング用のメッセージを動的に構築し、アプリケーションを探るための挿入を生成する様子を示しています。
if request.profile is not None:
post_message_events = request.profile.post_messages or []
for post_message_event in post_message_events:
post_message = post_message_event["data"]
# Traverse the message structure based on collected paths
post_message_pointer = post_message
for elm in post_message_event.get("path", []):
if elm not in post_message_pointer:
post_message_pointer[elm] = {}
post_message_pointer = post_message_pointer[elm]
# Generate and yield fuzzed messages
for generated_insertions in self.generate(post_message):
insertions = [WhatInsertionPoints.POST_MESSAGE, post_message]
insertions.extend(generated_insertions)
yield insertions
# Repeat the process for JSON-encoded messages
for generated_insertions in self.generate(json.dumps(post_message)):
insertions = [WhatInsertionPoints.POST_MESSAGE, json.dumps(post_message)]
insertions.extend(generated_insertions)
yield insertions
クロスサイトスクリプティング(XSS)脆弱性は、ファジング用のメッセージによって発火したJavaScriptメソッドのコールバックが悪意あるコードを実行したときに検出されます。Ostorlabは、XSS脆弱性が発火するたびにconsole.traceを利用してスタックトレースを収集することで、検出プロセスを強化しています。このアプローチにより、脆弱性に至る実行経路を正確に特定でき、より深い解析とより効果的な修復が容易になります。
まとめ
プロファイリング、Proxyオブジェクトによる計装、そして従来の動的ファジングの組み合わせは、これまで見つかっていなかったPostMessage XSS脆弱性の検出に効果的であることが実証されました。
しかし、文字列比較などの課題は、さらなる強化の必要性を浮き彫りにしています。次のステップとして、フィールドの比較を検出する静的解析を取り入れ、さらに優れたカバレッジを確保する予定です。