Content Security Policyの内部動作と回避耐性
XSSをコードではなくポリシーで抑え込む防御線。ディレクティブの評価順、nonce/hash/strict-dynamic の判定、そして実在するバイパス経路まで原理から押さえ、本当に効く CSP を書けるようになります。
- 1.CSP はレスポンスヘッダで宣言する許可リスト。ブラウザがリソース読み込み・スクリプト実行のたびに、対応ディレクティブをソースリストへ照合し、不一致なら拒否してレポートする。
- 2.allowlist 方式は CDN やJSONP の存在で穴だらけになりやすい。nonce/hash でインラインを個別許可し、strict-dynamic で動的生成スクリプトに信頼を伝播させる「strict CSP」が現代の本命。
- 3.それでも回避は残る。script-src を厳格化しても dangling markup や base-uri 未指定、policy injection、信頼した CDN 上の gadget で抜かれる。CSP は多層防御の一層であり、出力エスケープの代替ではない。
Content Security Policy(CSP)は「このページが何を読み込み・実行してよいか」をサーバーが宣言し、ブラウザが強制する許可リストです。XSS を出さないための入力対策とは別に、XSS が出てしまっても被害を抑える緩和層として働きます。ただし書き方を誤ると簡単に回避されます。ここでは評価アルゴリズムと既知のバイパスを原理から見ていきます。
ポリシーはどう適用されるか
CSP は Content-Security-Policy レスポンスヘッダ(または <meta http-equiv>)で配信され、セミコロン区切りのディレクティブ群を持ちます。各ディレクティブは「ディレクティブ名+ソースリスト」の形です。
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
img-src 'self' data:;
base-uri 'none';
object-src 'none'
ブラウザはリソースを読み込む/スクリプトを実行する直前に、その操作に対応するディレクティブを引きます。たとえば外部スクリプトの取得なら script-src、画像なら img-src です。該当ディレクティブが無い場合にだけ default-src へフォールバックします。default-src 自体も無ければ、そのリソース型は無制限になります。ここが重要で、script-src を明示した瞬間、script-src には default-src の値は一切引き継がれません。継承ではなく「個別指定があれば default を見ない」という上書き規則です。
script-src-elem や script-src-attr のような細分ディレクティブは、未指定なら script-src へ、それも無ければ default-src へと段階的にフォールバックします。一方 base-uri と form-action は default-src にフォールバックしません。default-src 'self' だけ書いて安心していると、base-uri は無制限のまま残ります。
ソースリストの照合アルゴリズム
各操作について、ブラウザは「対象 URL や実行形態が、ソースリストのいずれか1つにマッチするか」を判定します。1つでもマッチすれば許可、どれにもマッチしなければ拒否し、report-uri/report-to が設定されていれば違反レポートを送ります。複数の CSP ヘッダが存在する場合は全ポリシーの積(AND) で、最も厳しい組み合わせが残ります。後から緩いポリシーを足しても上書きで緩むことはありません。
照合の主なキーワードは次のとおりです。
| ソース表現 | 意味 | 注意点 |
|---|---|---|
| 'self' | ページと同一オリジン | scheme/host/port 一致。サブドメインは含まない |
| https://cdn.example.com | そのホストからの読み込み | そのCDN上に攻撃可能なJSがあると穴になる |
| 'unsafe-inline' | インラインscript/styleを許可 | nonce/hash があると無視される(後述) |
| 'unsafe-eval' | eval/new Function 等の文字列→コードを許可 | 付けると最大の緩和ポイントになる |
| 'nonce-xxxx' | 一致するnonce属性を持つ要素だけ許可 | 推測不能な値を毎レスポンス再生成する |
| 'sha256-...' | 本文ハッシュが一致するインラインだけ許可 | 1文字でも変わると不一致 |
インラインスクリプト(<script>...</script> や onclick= 属性)と eval 系は、ホスト名で許可できません。これらは既定で禁止され、許すには 'unsafe-inline' / 'unsafe-eval'、または nonce/hash の明示が要ります。これが CSP の XSS 緩和の核です。攻撃者が注入する典型的な <script>alert(1)</script> は、インラインなので nonce も hash も持たず、'unsafe-inline' が無ければ実行されません。
nonce・hash・strict-dynamic の仕組み
ホスト allowlist 方式(script-src 'self' https://cdn...)は壊れやすいことが知られています。許可した CDN 上に JSONP エンドポイントや古い Angular のようなライブラリ(いわゆる gadget)が1つでもあると、https://cdn.example.com/jsonp?callback=alert(1) のような形で許可ホスト経由の任意実行が通ってしまうからです。
そこで現代の推奨は nonce ベースの strict CSP です。
Content-Security-Policy:
script-src 'nonce-r4nd0m' 'strict-dynamic';
object-src 'none';
base-uri 'none'
<!-- このnonceを持つscriptだけ実行が許される -->
<script nonce="r4nd0m" src="/app.js"></script>
- nonce: サーバーがレスポンスごとにランダムな値を生成し、ヘッダと正規の
<script>の両方に載せます。攻撃者は注入時点でこの値を知り得ないため、注入スクリプトに正しい nonce を付けられません。値が推測・固定されると即破綻するので、リクエスト単位での再生成が必須です。 - hash: スクリプト本文の SHA-256/384/512 をヘッダに列挙し、本文が完全一致するインラインだけ許可します。内容が静的なインラインに向きます。
- strict-dynamic: nonce/hash で信頼されたスクリプトが
document.createElement('script')などで動的に生成した子スクリプトへ、信頼を伝播させます。同時に、ホスト allowlist('self'やhttps://...)を無効化します。これによりローダーが次々読み込む正規スクリプト群を、個々に nonce を振らずに通しつつ、ホストベースの抜け穴を閉じられます。
nonce か hash が script-src に存在すると、nonce 対応ブラウザは 'unsafe-inline' を無視します。そこで script-src 'nonce-...' 'unsafe-inline' と並べておくと、nonce 非対応の古いブラウザ(CSP Level 1 世代)では 'unsafe-inline' で最低限動き、nonce 対応ブラウザでは nonce が優先されて厳格に守られます。strict-dynamic を足すなら同様に https: を保険として併記します(strict-dynamic 非対応ブラウザではホスト許可が効き、対応ブラウザでは無視される)。
評価の順序と DOM 構築のタイミング
CSP の判定は「ディレクティブの並び順」ではなく「操作の種類」で引かれます。script-src を2回書けば後勝ちではなく、同一ヘッダ内の重複ディレクティブは最初の出現が有効で以降は無視されます。一方、別ヘッダで重ねた場合は前述のとおり全ポリシーの AND です。
実行時点も重要です。HTML パーサが <script> 要素を構築した瞬間に nonce 属性を読み取って判定するため、後から JavaScript で el.setAttribute('nonce', ...) を付けても遅く、効きません。さらに最近のブラウザは、パース後に DOM から読める nonce 属性値を隠蔽(.nonce プロパティ経由でしか取れず属性は空に見える)します。これは、注入された別要素から正規 nonce を盗み読みして再利用する攻撃を塞ぐためです。同一オリジン内のデータ保護という発想は 同一オリジンポリシーとサイト分離の信頼境界 と地続きです。
残るバイパス経路
strict CSP でも、設定漏れや設計ギャップで回避は起こります。代表例を押さえます。
| バイパス | 原理 | 対策 |
|---|---|---|
| base-uri 注入 | <base>タグ注入で相対URLのscriptを攻撃者ホストへ向ける | base-uri 'none' または 'self' を必ず指定 |
| dangling markup | 未閉じの属性で後続HTMLを取り込み、nonceごとデータを外部送信 | img-src等も絞る/注入自体を出さない |
| JSONP / gadget | 許可ホスト上のcallback付きAPIで任意JS実行 | strict-dynamic でホスト許可を捨てる |
| policy injection | ページ生成時の値にCSP自体へ任意文字列を混入 | ヘッダ生成箇所を入力から分離 |
| unsafe-eval 経由 | テンプレートエンジンが内部でnew Functionを使う | eval非依存ビルド/'unsafe-eval'を外す |
とりわけ base-uri の見落としは致命的です。script-src をいくら厳格にしても、攻撃者が <base href="https://evil.example/"> を注入できれば、ページ内の相対パス <script src="app.js"> が https://evil.example/app.js を指すようになり、しかもこのスクリプトは正規の nonce を持つため実行されます。だから strict CSP では base-uri 'none' がほぼ必須です。同様に object-src 'none' は Flash/プラグイン経由の実行面を塞ぐ定番です。
nonce ベースであっても、dangling markup や許可ホスト上の gadget、信頼するインライン event handler の許可ミスで回避され得ます。CSP は「XSS が出たときの被害を一段下げる保険」であり、根本対策は注入を発生させない出力エスケープと安全な DOM API(textContent/Trusted Types)です。Trusted Types を require-trusted-types-for 'script' で強制すると、危険なシンク(innerHTML など)への文字列代入を型レベルで遮断でき、CSP と相補的に効きます。
導入の実務 — Report-Only から始める
いきなり強制すると正規スクリプトまで止まり、ページが壊れます。定石は Content-Security-Policy-Report-Only でブロックせずレポートだけ集める段階を挟むことです。違反レポートで漏れている正規リソースを洗い出し、nonce 化やホスト追加を済ませてから本番の Content-Security-Policy へ切り替えます。認証フローやセッション Cookie を扱うページでは、CSP と Cookie とセッション の HttpOnly/SameSite、そして Web 認証の仕組み の対策を重ねて初めて XSS とセッション奪取の両面を抑えられます。CSP 単体で完結させようとしないのが、結局いちばん堅い設計です。
Web/フロントエンド Article
Content Security Policyの内部動作と回避耐性を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
セキュリティ
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
allowlist 方式は CDN やJSONP の存在で穴だらけになりやすい。nonce/hash でインラインを個別許可し、strict-dynamic で動的生成スクリプトに信頼を伝播させる「strict CSP」が現代の本命。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「セキュリティ / Web」に近いか確認する。
- 強みである「CSP はレスポンスヘッダで宣言する許可リスト。ブラウザがリソース読み込み・スクリプト実行のたびに、対応ディレクティブをソースリストへ照合し、不一致なら拒否してレポートする。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。