現場で泥をすすりながらインシデント対応をしていると、つくづく思うことがある。それは「完璧な防御など存在しないが、壊滅的な被害を未然に防ぐ『防波堤』は必ず設計できる」ということだ。
今日は、XSS(クロスサイトスクリプティング)という、Webセキュリティにおける「終わらない戦い」を終わらせるための最強の盾、Content-Security-Policy(CSP)について深掘りする。教科書的な説明は省く。現場で「なぜその設定が必要なのか」「どう運用すべきか」という核心に迫ろう。
—
なぜ「甘いCSP」は無意味なのか
多くの現場で目にするのが、script-src * 'unsafe-inline' といった、穴だらけのポリシーだ。「とりあえず動くように」という甘えが、攻撃者にエクスプロイトの余地を与える。
攻撃者が狙うのは、脆弱なアプリケーションの入力箇所から挿入される <script>alert(document.cookie)</script> だけではない。最近の攻撃は、正規のライブラリに悪意あるペイロードを紛れ込ませる「サプライチェーン攻撃」や、インジェクションによる「ドメイン内での悪意あるスクリプト実行」が主戦場だ。
これらを防ぐ唯一の道が、「何が実行を許可されているかをホワイトリスト化し、インラインスクリプトを原則禁止すること」にある。
—
実践:Nonce(ナンス)を活用した強固なCSP設計
unsafe-inline を封じると、ページ内の <script> タグが一切動かなくなる。これを回避するために、リクエストごとに動的な乱数(Nonce)を発行し、許可されたスクリプトにのみその鍵を持たせる手法をとる。
1. サーバーサイドでのNonce生成(PHPの例)
まず、サーバー側で暗号論的に安全なランダム値を生成し、HTTPレスポンスヘッダーに埋め込む。
<?php
// CSP用Nonceを生成(Base64エンコードされた安全なランダム文字列)
$nonce = base64_encode(random_bytes(16));
// CSPヘッダーをセット(script-srcにnonceを付与)
header("Content-Security-Policy: default-src 'self'; script-src 'nonce-{$nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'self';");
?>
<!DOCTYPE html>
<html>
<head>
<!-- 許可されたスクリプトにnonceを付与 -->
<script nonce="<?php echo htmlspecialchars($nonce, ENT_QUOTES); ?>">
console.log("このスクリプトは許可されているため実行されます。");
</script>
</head>
<body>
<h1>CSPで守られたページ</h1>
</body>
</html>
2. なぜ strict-dynamic なのか
現代のWebアプリは、メインのJSがさらに別のJSを動的に読み込むことが多い。strict-dynamic を付与することで、Nonceが付与されたスクリプトから動的に生成されたスクリプトも信頼されるようになる。これにより、運用コストを下げつつセキュリティを担保できる。
—
インフラ層での防御(Nginx設定)
アプリケーション側だけでなく、Webサーバー(Nginx)側でもHTTPヘッダーを強制する姿勢が重要だ。仮にアプリが攻撃されても、WebサーバーがCSPを吐き出せば被害は限定的になる。
# /etc/nginx/conf.d/security.conf
# 厳格なCSPヘッダーの付与
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-random-value-here'; object-src 'none'; frame-ancestors 'none'; form-action 'self';" always;
# XSSをブラウザ側で検知するフィルタを強制(レガシーブラウザ対策)
add_header X-XSS-Protection "1; mode=block" always;
※ 本来、nonce は動的なため、Nginx単体で設定する場合は ngx_http_headers_more_filter_module 等でレスポンスを書き換えるか、基本的にはアプリケーションコードで制御するのが定石だ。
—
攻撃者が狙う「盲点」への対策
CSPを導入しても、以下のディレクティブを忘れていると脆弱性が残る。
object-src 'none': Flash等のプラグインによる攻撃を完全に無効化する。これがないと、古いプラグイン経由でコード実行されるリスクが残る。base-uri 'self': これを忘れると、攻撃者が<base>タグを注入し、相対パスのJS読み込み先を自分のサーバーに書き換えられてしまう。connect-src: データの送信先を制限する。万が一XSSが成功しても、攻撃者のサーバーへデータを盗み出す(Exfiltration)行為をブロックできる。
—
最後に:完璧を目指さず、しかし妥協しない運用を
CSPの導入は、最初は「画面が真っ白になる」という地獄を見るかもしれない。だからこそ、まずは Content-Security-Policy-Report-Only ヘッダーを使って、ログを収集することから始めよ。
ブラウザが「もしこのポリシーを適用していたらブロックしていたもの」を、JSON形式でレポートサーバーに送信してくれる。これで既存の動線を確認しながら、徐々にポリシーを「厳格」へとシフトさせていくのがプロのやり方だ。
セキュリティは、ツールを入れたら終わりではない。「何が正当な挙動か」をエンジニアが定義し続ける作業だ。その泥臭い努力だけが、顧客の信頼とプロダクトの安全を支えている。
君たちのコードが、今日よりも少しだけ強固になることを願っている。質問があればいつでも来い。
コメント