CSP とは何か — XSS が起きても「実行させない」

Webアプリケーションのセキュリティ対策というと、まず「入力をサニタイズする」「出力をエスケープする」が思い浮かびます。 これらは正しいのですが、現実のアプリは依存ライブラリ・サードパーティタグ・テンプレートが複雑に絡み合い、 どこか一箇所のエスケープ漏れで XSS(クロスサイトスクリプティング)が成立してしまいます。

Content Security Policy(CSP)は、その「最後の砦」として機能する仕組みです。 サーバが HTTP レスポンスヘッダで「このページでは、どこから来たスクリプト・画像・スタイルなら実行・読み込みしてよいか」を ブラウザに宣言します。ブラウザは宣言に違反するリソースを問答無用でブロックします。

ポイントは、CSP は XSS の発生そのものを防ぐ仕組みではないということです。 攻撃者が <script>alert(document.cookie)</script> を注入することに成功しても、 そのスクリプトが CSP の許可リストに載っていなければ実行されません。 つまり CSP は「攻撃の成立」と「被害の発生」の間に挟む一枚の壁であり、多層防御(Defense in Depth)の一層です。

sequenceDiagram
    participant A as 攻撃者
    participant S as サーバ
    participant B as ブラウザ
    A->>S: 悪意あるスクリプトを注入<br/>(コメント欄など)
    S->>B: HTML + CSPヘッダを返却<br/>script-src 'self'
    Note over B: 注入された inline script を検出
    B->>B: CSPポリシーと照合
    B--xB: ポリシー違反 → 実行をブロック
    B->>S: 違反レポートを送信 (任意)
    Note over B: ページは表示されるが<br/>攻撃コードは動かない

ディレクティブの読み方 — ポリシーは「指示」の集合

CSP は Content-Security-Policy ヘッダの値として、セミコロン区切りのディレクティブの集合で表現します。 各ディレクティブは「リソースの種類」と「許可するソース」のペアです。

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'self';
  object-src 'none'

上の例を日本語に訳すと「デフォルトは同一オリジンのみ許可。スクリプトは自分自身と指定CDN。 スタイルはインラインも許可。画像は data URI と HTTPS全般。API通信は自オリジンと指定API。 iframe 埋め込みは全面禁止。<base> は自オリジンのみ。プラグインオブジェクトは禁止」となります。

押さえるべき主要ディレクティブ

ディレクティブ 制御対象 実務での重要度・補足
default-src すべての fetch 系のフォールバック 最初に設定する土台。個別ディレクティブが未指定の時に適用される
script-src JavaScript の実行元 ★最重要。XSS対策の核。nonce / hash / strict-dynamic を使う場所
style-src CSS の適用元 inline style を使う設計だと unsafe-inline が必要になりがち
img-src 画像の読み込み元 data: スキームの扱いに注意
connect-src fetch / XHR / WebSocket の接続先 API・分析ツールの通信先をここで制御
frame-ancestors 自ページを埋め込める親 clickjacking 対策。X-Frame-Options の後継
base-uri タグで設定可能なURL 'self' か 'none' に固定するのが鉄則(nonce迂回を防ぐ)
object-src / プラグイン 'none' 固定が推奨。古い攻撃面を塞ぐ

allowlist 方式の限界 — なぜホスト列挙では守れないのか

CSP を初めて導入する多くの人は、まず「信頼できるドメインを列挙する」方式(allowlist / ホストベース)を取ります。 script-src 'self' https://cdn.example.com https://www.google-analytics.com ... のように、 使っているCDNやタグのドメインを並べていくスタイルです。直感的でわかりやすいのですが、 これがセキュリティとしてはほとんど機能しないことが知られています。

Google の研究者が約160万のホストの CSP を調査した結果、allowlist ベースのポリシーの大多数が実質的にバイパス可能でした。 主な抜け穴は次の3つです。

① JSONP エンドポイント経由のバイパス

許可したドメインに JSONP エンドポイント(任意のコールバック関数名を URL に埋め込めるAPI)があると、 攻撃者はそれを使って任意のコードを実行できます。たとえば許可リストに入っている人気CDNが ?callback=alert(document.domain)// のような呼び出しを許してしまうと、CSP を回避されます。

<!-- script-src に許可済みドメインだが…JSONP が抜け穴になる -->
<script src="https://trusted-cdn.example/api/jsonp?callback=alert(document.cookie)//"></script>
<!-- ブラウザから見れば「許可されたドメインのスクリプト」なので実行されてしまう -->

② 危険なライブラリ・AngularJS 等のガジェット

許可ドメインに AngularJS(特に古いバージョン)や、文字列を eval 相当で評価するライブラリがホストされていると、 それらの「ガジェット」を組み合わせて任意コード実行に至るケースがあります。 自分が直接書いていないコードでも、許可ドメインにある以上 CSP は信頼してしまうのが本質的な弱点です。

③ 運用負荷とリストの肥大化

サードパーティタグが増えるたびにドメインを追加し、リストはどんどん長くなります。 一つ一つのドメインが「本当に安全か」を検証し続けるのは現実的に不可能で、 結果として https: や広範なワイルドカードで妥協 → 事実上ザル、という末路を辿りがちです。

Strict CSP — nonce と strict-dynamic で組む

Google が web.dev や csp.withgoogle.com で推奨し、OWASP CSP Cheat Sheet も第一選択として挙げているのが Strict CSP(nonce ベース)です。発想を「どのドメインを信じるか」から 「自分が出力したスクリプトだけを信じる」へ転換します。

nonce — レスポンスごとの使い捨てトークン

