はじめに:CSPを「形式的なお守り」だと思っていないか
ペネトレーションテストの現場において、未だに多くのWebアプリケーションが「とりあえず入れておいた」レベルの脆弱なContent-Security-Policy(CSP)を採用しているのを目にする。 unsafe-inline が平然と許可され、unsafe-eval が放置されているCSPは、セキュリティヘッダーとしての実質的な価値を持たない。それはまるで、頑丈な鉄板の扉に、紙でできた鍵穴がついているようなものだ。
攻撃者の視点から言えば、XSS(Cross-Site Scripting)の脆弱性を発見した際、最初に見るのがこのCSPの設定だ。もしそこに一文字でも unsafe-inline が含まれていれば、DOM Based XSSやStored XSSの爆発力は全く削がれない。
本稿では、現代のフロントエンドアーキテクチャおよび攻撃ベクトルを踏まえ、いかにして「バイパス不可能な真の防御層」としてのCSPを設計・実装するか、その極意をコードとアーキテクチャのレベルから徹底的に紐解いていく。
—
1. 現代のXSSにおけるCSPの役割と限界
古き良きXSSは、単純に <script>alert(1)</script> を画面に反射させるだけのものだった。しかし、モダンなWebアプリケーションでは、ReactやVue.jsなどの仮想DOMの普及、さらにはDOMPurifyのようなサニタイザーの導入により、単純な文字列の挿入は容易にブロックされるようになった。
だが、攻撃者は進化している。彼らが狙うのは、脆弱なライブラリのガジェット(DOM Clobbering)、オープンリダイレクターを組み合わせたスクリプトのロード、そしてJSONPエンドポイントの悪用だ。ここで伝統的な script-src 'self' のような設定は無力化される。なぜなら、多くの場合、オリジン内にはアップロード機能やJSONPを返す古いAPIが存在し、それらが「信頼されたスクリプトの送信元」として悪用されるからだ。
ここで必要になるのが、静的なオリジン信頼モデルの完全な否定と、動的なコンテキスト信頼モデル(nonceとstrict-dynamic)への移行である。
—
2. 壊れたCSPのアンチパターン
まずは、現場でよく見かける「誤った設定」を確認しておこう。以下のヘッダーは、一見すると厳格に見えるが、実質的に何の防御にもなっていない。
# 【危険なアンチパターン】これではXSSを防げない
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com 'unsafe-inline';
この設定の致命的な欠陥は以下の通りである:
1. 'unsafe-inline' が指定されているため、インラインスクリプトの実行が許可される。
2. https://trusted-cdn.com が信頼されているが、もしこのCDN上に任意のファイルをアップロードできる脆弱性(あるいは古いライブラリの脆弱性)があれば、攻撃者は簡単に任意のスクリプトを実行できる。
ペネトレーションテスターの武器庫には、「JSONPインジェクションによるCSPバイパス」や「AngularJSのテンプレートインジェクション(unsafe-eval がある場合)」といった手法が常備されている。これらを無効化するには、根本的なポリシーの書き換えが必要だ。
—
3. nonceと strict-dynamic による要塞化
現代のCSPにおけるゴールドスタンダードは、'nonce'(暗号学的ノンス)と 'strict-dynamic' を組み合わせた実装である。
このアプローチの思想はシンプルだ。「サーバー側が動的に生成した信頼できるスクリプト(nonceを持つもの)以外は、一切の実行を拒否する。ただし、その信頼されたスクリプトがさらに別のスクリプトを動的に読み込む場合(モジュールローダーなど)に限り、その子孫も暗黙的に信頼する」というものだ。
実装アーキテクチャの全体像
リクエストごとに強度の高いランダム値を生成し、HTTPレスポンスヘッダーとHTMLの <script> タグの両方に付与する。
<?php
// リクエストごとに予測不可能な暗号学的ランダムnonceを生成
// CSPヘッダーとスクリプトタグの両方にこれを埋め込む必要がある
$nonce = base64_encode(random_bytes(16));
// 厳格なCSPヘッダーの送信
header("Content-Security-Policy: default-src 'self'; script-src 'nonce-{$nonce}' 'strict-dynamic' https:; object-src 'none'; base-uri 'none';");
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Secure CSP Implementation</title>
</head>
<body>
<h1>CSP Test Page</h1>
<!-- サーバー側で生成された正しいnonceを持つスクリプトは実行される -->
<script nonce="<?php echo $nonce; ?>">
console.log("このスクリプトは信頼されており、実行されます。");
</script>
<!-- 攻撃者が注入したnonceを持たないスクリプトはブラウザによって即座にブロックされる -->
<!-- <script>alert('XSS');</script> -->
</body>
</html>
パラメータの深い解説
script-src 'nonce-{$nonce}' 'strict-dynamic''nonce-...': 許可された一回限りのトークン。攻撃者は推測不可能なため、インラインスクリプトを勝手に実行できない。'strict-dynamic': このフラグを指定すると、nonceを持つスクリプト(親)が動的に読み込む外部スクリプト(子)への信頼が自動的に継承される。これにより、WebpackやViteなどのモダンなバンドラーで作られたアプリケーションが、CSPによって壊れるのを防ぐことができる。object-src 'none'<object>や<embed>、<applet>タグを通じたプラグインベースの攻撃(Flashや古いJavaアプレットなど)を完全に遮断する。これはXSS対策の基本中の基本だ。base-uri 'none'- HTMLの
<base>タグの書き換えによる相対パス・ハイジャック(相対パスで読み込まれるスクリプトの向き先を攻撃者のサーバーに変更する手法)を防ぐ。
—
4. レポートモード(CSP Report-Only)を活用した安全な導入プロセス
いきなり本番環境で厳格なCSP(Content-Security-Policy ヘッダー)をデプロイすると、既存の正当なサードパーティ製ウィジェットやアナリティクス(Google Analyticsなど)、社内ツールが突然ブロックされ、サービスが停止する「自爆テロ」を引き起こすリスクがある。
これを回避するためには、必ず Content-Security-Policy-Report-Only ヘッダーから始めることだ。このモードでは、ポリシーに違反する動作があってもブロックはされず、ブラウザが指定のエンドポイントにJSON形式の違反レポートを送信する。
レポートを受信するPHPエンドポイントのサンプル
<?php
// 署名やログの記録を行うレポート受信用スクリプト
$input = file_get_contents('php://input');
if (!empty($input)) {
$data = json_decode($input, true);
if ($data && isset($data['csp-report'])) {
$report = $data['csp-report'];
// 実運用ではSyslogやSIEM(SplunkやDatadogなど)へ転送する
$logMessage = sprintf(
"CSP Violation: Blocked URI: %s, Violated Directive: %s, Document URI: %s\n",
$report['blocked-uri'] ?? 'N/A',
$report['violated-directive'] ?? 'N/A',
$report['document-uri'] ?? 'N/A'
);
file_put_contents('/var/log/csp_violations.log', $logMessage, FILE_APPEND);
}
}
http_response_code(204);
このレポートを数日間〜数週間監視し、意図しないブロックがないことを確認した上で、初めて実際の Content-Security-Policy ヘッダーへと切り替える。これがインシデントを防ぐプロのデプロイ作法である。
—
おわりに:攻撃者にとっての「壁」を高くするために
CSPは、アプリケーションコードに潜むXSS脆弱性を「魔法のように消し去る」ものではない。しかし、仮に脆弱性が存在したとしても、それを致命的なシステム乗っ取り(セッションハイジャックや不正送金スクリプトの実行)へエスカレートさせないための、最後の防壁(ディフェンス・イン・デプス)である。
テックリードやセキュリティアーキテクトが担うべきは、開発者の利便性を盾にした「緩いCSP」の妥協を許さず、nonceと strict-dynamic を軸としたモダンな要塞を構築することだ。攻撃者が「このサイト、CSPが堅すぎてスクリプトインジェクションが刺さらないぞ」と舌打ちして諦める瞬間を作ることこそが、我々オフェンシブ・ディフェンシブ双方のプロフェッショナルが目指すべきゴールである。
コメント