CSPは「お守り」ではない。XSSを無力化する「防壁」だ。
現場でコードをレビューしていると、CSP(Content Security Policy)を「とりあえず入れておく程度のヘッダー」と勘違いしているエンジニアが多すぎる。`unsafe-inline`を許容して「はい、エラーが出なくなりました」と満足しているなら、それは玄関の鍵を開けたままセキュリティステッカーを貼っているようなものだ。
今日は、攻撃者がXSS(クロスサイトスクリプティング)を仕掛ける際の「心理」と「盲点」を踏まえ、現代Web開発で通用する「強固なCSP設計」について語ろうと思う。
—
1. 攻撃者はどこを狙うのか?(PoCのリスク)
XSSの目的は、多くの場合「セッションハイジャック」や「管理者の権限奪取」だ。脆弱なアプリでは、攻撃者は以下のように `
`unsafe-inline` を許可した甘いCSPを設定していると、これらのスクリプトはブラウザによって「信頼できるもの」と見なされ、即座に実行される。あなたがどれほど強固な認証を実装していても、実行権限を持つJSがブラウザ上で動いてしまえば、ゲームオーバーだ。
---
2. 厳格なCSPの設計思想:NonceとStrict-Dynamic
「厳格なCSP」とは、「信頼できないスクリプトを一切実行させない」という哲学のことだ。これを実現するための鍵が `nonce` と `strict-dynamic` である。
実装の戦略
1. Nonce(乱数): サーバー側でリクエスト毎に使い捨てのトークンを生成し、許可されたスクリプトタグにのみ付与する。
2. Strict-Dynamic: Nonceが付与されたスクリプトから読み込まれる「子スクリプト」も自動的に信頼させる。これにより、依存関係の複雑なJSライブラリにも対応できる。
---
3. 実装サンプル:NginxとPHPでのセキュアな構成
まずはNginx側でベースとなるポリシーを定義し、PHP側で動的なNonceを生成するのが現場の定石だ。
PHP側の実装例(Nonceの生成と出力)
Nginx設定ファイル(堅牢なベースライン)
NginxでCSPヘッダーを強制する設定例
add_header Content-Security-Policy "
default-src 'self';
script-src 'nonce-ランダムな値' 'strict-dynamic' https:;
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
upgrade-insecure-requests;
" always;
- `object-src 'none'`: Flash等の古いプラグインによる攻撃を完全遮断。
- `base-uri 'none'`: `
`タグによるドメインハイジャックを防止。 - `frame-ancestors 'none'`: クリックジャッキング攻撃を無効化。
- `upgrade-insecure-requests`: 全てのHTTP通信を強制的にHTTPSへ昇格させる。
---
4. レポート機能の実装:インシデントの予兆を捉える
CSPを導入した直後は、既存の機能が壊れる可能性がある。ここで重要なのが `report-to` または `report-uri` だ。
add_header Content-Security-Policy "default-src 'self'; report-uri /csp-violation-report-endpoint;";
サーバーサイドで `/csp-violation-report-endpoint` を作成し、JSON形式で送られてくる違反ログを収集する。「なぜブロックされたのか」を解析するまでがセキュリティエンジニアの仕事だ。 ログを溜め込み、分析し、ポリシーを微調整する。この泥臭いサイクルこそが、システムを「鉄壁」へと進化させる。
---
後輩エンジニアへ、最後の助言
CSPは「魔法の杖」ではない。XSSの根本的な原因は、ユーザー入力を適切にエスケープしていない、あるいはコンテキストに応じた出力処理をしていないことにある。
CSPはあくまで「万が一、脆弱性が残っていた場合の最後の防波堤」だ。
1. 入力値を信じるな(バリデーション)。
2. 出力値を信じるな(エスケープ)。
3. そして、CSPで蓋をせよ。
この三段構えができて初めて、胸を張って「セキュアな開発をしている」と言える。まずは今のプロジェクトのCSP設定を確認し、`unsafe-inline` の文字がないか探すことから始めてほしい。それが、君がプロのセキュリティエンジニアとして歩む第一歩だ。

コメント