CSPは「設定して終わり」の特効薬ではない:現代のフロントエンド防衛論
「CSPを導入したからXSSは完璧だ」と豪語するエンジニアに出会うと、私は決まってそのCSPヘッダーをブラウザのデベロッパーツールで確認する。十中八九、`’unsafe-inline’`や`’unsafe-eval’`が放置され、結局は「穴の空いた防弾チョッキ」を着ているのと同じ状態になっているからだ。
CSP(Content-Security-Policy)は単なる文字列の羅列ではない。それはブラウザという実行環境に対する「強制的な制約ルール」であり、攻撃者がいかに巧妙にDOMを操作しようとも、その背後にあるポリシーが堅牢であれば、実行コードの注入を物理的に遮断できる防衛壁だ。
今日は、教科書的な説明は省く。現場で泥をすすり、インシデントの火消しをしてきた我々が、いかにして「破られない」CSPを設計すべきか、その深淵に切り込んでいく。
—
1. 聖域を定義する:script-src, object-src, base-uri の鉄則
まず、現代のWebアプリケーションにおいて、以下の3つのディレクティブは「聖域」として扱う必要がある。
object-src ‘none’
これは議論の余地がない。プラグイン(FlashやJavaアプレットなど)を動かすための`object`, `embed`, `applet`タグは、攻撃者の格好の侵入経路だ。現在このタグを正当な理由で使う必要性はほぼゼロに近い。迷わず `’none’` を指定せよ。
base-uri ‘self’
意外と見落とされるのがこれだ。`base`タグを悪用されると、ページ内のすべての相対URL(スクリプトの読み込みパスなど)を攻撃者が用意した外部ドメインへ強制的に書き換えられる。これだけで、いかに厳格なCSPを組んでいても、攻撃者は「正当なスクリプト」を「悪意ある偽サイトのスクリプト」へ差し替えることができてしまう。
script-src の現代的アプローチ
`script-src`に `’unsafe-inline’` を含めることは、防衛を放棄するのと同じだ。我々は「インラインスクリプトを一切許容しない」か、「nonce(ナンス)やhashで厳密に制御する」かの二択しか持っていない。
—
2. nonce(ナンス)による「信頼の境界線」の実装
動的に生成されるインラインスクリプトをどうしても排除できない場合(例えば、SSRで埋め込まれた状態管理データや、特定の微調整スクリプト)、`nonce`を用いるのが現代のベストプラクティスだ。
nonceは、サーバー側でリクエストのたびに生成する「予測困難なランダム値」である。
レスポンスヘッダーの例
Content-Security-Policy: default-src ‘self’; script-src ‘nonce-dGhlLXNlY3VyZS1yYW5kb20tdmFsdWU=’;
ここが重要: nonceの強度は、サーバー側の乱数生成の質に依存する。また、CDNのキャッシュでnonceが固定化されないよう、ヘッダーの付与はアプリケーションサーバー層で行うこと。キャッシュに乗せてしまうと、攻撃者が一度nonceを盗み取るだけで、そのnonceが有効な間は攻撃し放題となる。
—
3. 生成AI時代の脅威:プロンプトインジェクションとCSP
最近のトレンドとして、生成AIが生成したコードをそのままブラウザで実行するケースが増えている。ここで恐ろしいのは、AIが「もっともらしいが脆弱なコード」を生成し、それをユーザーが盲信して実行することだ。
もしあなたのアプリケーションがAIの出力をDOMにレンダリングするなら、CSPは「最後の防御層」として機能しなければならない。万が一、AIが`onerror`属性を埋め込んだとしても、`script-src`がハッシュやnonceで固められていれば、インラインイベントハンドラー(`onclick=”…”`)は実行されず、XSSの連鎖を断ち切れる。
—
4. 実戦的なCSP設計のロードマップ
いきなり厳格な設定を投入して、本番環境の機能が全滅したという悪夢は避けたい。以下のステップで進めるのが、プロのやり方だ。
1. Report-Onlyモードで開始する
`Content-Security-Policy-Report-Only` ヘッダーを用いて、違反を検知するがブロックはしない状態でログを収集する。Sentryや独自のレポートエンドポイントで、どのスクリプトがブロックされているか徹底的に洗い出す。
2. サードパーティスクリプトを排除する
Google Analyticsや広告タグの多くは、外部スクリプトの読み込みを要求する。これらは可能な限りローカルプロキシ経由で配信するか、信頼できるドメイン(`strict-dynamic`の利用を検討)に限定する。
3. Strict CSPへの移行
最終的には、以下の構成を目指す。
推奨される厳格なCSPヘッダー構成
Content-Security-Policy:
default-src ‘self’;
script-src ‘nonce-random-value’ ‘strict-dynamic’ https:;
object-src ‘none’;
base-uri ‘self’;
require-trusted-types-for ‘script’; # Trusted Typesの有効化
- Trusted Typesの活用: これは次世代の防衛策だ。`innerHTML`への直接代入をブラウザ側で禁止し、あらかじめ許可した「型(TrustedHTML)」のみがDOMに挿入できる仕組み。これでDOMベースのXSSはほぼ絶滅する。
—
最後に:防御は「隙」を突く作業である
セキュリティとは、いかに攻撃者の思考を先回りし、彼らが頼りにする「ブラウザの柔軟な挙動」を、意図的に「不自由な檻」に閉じ込めるかというゲームだ。
CSPは、開発者にとっては時に煩わしい存在かもしれない。しかし、インシデントが発生した際、その「煩わしさ」が会社を守る最後の砦になることを忘れないでほしい。脆弱性を見つけるのがハッカーの仕事なら、その脆弱性が存在してもなお「実行させない」環境を作るのが、我々アーキテクトの仕事だ。
あなたのアプリケーションのヘッダーは、今夜も攻撃者を跳ね返しているだろうか。まずは、`unsafe-inline`を削除するところから始めてほしい。

コメント