セキュリティ診断のレポートに「Content-Security-Policy ヘッダーが未設定」という指摘が並ぶことがあります。ヘッダーを足し、再診断で指摘が消える。報告書の該当欄は「対応済み」になります。
ここで判断が止まりやすくなります。何が「済」になったのかが、レポートの文面からは読み取れないためです。
最初は、CSP(Content Security Policy/ブラウザーに「どこから来たコードを実行してよいか」を宣言する仕組み)を入れればXSS(クロスサイトスクリプティング/攻撃者が仕込んだスクリプトを利用者のブラウザーで実行させる攻撃)は防げると考えていました。実行元を制限するのだから、攻撃者のスクリプトは動かない——筋は通っています。しかし仕様の本文を読むと、置かれている言葉が違いました。仕様は「防ぐ」ではなく「緩和する」と書き、「多層防御として使うのが最良である」と続けます。
本記事が立てる仮説はこれです——CSPの設計判断でつまずくのは、設定項目を覚えていないからではなく、評価軸を「防げている / 防げていない」の二値で持っているからではないか。仕様が示しているのは二値ではなく、被害の上限です。
この仮説が当たっているなら、起きる失敗は具体的です。許可リストにドメインを足し続け、タグやSaaSが増えるたびに行が伸び、誰も全体を説明できなくなる。そして「対策済み」という一行だけが引き継がれます。
本記事は、CSPの設定を決める前に何を決めるかに絞ります。個別サイトに適合するヘッダーの設定例は扱いません。外部スクリプトの棚卸し手順はサードパーティスクリプトの棚卸しは、デプロイの棚卸しではないに、nonce(1回限りの使い捨ての値)の受け渡しを含む配信側の実装はJavaScript・CSSの最適化に預けます。
30秒で要点
- 策定中の Content Security Policy Level 3(W3C Working Draft、文書の日付2026年9月16日)は、CSPをコンテンツ注入のリスクを「緩和する(mitigating)」仕組みと位置づけ、「多層防御として使うのが最良である」「悪意ある注入が引き起こす被害を減らす」と書いている
- したがって評価軸は「防げている / 防げていない」ではなく、被害の上限がどこまで下がったかになる
- 許可リストは信頼の単位を「ドメインという場所」に置く。ハッシュ指定は信頼の単位を「ファイルの中身」に移す
'strict-dynamic'を書くと、許可リストや'self'・'unsafe-inline'のような指定は無視される。設定は足し算では読めない- 同じソースリストにnonceまたはハッシュの指定があると、
'unsafe-inline'を書いてもそのリストは「すべてのインラインを許可する」とは判定されない
なぜ「CSPを設定すればXSS対策済み」と読めてしまうのか
この誤読は、知識不足からではなく2つの構造から生まれます。
第一に、ヘッダーの有無は判定しやすく、被害の上限は判定しにくいという非対称があります。Content-Security-Policy が送られているかどうかは、応答を1回見れば分かります。一方、注入が起きたときに何がどこまで実行されうるかは、ページに載っているスクリプトの経路を辿らないと分かりません。チェックリストの上では、前者は「済」と書けて後者は書けません。済と書ける項目は、書けない項目を代表してしまうことがあります。
第二に、設定が足し算に見えるという事情があります。許可したいドメインを1行足す、インラインを使いたいので 'unsafe-inline' を足す、新しいSaaSを導入したのでまた1行足す。追加した指定はすべて効いている、と読むのが自然です。しかし後述のとおり、CSPには書いた指定が無効になる条件があります。足し算の感覚のままだと、この挙動は読めません。
この2つが重なると、「行は増えているのに何が守られているか説明できない」という状態になります。そして棚卸しの動機も生まれません。指摘は消えているからです。
仕様自身が「緩和」「多層防御」「被害の縮小」と書いている
参照するのは、W3C が公開している Content Security Policy Level 3 です。文書のステータスは Working Draft、日付は2026年9月16日、版識別子は WD-CSP3-20260916 でした(2026年10月20日に取得して確認)。
ここは先に断っておく必要があります。Working Draft は勧告(Recommendation)ではありません。本記事が書けるのは「策定中の仕様がこう書いている」までで、「W3Cの標準がこう定めている」ではありません。内容は今後変わりえます。
その導入部(§1)に置かれている言葉は3つです。
- クロスサイトスクリプティングのようなコンテンツ注入脆弱性のリスクを mitigating(緩和する) ための仕組みである
- CSP is best used as defense-in-depth.(多層防御として使うのが最良である)
- It reduces the harm that a malicious injection can cause(悪意ある注入が引き起こしうる被害を減らす)
3つとも、被害の大きさについての記述です。「注入を発生させない」ではなく、「注入が起きたときに何ができるかを狭める」。仕様が自分の役割をそう書いています。
ここで方向を間違えないようにします。仕様は「CSPでは防げない」とも書いていません。 書かれているのは緩和・多層防御・被害の縮小です。「意味がない」という読み替えは、記述から出てきません。退けられるのは両端——「入れたから済んだ」と「どうせ防げないから後回し」——のほうです。
同じ構造は、他の「設定すれば済むように見える機能」にも現れます。SameSite=LaxはCSRF対策になるのかとCORSは共有を許す仕組みであって遮断する仕組みではないで扱った論点と、本記事は並びます。いずれも、機能が実際に決めている範囲と、対策として期待される範囲がずれているという同じ形です。
信頼の単位は「場所」にも「中身」にも置ける
被害の上限を決めているのは、結局のところ「何を信頼の起点に置いたか」です。CSPはその起点の置き方を複数用意しています。
許可リストは、ドメインという場所を信頼する
script-src にドメインを並べる書き方は、そのホストから来たものは信頼するという宣言です。MDN の script-src の解説は、この方式を a broad-brushed approach(大まかな筆致の方法)と呼び、コードを安全に読み込める場所を指定するやり方だと説明しています。
重要なのは、MDN がこれを否定していない点です。多くのリソースを使っていて、かつ信頼先が侵害されないと確信できる場合には実務的な方法だ、という条件付きの評価になっています。条件が付いているのは、信頼の単位がファイルではなくホスト全体だからです。そのホストに置かれた別のファイルも、同じ宣言の対象に入ります。
ハッシュ指定は、ファイルの中身を信頼する
ハッシュ指定は、起点を場所から中身へ移します。MDN はハッシュのほうがドメインを信頼するより安全だとし、理由をこう書いています——ファイルが改変されていなければ、侵害されたサイトから読み込まれても使われるから。
同じページは、インラインを許可する方法が3つあることも示しています。'unsafe-inline'、nonce の指定、そして該当ブロックに一致するハッシュの指定です。そのうえで、すべてのインラインスクリプトを許可することはセキュリティリスクと見なされるため、nonce かハッシュを使うことを推奨するとしています。
この2つを並べると、選択肢は「厳しい設定か緩い設定か」ではないと分かります。どちらも信頼の宣言であり、違うのは単位です。 場所で信頼するか、中身で信頼するか。許可リストが膨らんで管理できないという症状は、単位が場所のままであることの帰結です。
書いた指定が無効になる条件が2つある
ここが、足し算の感覚でもっともつまずく部分です。
nonce かハッシュがあると、'unsafe-inline' は効かない
仕様の §6.7.3.2「Does a source list allow all inline behavior for type?」は、あるソースリストが「すべてのインラインを許可する」と判定される条件を定めています。冒頭は、'unsafe-inline' を含み、かつ続く手順でそれが上書きされない場合に許可する、と書かれています。
その手順の中に、こうあります——式が nonce-source または hash-source の文法に一致したら "Does Not Allow" を返す。つまりnonce かハッシュの指定に到達した時点で、そのリストは「すべて許可」ではなくなります。同節の例でも、'unsafe-inline' と 'nonce-abc' を含むリストは「すべてのインラインを許可しない」側に分類されています。
これは 'strict-dynamic' を使ったときだけの話ではなく、インライン許可の判定アルゴリズムそのものの規定です。ただし範囲は限られます。これは同じソースリストの中での関係です。「どこかに nonce を書けば、別の場所の 'unsafe-inline' が必ず効かなくなる」とは一般化できません。
'strict-dynamic' を書くと、許可リストが無視される
MDN は 'strict-dynamic' をこう説明しています。markup 上のスクリプトに nonce かハッシュを添えて明示的に与えた信頼が、その root script が読み込むすべてのスクリプトへ伝播する。そして同時に、許可リストや 'self'・'unsafe-inline' のような指定は無視される。
信頼の単位の移動を徹底する指定だと読めます。場所による宣言をやめ、「最初に自分が信頼したスクリプト」を起点にして、そこから先は連鎖で許す。だから場所の宣言は要らなくなり、無視されます。
実務的な落とし穴はここです。許可リストを維持したまま 'strict-dynamic' を足すと、許可リストの行は残っているのに効いていない状態になります。設定ファイルを読んだ人は、残っている行を根拠に「このドメインは許可されている」と判断します。差分レビューでも、足した行は見えますが、足したことで無効になった行は見えません。
なお、仕様には 'strict-dynamic' の詳細を扱う節(§8.2)がありますが、本記事の確認範囲には含まれていません。'strict-dynamic' の挙動について本記事が書いたのは、MDN の記述と仕様 §1.3(Level 2 からの変更点)の範囲までです。
確かなことと、まだ確かめきれていないこと
確かなこと:上記の引用は、2026年10月20日に CSP Level 3 の原文と MDN の script-src ページを取得して該当箇所を読んだものです。導入部の3つの記述、文書のステータスと日付、インライン許可判定の手順、'strict-dynamic' の2つの効果、ハッシュと許可リストについての評価は、いずれも原文にあります。
まだ確かめきれていないこと:参照した文書は草案であり、勧告ではありません。また本記事が原文を読んだのは §1・§1.3・§6.7.3.2 と MDN の該当ページであり、'strict-dynamic' の用法を扱う §8.2 は読んでいません。
さらに、本記事の情報源からは書けないことがいくつかあります。違反レポートの実例、レポート収集の挙動、導入率の統計は、いずれも上記2つの資料に含まれていません。「ホストの許可リストは迂回されやすい」という趣旨の記述も、どちらの資料にもありません。 書けるのは「信頼の単位がファイルではなくホスト全体になる」という、原文から導ける範囲までです。
判断軸:二値で評価せず、被害の上限で評価する
ここまでの整理から導かれる判断軸は1行です——CSPを「対策済みかどうか」で評価せず、「注入が起きたとき何がどこまで実行されうるか」で評価する。
この軸を持つと、優先順位が決まります。許可リストの行を1本減らすことと、インラインスクリプトを nonce かハッシュに移すことは、作業量としては似ていても、被害の上限に与える影響が違います。後者は信頼の単位を動かし、前者は単位を動かさずに範囲を狭めるだけです。どちらを先にやるかは、いま単位がどこにあるかで決まります。
同時に、この軸は2つの極を退けます。
- 「CSPを入れたからXSS対策は完了」——仕様が書いているのは緩和・多層防御・被害の縮小です。多層防御の一枚として最良、という書き方は、他の層が別に要ることを前提にしています
- 「CSPでは防げないから意味がない」——仕様は多層防御の一枚として位置づけています。一枚としての価値は記述に含まれています
方針を決めるまでの順序と、数えておく1つの比
設定を書き換える前に踏める手順があります。本記事の射程は、この4段階の最後——方針を文章にするところまでです。
- 観測する。 いきなり強制せず、効果を監視するだけのヘッダー(
Content-Security-Policy-Report-Only)で現状を取る。仕様の §3.2 は、このヘッダーを「効果を強制せずに監視する」ものと定義し、サイトの挙動の見積もりで report-only のポリシーを配り、違反レポートを見て、挙動に確信が持ててから強制へ移る、という反復的な進め方を示しています。ここでの目的は「止めること」ではなく「何が載っているかを知ること」です - 違反を棚卸しする。 どのドメインから、どのインラインが、どの画面で動いているかを一覧にする
- 信頼の起点を決める。 場所(ドメイン)で信頼するものと、中身(ハッシュ・nonce)で信頼するものを分ける。ここが設計判断の本体です
- 方針として書く。 「なぜこのドメインを許可しているか」を1行ずつ書けるかを確認する
棚卸しのときに、1つだけ比を取っておくと判断が具体的になります。
- 分母:ページに載っているスクリプトの実行点の総数(外部ファイル+インラインブロック)。単位は件
- 分子:そのうち、中身(ハッシュまたは nonce)で信頼していると言えるものの件数
この比が小さいほど、信頼の重心は場所に寄っています。数字を目標にする必要はありません。次に手を入れる場所を決めるための現在地として置きます。
手を動かす前にできる最小の確認もあります。いま出している Content-Security-Policy ヘッダーを1本取り出し、'strict-dynamic' が含まれているかを見る。 含まれていて、同じ指定の中にドメインの許可リストも並んでいるなら、その許可リストは効いていません。行は残っているのに効いていない——という状態が手元にあるかどうかは、これだけで分かります。
外部から読み込んでいるものの実態把握はサードパーティスクリプトの棚卸しは、デプロイの棚卸しではないが扱っています。棚卸しの手順はそちらに、棚卸しの結果をどの単位の信頼に割り当てるかは本記事に、という分担です。埋め込みの境界についてはiframeのサイズ調整は埋め込み先の同意を必要とするも合わせて読めます。
判断に迷ったときに立ち返る問い
設定ファイルを前にしたときに立ち返りたいのは、「この行は何を信頼すると宣言しているのか」という問いです。ドメインを書いた行は場所を信頼しています。ハッシュを書いた行は中身を信頼しています。同じファイルに両方が並んでいるのは矛盾ではありませんが、どちらが起点なのかは決まっているはずです。
もう1つは、「仕様は自分でこの仕組みの限界を書いていないか」という問いです。この例では、書いてありました。導入部の1段落目に、緩和であり多層防御であり被害の縮小であると置かれています。実装者向けの文書が自分で射程を書いているとき、その記述は読み手が持ち出せるもっとも強い根拠になります。
参考資料・引用元
- Content Security Policy Level 3(W3C Working Draft、文書の日付 2026年9月16日、版識別子 WD-CSP3-20260916。2026年10月20日確認)。§1(Introduction)、§1.3(Changes from Level 2)、§3.2(The Content-Security-Policy-Report-Only HTTP Response Header Field)、§6.7.3.2(Does a source list allow all inline behavior for type?)を参照した。勧告として発行された文書ではない。
- MDN Web Docs — Content-Security-Policy: script-src(最終更新 2026年8月12日。2026年10月20日確認)