生成した nonce の使い方
CSP nonce のプリセットが有効になっています。ランダムな 16 バイトを Base64 にしたもので、
==で終わる 24 文字の値になります。すぐ貼り付けられる形式 に、ヘッダーとそれに対応するタグが次のように表示されます。
Content-Security-Policy: script-src 'nonce-SQu4/GjzG3plY6CUwsx8gA==' <script nonce="SQu4/GjzG3plY6CUwsx8gA==" src="/app.js"></script>ローカルのテストページやステージング環境の設定でポリシーを試すのに使ってください。別の値が欲しければ 再生成 を押します。
このページからコピーした nonce はどのリクエストでも同じなので、本番環境では何も守りません。実際の運用では、下に示すようにサーバー側のコードで生成します。
nonce ベースのポリシーの仕組み
script-src 'nonce-…' を指定すると、ブラウザは、スクリプトの nonce 属性がそのレスポンス自身のヘッダーにある値と一致する場合にだけ、そのスクリプトを実行します。自分の <script> タグには、テンプレートから属性を付けます。XSS の脆弱性を突いて攻撃者が注入したマークアップはその値を知りません。ページを読み込むたびに値が変わるからです。そのため、ブラウザは実行を拒否します。
実際には、次の 3 点が重要です。
- 値はテキストとして比較されます。
=も含めて正確にコピーし、ヘッダーでは'nonce-…'を囲む一重引用符を残し、属性では付けないでください。 - ブラウザは属性を隠します。 ページが読み込まれると
getAttribute("nonce")は空文字列を返し、CSS セレクターからも値は見えなくなるため、一部の情報窃取の手口を防げます。値が必要なスクリプトは、代わりにnonceプロパティを読みます。 'strict-dynamic'を使うと、nonce の付いた信頼済みのスクリプトが、個別の nonce なしでさらにスクリプトを読み込めます。現代的なポリシーでは nonce とよく組み合わせて使われ、これを指定するとブラウザはhttps:のようなホストの許可リストを無視します。
ポリシーは <meta http-equiv="Content-Security-Policy"> タグで配信することもできます。そこでも nonce は機能しますが、frame-ancestors など一部のディレクティブは使えません。
レスポンスごとに nonce を生成する
サーバーはリクエストごとにランダムな 16 バイトを作り、それをヘッダーに入れ、同じ値をテンプレートに渡します。フレームワークなしの Node.js では次のようになります。
const crypto = require("crypto");
const http = require("http");
http.createServer((req, res) => {
const nonce = crypto.randomBytes(16).toString("base64");
res.setHeader("Content-Security-Policy",
`script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'`);
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(`<script nonce="${nonce}" src="/app.js"></script>`);
}).listen(8080);
Python なら、base64.b64encode(secrets.token_bytes(16)).decode() で同じ種類の値が得られます。CSP のミドルウェアを持つフレームワークは、たいていこれをテンプレート変数として提供しています。何を使うにせよ、サーバーの前段にあるキャッシュが、ある nonce を含む HTML を保存してすべての訪問者に返していないか確かめてください。ヘッダーは変わるのにページ本文がキャッシュされると、すべてのスクリプトが動かなくなります。両方がキャッシュされると、全員に同じ nonce が繰り返されます。
nonce、ハッシュ、SRI
nonce は 1 つのレスポンスに対してスクリプトタグを承認します。CSP のハッシュは特定のインラインスクリプトを恒久的に承認します。'sha256-…' は、<script> と </script> の間の正確なテキストの SHA-256 を Base64 にしたものです。<script>alert('Hello');</script> なら次のようになります。
script-src 'sha256-gKHd+pSZOJ3MwBsFalomyNobAcinjJ44ArqbIKlcniQ='
ハッシュは、静的サイトや変更されないインラインのスニペットに向いています。SHA-256 から Base64 へ ツールで計算できますが、空白に注意してください。空白が 1 つ増えるだけでハッシュは変わります。サブリソース完全性(SRI)は、同じ Base64 のダイジェストを使う 3 つ目の仕組みです。<script src> に integrity="sha384-…" を付けると、ブラウザはダウンロードしたファイルそのものを検査します。そうしたタグは SRI ハッシュ生成ツール で作れます。
よくある質問
CSP の nonce はどのくらいの長さにすべきですか?
CSP の仕様では、エンコード前に 128 ビット以上のランダム性を求めています。これは 16 バイト、Base64 で 24 文字です。長い分には問題ありませんが、短いと保護が弱まります。
すべてのページで同じ nonce を使い回せますか?
できません。固定の nonce は HTML に見えているので、マークアップを注入できる攻撃者はそれを自分の <script> タグにコピーできます。HTTP レスポンスごとに新しく生成してください。
静的サイトで CSP の nonce を使えますか?
新しい nonce を挿入するエッジ関数など、前段で毎回レスポンスを書き換える仕組みがある場合に限ります。純粋な静的ホスティングは全員に同じバイト列を返すので、代わりにスクリプトのハッシュ('sha256-…')か 'self' の外部ファイルを使ってください。
nonce に URL セーフな文字を使ってもよいですか?
はい。CSP の文法は Base64 の両方のアルファベットを受け付けます。英字、数字、+、/、-、_ に加え、末尾に最大 2 つの = を使えます。標準の Base64 を使うのが一般的です。