nonce(number used once)は、サーバが HTTP レスポンスを返すたびに生成する ランダムな使い捨て文字列です。CSPヘッダに 'nonce-xxxx' として埋め込み、 同じ値を信頼したい <script> タグの nonce 属性に付けます。 ブラウザは「nonce が一致するスクリプトだけ」を実行します。

# サーバがレスポンスごとに生成(推測不能なランダム値・base64で16バイト以上)
Content-Security-Policy:
  script-src 'nonce-r4nd0mB4se64Value' 'strict-dynamic';
  object-src 'none';
  base-uri 'none'
<!-- nonce が一致する → 実行される -->
<script nonce="r4nd0mB4se64Value">
  initApp();
</script>

<!-- 攻撃者が注入したスクリプトには正しい nonce を付けられない → ブロック -->
<script>alert(document.cookie)</script>

攻撃者は「次のレスポンスで使われる nonce 値」を事前に知ることができないため、 注入したスクリプトに正しい nonce を付けられません。これが nonce 方式の強さです。

strict-dynamic — 信頼を「伝播」させる

現代のアプリでは、最初に読み込んだスクリプトがさらに別のスクリプトを動的に挿入します (バンドラのチャンク読み込み、タグマネージャ等)。それら一つ一つに nonce を付けるのは非現実的です。

そこで strict-dynamic を使います。これは 「nonce または hash で信頼されたスクリプトが document.createElement('script') 等で生成した 子スクリプトも、自動的に信頼してよい」とブラウザに伝えるキーワードです。 信頼が連鎖的に伝播するため、ホスト名の列挙が一切不要になります。

# モダンブラウザ向け Strict CSP の定番形
Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'none'
観点 allowlist 方式 Strict CSP(nonce + strict-dynamic)
信頼の基準 許可ドメインのリスト 自分が出力した nonce/hash
JSONPバイパス 脆弱(抜け穴になる) 無効化される(強い)
運用コスト タグ追加のたびに更新 サードパーティ追加でも基本不変
CDN/チャンク読込 ドメイン列挙が必要 strict-dynamic で自動許可
静的サイトでの利用 容易 hash 方式なら可(nonceは動的生成前提)
推奨度(2026) 非推奨 第一選択(OWASP/Google推奨)

壊さず導入する — Report-Only と Reporting API

CSP の怖いところは、ポリシーが厳しすぎると正規のスクリプトまでブロックしてサイトが壊れることです。 いきなり本番に強制適用するのは危険なので、まず「違反を記録するだけで実際にはブロックしない」モードで観測します。

Content-Security-Policy-Report-Only

Content-Security-Policy の代わりに Content-Security-Policy-Report-Only ヘッダを使うと、 ポリシー違反はレポートとして送信されるだけで、リソースは通常どおり実行されます。 本番トラフィックで「もし強制したら何が壊れるか」を安全に可視化できます。

# まずは観測モードで導入。ブロックせず違反だけ集める
Content-Security-Policy-Report-Only:
  script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';
  report-to csp-endpoint

report-uri は非推奨、report-to / Reporting API へ

違反レポートの送信先指定には、長年 report-uri ディレクティブが使われてきましたが、 これは非推奨となり、後継の report-to ディレクティブ + Reporting API に置き換わりました。 report-toReporting-Endpoints ヘッダで定義した名前付きエンドポイントを参照します。

# Reporting-Endpoints ヘッダでエンドポイント名を定義
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"

# CSP 側はその名前を report-to で参照
Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  report-to csp-endpoint

推奨される段階導入フロー

graph LR
    A["Report-Only で<br/>緩いポリシー適用"] --> B["違反レポートを<br/>収集・分析"]
    B --> C["正規リソースを<br/>nonce対応に修正"]
    C --> D["Report-Only で<br/>ポリシーを厳格化"]
    D --> E{"違反が<br/>ほぼゼロ?"}
    E -->|No| B
    E -->|Yes| F["Enforce モードへ<br/>(本番強制)"]

CSP の進化 — 仕様の歴史

CSP Level 1(W3C勧告へ)

script-src / style-src などの基本ディレクティブが標準化。allowlist 方式の時代。

CSP Level 2

nonce・hash・child-src・frame-ancestors などを追加。inline script を安全に許可する道が開けた。

strict-dynamic 提案 / Strict CSP 普及の起点

Google が allowlist の脆弱性を大規模調査で示し、nonce + strict-dynamic ベースの「Strict CSP」を提唱。

Reporting API への移行

report-uri が非推奨化され、report-to + Reporting API へ。違反監視の仕組みが汎用化。

Strict CSP が事実上の標準

strict-dynamic は主要モダンブラウザで広くサポート。nonce ベースの Strict CSP が推奨アプローチとして定着。

実務でハマりやすいポイント

CSP は「一度書いて終わり」ではなく、Report-Only での観測 → 修正 → 厳格化を繰り返す運用です。 完璧な一発を狙うより、まず object-src 'none'; base-uri 'none' という壊れにくい部分から enforce し、 script-src は Report-Only で育てていく——この二段構えが、現実のプロダクトで挫折しないコツです。

まとめ

Content Security Policy は、XSS が万一成立しても「実行させない」ことで被害を断つ多層防御の一層です。 従来のホスト列挙(allowlist)方式は JSONP やライブラリのガジェットで容易にバイパスされ、運用コストも高いため、 2026年現在は nonce + strict-dynamic による Strict CSP が第一選択です。 本番を壊さないために Content-Security-Policy-Report-OnlyReporting API(report-to)で 段階導入し、base-uri / object-src の固定など壊れにくい部分から着実に固めていきましょう。

理解度チェック

問題 0 / 70%
Q1

CSP の役割を最も正確に説明しているものはどれですか?

キーボード: 1〜4 で選択、Enter で回答