コース目次 / 第2章
CSP を読む — 本物のポリシーを、指令ごとに
このサイトが実際に配信しているCSPを、ディレクティブごとに読み解きます。default-src・script-src(ハッシュ方式)・object-src・frame-ancestors などが、それぞれ何を許し何を禁じているかを掴みます。
このサイトがいま本番で配信している CSP を読みます。scripts/build-headers.mjs がビルド時に生成し、dist/_headers に書いているものです。ハッシュの列は長いので、要点が見えるように省略して示します。
Content-Security-Policy:
default-src 'self';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none';
img-src 'self' data:;
script-src 'self' https://static.cloudflareinsights.com 'sha256-…'(複数);
connect-src 'self' https://cloudflareinsights.com;
style-src 'self' 'sha256-…'(複数);
style-src-attr 'unsafe-inline'
CSP は「このページで、何を・どこから読み込み・実行してよいか」を、資源の種類ごとに宣言するものです。1つずつ読みます。
土台 — default-src ‘self’
default-src は、他の -src 指令が無いときの既定値です。‘self’ は「同一オリジンだけ」。つまり、明示的に緩めない限り、スクリプトも画像も通信先もすべて自分のオリジンに限るのが土台。ここから、必要なものだけを個別の -src で足していきます。「既定は最も厳しく、例外だけ開ける」——認可の deny-by-default と同じ発想です。
芯 — script-src(‘unsafe-inline’ が無い)
このコースの中核です。ここが XSS 対策の要になります。
script-src ‘self’ https://static.cloudflareinsights.com ‘sha256-…’
読み解くと——スクリプトの実行を許すのは、①同一オリジンのファイル(‘self’)、②Cloudflare の計測スクリプト1つ、③特定のインラインスクリプト(ハッシュで名指し)だけ。
決定的に大事なのは——‘unsafe-inline’ が無いことです。だから、攻撃者が注入した <script>…</script> や onerror= のようなインラインは、ハッシュに載っていない限り実行されません。これが「XSS の二枚目の壁」の正体。次章で、実際に止まるところを見ます。
なぜ「ハッシュ方式」なのか、も押さえます。
このサイトが正当に使うインラインスクリプト(OS 切替の同期スクリプトなど)は、その中身の SHA-256 ハッシュを script-src に列挙して許可しています。build-headers.mjs が、実際に出力された HTML を読んでハッシュを計算する。だから「このハッシュと一字でも違うスクリプト」は通らない——正規のインラインだけを、内容で名指しできる。‘unsafe-inline’(全インライン許可)とは正反対の、狙い撃ちの許可です。静的サイトなので、ビルド時に確定できるこの方式が向いています。
締める指令 — object-src / base-uri / form-action / frame-ancestors
残りは、それぞれ特定の攻撃口を塞ぎます。
object-src ‘none’ — <object>/<embed>(古いプラグイン)を全面禁止。今どき使わないので、塞いでおく。
base-uri ‘self’ — <base> タグでページ内の相対URLの基準をすり替える攻撃を防ぐ。
form-action ‘self’ — フォームの送信先を自オリジンだけに。注入されたフォームで、入力を外部へ送り出す手口を塞ぐ。
frame-ancestors ‘none’ — このページを誰にも iframe で埋め込ませない(クリックジャッキング防止。前章の X-Frame-Options の新しい版)。
通信先とスタイル — connect-src / style-src / style-src-attr
connect-src ‘self’ https://cloudflareinsights.com — fetch / XHR の送信先を、自オリジンと計測先だけに限る。注入されたスクリプトがデータを外部へ持ち出すのを難しくする(万一動いても、送り先が塞がれている)。
style-src ‘self’ ‘sha256-…’ — スタイルも、自オリジンとハッシュ済みのものだけ。
style-src-attr ‘unsafe-inline’ — ここだけ緩い。style=“…” という属性のインラインを許す。理由は明快で、コードを色分けする Shiki が全トークンに style 属性を吐き、進捗バーも JS で幅を書き換えるため。緩んでいるのは style 属性だけで、スクリプトは厳格なまま。属性のスタイルは XSS の実行経路にはならないので、影響は限定的です。
style-src-attr のコメントは build-headers.mjs にそのまま書かれています——「なぜ緩めるか」を理由付きで残す。健全な CSP は、緩い箇所に必ず理由がある。診断で他人の CSP を読むときも、‘unsafe-inline’ や * を見つけたら「なぜここが緩いのか、正当な理由があるのか」を問います。
持ち帰る一言
既定は ‘self’、script-src はインライン非許可、緩い箇所には理由。 default-src で土台を最も厳しくし、script-src からインラインを外して(ハッシュで狙い撃ち許可)XSS を締め、object/base/form/frame-ancestors で個別の口を塞ぐ。緩めた一箇所(style 属性)には明確な理由がある。次章で、この script-src が実際に XSS を止めるところを、有無で見比べます。
こうなっていればOK
卒業まであと2章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。