【実務・中級編】 Content-Security-Policy(CSP)によるXSSの多層防御 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

CSPは「気休め」か「鉄壁」か?現場で使えるモダンな防御戦略

お疲れ様。現場でコードを書いていて、XSS(クロスサイトスクリプティング)の対策として「エスケープ処理をちゃんとしてるから大丈夫」なんて思っていないか?

はっきり言おう。それは甘い。開発者のミスは必ず起きる。数万行のコードの中で、たった一箇所のエスケープ漏れが、君たちの苦労して作ったサービスを一夜にして崩壊させる。

だからこそ、ブラウザ側で実行されるコードを強制的に縛り上げる Content-Security-Policy (CSP) が重要になる。今回は、単なる気休めのポリシーではなく、攻撃者が「これ以上何もできない」と舌打ちするレベルの堅牢な実装を伝授する。

—

なぜ従来のCSPは突破されるのか

多くのエンジニアが書くCSPは、大抵こんな感じだ。

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com;

一見正しそうに見えるが、これには致命的な盲点がある。もし君たちのサービスが trusted.cdn.com 上に古いバージョンの AngularJS や jQuery のライブラリを置いているとしたら、攻撃者はそこにある「Gadget(脆弱なライブラリ関数)」を悪用して、CSPの制限を容易にバイパスする。

これを「CSP Bypass via Gadgets」と呼ぶ。攻撃者は君たちの許可したドメインの中で、君たちのコードを使って攻撃するんだ。

—

現代の最適解:nonceとstrict-dynamicによる「厳格な防御」

現代のCSPで目指すべきは、「信頼できるソースをリストアップする」という管理コストの高い手法から、「信頼できるスクリプト以外は一切実行させない」というホワイトリスト方式への転換だ。

これを実現するのが nonce と strict-dynamic だ。

1. 実装のロジック

  • nonce: リクエストごとに生成されるランダムな文字列。HTML内の <script> タグにこの値を付与し、ヘッダーのポリシーと一致する場合のみ実行を許可する。
  • strict-dynamic: これを指定すると、許可されたスクリプトが動的に読み込んだ子スクリプトも自動的に許可される。これにより、CDN上のライブラリ管理から解放される。

—

実践:セキュアなCSPの実装例

PHPでWebアプリケーションを構築していると仮定して、フロントエンドとバックエンドの連携コードを示す。

PHP側:nonceの生成とヘッダー付与

<?php
// 1. リクエストごとに cryptographically secure なランダム値を生成
$nonce = base64_encode(random_bytes(16));

// 2. CSPヘッダーを設定
// 'unsafe-inline' は strict-dynamic があるモダンブラウザでは無視される(後方互換用)
header("Content-Security-Policy: script-src 'nonce-$nonce' 'strict-dynamic' https: http:; object-src 'none'; base-uri 'self';");

// 3. テンプレート側に渡すためにグローバルに保持するか、テンプレートエンジンに渡す
?>

HTML側:nonceの適用

<!DOCTYPE html>
<html>
<head>
    <!-- このnonceがないスクリプトはブラウザによって即座にブロックされる -->
    <script nonce="<?php echo $nonce; ?>">
        console.log("このスクリプトは信頼されているので実行されます");
    </script>
</head>
<body>
    <!-- 攻撃者が注入しようとする <script>alert(1)</script> は、nonceがないため実行されない -->
</body>
</html>

—

インフラ層での防御(Nginx設定)

アプリケーション側だけでなく、Webサーバー(Nginx)でも強制的にヘッダーを付与しておくのが鉄則だ。アプリケーションのバグでヘッダーが出力されない事故を防ぐ。

# /etc/nginx/conf.d/security.conf
# 注意: Nginx側でnonceを動的に生成するのは難しいため、
# 厳格なポリシーをベースとして置き、アプリ側で補強する運用がベスト
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; frame-ancestors 'none'; sandbox allow-forms allow-scripts;" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;

—

現場のエンジニアへ:最後の教訓

CSPを設定した直後は、ブラウザのコンソールがエラーで真っ赤になるだろう。これは「攻撃が防がれている」証拠だ。

1. まずは Content-Security-Policy-Report-Only で運用する: いきなりブロックすると正規機能が死ぬ。まずはレポートモードで運用し、どのスクリプトがブロックされているかを確認せよ。
2. Report-URI / report-to を活用せよ: ブロックされた内容をSentryなどの監視ツールに飛ばすように設定すれば、攻撃者がどこでXSSを試みているかが手に取るようにわかる。

攻撃者は常に「抜け穴」を探している。しかし、ポリシーを厳格に管理する君たちは、もはや「ただのアプリケーション開発者」ではない。「ブラウザの挙動をコントロールする防御の守護者」だ。

このコードをコピー&ペーストするだけでなく、なぜこの設定が必要なのか、チームメンバーに語れるようになってほしい。それが、本当のエンジニアリングというものだ。

コメント

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