XSS防御の終着点:CSP script-src で「無力化」という名の聖域を築く
XSS(クロスサイトスクリプティング)は、もはや「古い脆弱性」などではない。攻撃の主戦場がSPA(Single Page Application)やサーバーサイドレンダリングの複雑な交差点へと移り変わる中で、我々が直面しているのは、単なるスクリプト注入を超えた「ブラウザの実行コンテキストの乗っ取り」である。
多くのエンジニアが「入力値のサニタイズ」という泥沼で溺れている間に、ハッカーはDOMの動的な生成過程や、ライブラリの依存関係に潜む脆弱性を突いてくる。今、我々に必要なのは、アプリケーションコードの不完全さを前提とした「ブラウザ側での強制的な防御レイヤー」、すなわち Content-Security-Policy (CSP) の厳格な設計だ。
なぜ unsafe-inline は「死の宣告」なのか
多くの現場で目にする script-src 'unsafe-inline'。これは、家中の窓を全開にして、泥棒に向かって「ここから入ってください」と看板を出しているようなものだ。
攻撃者は、反射型XSSでJavaScriptを注入する際、タグをそのまま挿入するか、あるいは onerror や onload といったイベントハンドラを悪用する。unsafe-inline を許可している限り、ブラウザは「そのスクリプトが開発者の書いたものか、攻撃者の注入したものか」を判別できない。
我々アーキテクトが目指すべきは、この『信頼の根拠(Root of Trust)』をブラウザの実行層に埋め込むことにある。
Nonce(ナンス)ベースの防衛:動的生成の力
最も洗練された防御手段は、リクエストごとに一意のランダムな文字列(nonce)を生成し、それを信頼の証として用いることだ。
CSP設定例
HTTPヘッダーでポリシーを定義 Content-Security-Policy: default-src 'self'; script-src 'nonce-dGhlLWNyeXB0by1rZXk=' 'strict-dynamic';
このポリシーにおいて、ブラウザは以下の条件を満たさない限りスクリプトを実行しない。
1. Nonceの一致: scriptタグに nonce="dGhlLWNyeXB0by1rZXk=" が付与されていること。
2. strict-dynamicの活用: 初回ロードされたnonce付きのスクリプトが、動的に追加した子スクリプトまでを自動的に信頼する(モダンブラウザ向け)。
ここで重要なのは、Nonceは静的なキャッシュを許さないということだ。CDN等でページをキャッシュする場合、レスポンスごとにこの値を書き換える必要がある。面倒? その「手間」こそが、攻撃者にとっての「壁」となるのだ。
ハッシュ(Hash)を用いた静的リソースの固定
外部スクリプトを読み込めない、あるいはページキャッシュを最大限に活用したいという制約があるなら、スクリプトの中身そのものをハッシュ値でホワイトリスト化する。
スクリプトの内容(SHA-256)を事前に計算して固定
Content-Security-Policy: script-src 'sha256-R43...(中略)...=' 'self';
この手法は、サードパーティ製ライブラリのCDN読み込みなど、内容が頻繁に変わらないリソースに対して絶大な効果を発揮する。もし攻撃者がスクリプト内のコードを1バイトでも改ざんすれば、ハッシュ値が一致しなくなり、ブラウザは即座に実行を拒否する。
生成AI時代の新たな脅威:ガードレイルとしてのCSP
昨今、プロンプトインジェクションによって生成AIが「悪意のあるHTMLやスクリプトを出力させられる」ケースが増加している。AIが生成したコードは、それが安全かどうかをAI自身が判断できないことが多い。
ここでCSPが最後のガードレイルとなる。アプリケーション側でAIの出力をレンダリングする際、HTMLに nonce を付与できない構造であれば、CSPの object-src 'none' や base-uri 'self' を併用し、攻撃の伝搬を物理的に断ち切る必要がある。
監査の視点:アーキテクトへの問い
私がセキュリティ監査を行う際、必ずチェックするのが以下の3点だ。
1. unsafe-eval は根絶されているか?: eval() や setTimeout(string) を使用するレガシーなJSが存在しないか。
2. base-uri の設定: タグによる相対パスの乗っ取りを防いでいるか。
3. report-uri または report-to: CSP違反が発生した際、そのログはどこに集約されているか。
特に3番目は重要だ。CSPは単なる防衛壁ではなく、「攻撃の予兆を検知するセンサー」でもある。CSP違反ログを解析すれば、どこでXSSが試行されているか、どの脆弱なライブラリが攻撃の踏み台にされているかが手に取るように分かるはずだ。
結論:完璧な防御は「制約」から生まれる
セキュリティとは、自由を奪うことではない。「何が許され、何が許されないのか」を明確な数学的根拠(暗号論的ハッシュや乱数)に基づいて定義し、それをマシンレベルで強制することだ。
unsafe-inline を排除し、Nonceとハッシュでスクリプトの実行を封鎖する。この泥臭くも精緻な設計を積み重ねることで初めて、我々は攻撃者に対して「このアプリケーションを攻略するのは、コストに見合わない」という諦めを突きつけることができる。
技術は日々進化するが、防御の基本原理は変わらない。攻撃者の視点を持ち、ブラウザという強力なエージェントをいかに味方につけるか。それこそが、我々エンジニアが追求すべき本質的なアーキテクチャである。
コメント