- ブラウザURL入力今ここ
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
前回は、サーバに届かない経路を見ました。紛れ込んだスクリプトは出所では見分けられません。今回は、サーバが自分の書いたものに印を付け、ブラウザに照合させる仕組みを見ます。
まず結論
CSP の仕様は、CSP を注入に対する最初の防御線ではなく、注入が起きたときの被害を減らす多層防御だと自ら書いています。
読み込み元を許可リストで絞る元の方式は「どこから来たか」で判断するため壊れやすく、いまは応答ごとに作る nonce などで「サーバが自分で書いたスクリプト」に印を付ける Strict CSP が、OWASP で現在の最有力の実践(current leading practice)とされています。
ブラウザが照合するのは印の一致だけです。どのスクリプトが本当に望んだものかは、印を付けるサーバが自分で決めるしかありません。
CSP Level 3 の仕様は、CSP を XSS などの注入に対してどう位置づけているでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
仕様は、CSP を最初の防御線と呼んでいない
CSP は XSS を防ぐ仕組みだと紹介されがちですが、仕様の序文は位置づけを先に断っています。
CSP is not intended as a first line of defense against content injection vulnerabilities. Instead, CSP is best used as defense-in-depth. It reduces the harm that a malicious injection can cause, but it is not a replacement for careful input validation and output encoding.
注入を入口で止めるのは、『セキュリティとAIの仕組み』の「なぜ入力がコードになるのか」で見た、データをコードとして解釈させない処理のほうです。CSP は、それが漏れて注入が起きた後に効く層です。
では何を手がかりに絞るのか。元の方式は許可リストでした。OWASP は The original mechanism for building a CSP involved creating allow-lists と書き、続けて current leading practice is to create a "Strict" CSP としています。
許可リストは、スクリプトをどこから来たかで判断します。仕様 §8.2(非規範)は、この方式の限界をこう書きます。
Host- and path-based policies are tough to get right, especially on sprawling origins like CDNs. ... those lists end up being brittle, awkward, and difficult to implement and maintain.
2016年の論文 "CSP Is Dead, Long Live CSP!" は、検索エンジンのコーパスから集めた 26,011 の異なるポリシーについて bypasses in 94.72% of all distinct policies と報告しました。2016年のこの集合での値で、今の Web 全体の割合ではありません。構造としては、リストに載せたホストが任意のスクリプトを返せる機能を持っていれば、出所が正しくても中身は選べない、ということです。
そこで印を使います。サーバは応答ごとに推測できない値(nonce)を作り、ヘッダと、自分で書いた script の両方に書きます。ブラウザは両者を照合します。
- ・ブラウザが見るのは印の一致だけで、中身の善し悪しは見ない
- ・どの script に印を付けるか(=望んだものはどれか)を決めるのはサーバ
印の無いインライン script は、同じページにあっても動かない
127.0.0.1 に立てた node のサーバで確かめます。応答ごとに crypto.randomBytes(16) で nonce を作り、script-src 'nonce-…'; base-uri 'none'; object-src 'none' を返します。ページには nonce 付きのインライン script(A)と、nonce の無いインライン script(B)を最初から置きました。注入は再現しておらず、B を「紛れ込んだもの」の代わりにしています。
[browser] version 151.0.7922.34[console.error] Executing inline script violates the following Content Security Policy directive 'script-src 'nonce-tGZKeRiBNYHRSdA2tCwhKQ==''. Either the 'unsafe-inline' keyword, a hash ('sha256-r/SEfy8WFK4MYoWZQpZ4BLGoFK8sYR/L5UMIOkdbR7M='), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.[console.error] Failed to load resource: the server responded with a status of 404 (Not Found)[dom] #a: A: ran (has nonce)[dom] #b: B: not run[dom] #v: violation: directive=script-src-elem blockedURI=inline disposition=enforce--- server log[server] GET / nonce=tGZKeRiBNYHRSdA2tCwhKQ==[server] HEAD / nonce=n8xBcqI4u/3Qn1IgcpUPcQ==[server] HEAD / nonce=fTGDY/pqMmpJZq7X0qnRaQ==
2行目がコンソールの違反表示、5行目が B の結果です。A は実行され、B は実行されませんでした。2回走らせて同じでした。3行目の 404 は favicon で、本題とは無関係です。
コンソールは、インラインの実行を許す手段を3つ並べています。'unsafe-inline'、hash、nonce です。'unsafe-inline' はインラインを全部許すので、紛れ込んだものも通ります。hash は中身から計算する値で(ここでは SHA-256)、表示された値はブロックされた B の本文から計算し直して一致しました。
最後の3行は、GET 1回と HEAD 2回で nonce が3つとも違うことを示しています。16バイトは128ビットで、base64 では24文字です。
実演で、nonce の無いインライン script(B)が実行されなかったことから言えるのはどれですか。
印は、推測できないことにしか価値がない
仕様 §7.1 は nonce の作り方を規定しています。
If a server delivers a nonce-source expression as part of a policy, the server MUST generate a unique value each time it transmits a policy. The generated value SHOULD be at least 128 bits long (before encoding), and SHOULD be generated via a cryptographically secure random number generator
攻撃側が作れない値をサーバが要求ごとに配る、という点で CSRF トークンと同じ形です。ただし CSP の nonce はページに書かれます。同じ節の注記はこうです。
An attacker who can gain access to the nonce can execute whatever script they like, whenever they like.
秘密だから安全なのではありません。ページの外からは当てられないことが価値です。
もう1つの弱点は、印を付ける側にあります。OWASP は __Don't__ create a middleware that replaces all script tags with "script nonce=..." because attacker-injected scripts will then get the nonces as well. と警告します。組み上がった HTML の中では、自分が書いた script と紛れ込んだ script は見分けられません。印は、テンプレートがまだ区別を知っている段階で付けるしかありません。
仕様 §8.5(非規範)は、script-src を nonce か hash と 'strict-dynamic' で、base-uri を 'self' か 'none' で書いたものを Strict CSP と呼びます。ただし同じ節に it should be avoided when possible という 'strict-dynamic' への注記もあり、「仕様が strict-dynamic を推奨している」とだけ言うと半分落ちます。
ブラウザ自身も、乗っ取られる前提で作られている
『ブラウザの仕組み』の「なぜプロセスを分けるのか」で見た Chromium の設計も、同じ前提から始まります。
It's also nearly impossible to build a rendering engine that is perfectly secure.
レンダラは外から来たデータを解釈する係なので、乗っ取られる前提で扱われ、サンドボックスに閉じ込められます。その効果について Sandbox FAQ はこう書きます。
The sandbox limits the severity of bugs in code running inside the sandbox.
CSP3 §1 の It reduces the harm that a malicious injection can cause と同じ形です。無くすのではなく、入られた後にできることを減らす。
Chromium のサンドボックスと CSP について、それぞれの一次ソースの記述から言える共通点はどれですか。
CSP の nonce
注入を止めるのはエスケープで、CSP は注入が起きた後に効きます。サーバが自分で書いた script にだけ印を付け、印の無い script はブラウザが実行しません。
Chromium のサンドボックス
完全に安全なレンダラは作れないという前提から、レンダラを乗っ取られても使える資源が限られるように閉じ込めます。公式は効果を「深刻度を下げる」と書きます。
ページの中のスクリプトと、ブラウザの中のプロセス。守る対象は違いますが、共通する設計の前提は何でしょうか。
それぞれの一次ソースが、侵入そのものを防げると書いているかどうかを見てください。
この章のまとめ
- CSP3 §1 は CSP を最初の防御線ではなく多層防御と書き、入力検証・出力エンコードの代わりではないとする。
- 許可リストは元の方式で、出所で判断する。2016年の論文は 26,011 ポリシーの 94.72% が破れると報告した(年とコーパスを添えて引く)。
- nonce は応答ごとに一意(MUST)、128ビット以上(SHOULD)。ページには書かれるので、秘密ではなく外から当てられないことが価値。
- ブラウザは印の一致だけを見る。どれに印を付けるかはサーバが決める。一括で付けると区別が消える。
- Chromium のサンドボックスも、侵入は起きる前提で入った後にできることを絞る。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
「nonce 方式の CSP を入れたので、テンプレートの出力エスケープは外してよい」という提案の扱いとして適切なのはどれですか。
用語集(3語)
- CSP (Content Security Policy)
- どのスクリプトなどを実行・読み込みしてよいかをサーバが応答ヘッダで宣言し、ブラウザに強制させる仕組み。CSP Level 3 は W3C の Working Draft。
- nonce(CSP)
- サーバが応答ごとに作り、ヘッダと自分で書いた script の両方に書く値。CSP3 §7.1 は毎回一意を MUST、128ビット以上を SHOULD とする。OpenID Connect の nonce とは別物。
- Strict CSP
- script-src を nonce か hash と 'strict-dynamic' で、base-uri を 'self' か 'none' で書いた CSP(CSP3 §8.5、非規範)。
出典(一次ソース)
- W3C — Content Security Policy Level 3 (Working Draft, 16 September 2026) §1, §7.1, §8.2, §8.5 ↗
- Weichselbaum, Spagnuolo, Lekies, Janc — CSP Is Dead, Long Live CSP! (CCS'16, DOI 10.1145/2976749.2978363) ↗
- OWASP Cheat Sheet Series — Content Security Policy Cheat Sheet ↗
- Chromium — Multi-process Architecture ↗
- Chromium — Sandbox FAQ ↗