紡Book の勉強ノートに戻る
コードレビュー学習2026.05.11

「CSPって何?」から XSS 防御の層構造を理解するまで

セキュリティCSPXSSNext.js判断軸

コードレビューで CSP-Report-Only を一旦外す判断をしたあと、その差分を見直しながら「そもそも CSP って何?」を整理した学習ノートです。

この記事でわかること

  • next.config.ts がそもそも何を設定するファイルか
  • CSP(Content-Security-Policy)違反とは何か / Enforce と Report-Only の違い
  • XSS 防御の 3 層構造 と、CSP がその中でどこに位置するか
  • 形骸化していた Report-Only を「直す / 外す / 後回し」の 4 択で判断した流れ
  • 違反レポートの受け皿の 3 パターン(自前 / SaaS / CDN)
  • セキュリティ機能の「重要さ」を優先順位の文脈で見るコツ

なぜ next.config.ts の差分が「ログ」に見えるのか

next.config.ts から CSP-Report-Only 関連の記述を消したコミットは、一見「なんだかログっぽいものを消した差分」に見える。Report-Only という単語と、削除された report-uri 風の構造から、観測系・通報系の何かだということだけは伝わってくる。

でも next.config.ts 自体は別にログ専用のファイルではありません。整理すると、

  • ビルド/ランタイムの挙動(画像最適化、リダイレクト、rewrites、experimental フラグなど)
  • HTTP レスポンスヘッダの定義(async headers() のブロック)
  • 環境変数の公開、Webpack カスタマイズ、出力モード

を 1 ファイルにまとめる、Next.js アプリの根本設定ファイル。今回ログっぽく見えたのは、消したのが「CSP 違反を通報先に送る観測モード」だったから、というだけだった。

ここから「じゃあ CSP 違反って何?」「Report-Only ってどういう状態?」を順に詰めていった。

CSP 違反とは何か

CSP(Content-Security-Policy)は、サーバーがブラウザに対して

このページでは「script はこのオリジンから」「img はこの CDN から」「font はこのフォントサービスから」しか読み込まないでね

という**許可リスト(whitelist)**をレスポンスヘッダで宣言する仕組み。具体的には script-src / img-src / font-src / connect-src みたいなディレクティブで、リソース種別ごとに「どこからなら OK か」を書きます。

ブラウザはページを描画する過程で、この許可リストに載っていないリソースを読み込もう・実行しようとするたびに「違反」として検知します。例えば、

  • script-src 'self' しか許可してないのに、<script src="https://evil.example.com/..."> を読もうとします
  • style-src に 'unsafe-inline' がないのに、HTML に <div style="color:red"> のインラインスタイルが入っています
  • connect-src に登録してない API に fetch します
  • frame-ancestors 'none' のページを別サイトが <iframe> で埋め込もうとします

こういうのが全部 CSP 違反。XSS やクリックジャッキング対策が主目的で、攻撃者がスクリプトを混入させても、CSP で許可されてない場所からは実行/通信できないので被害を抑えられる、という防御層になります。

Enforce モードと Report-Only モード

違反が起きたときのブラウザの挙動には 2 モードあります:

  • Enforce モード(Content-Security-Policy ヘッダ)
    • 違反したリソースの読み込み・実行をブロックします
    • 本番運用でユーザを実際に守るのはこっち
  • Report-Only モード(Content-Security-Policy-Report-Only ヘッダ)
    • ブロックはしません
    • report-uri / report-to で指定したエンドポイントに違反レポートの JSON を POST するだけ

Report-Only を使う典型的な流れは、

  1. いきなり Enforce で本番に出すと、思わぬ違反で自分のサイトが壊れるリスクがあります
  2. なのでまず Report-Only で観測して、想定外の違反が出ないかを確認
  3. 落ち着いたら Enforce に昇格

という段階導入のための観測モード。クラウドサービスを本番投入する前に「ステージング環境でログだけ取る」のと感覚的に近いです。

ここで重要なのは、Report-Only は通報先(report-uri / report-to)が無いとほぼ意味がないこと。

許可リストは next.config.ts。ヘッダで渡り、ブラウザのメモリで照合する。DB には無い。next.config.ts許可リストの定義ファイル・GitHTTP ヘッダレスポンスに載せる保存しないブラウザのメモリ出所を照合する保存しない違反したときの3通り。通報の置き場所がここで分かれる。Enforce読み込みをブロック通報は必須ではない通報先ありJSON を POST受け皿が保存する通報先なしコンソールのみ保存しない・今回外したもの右の列は誰の DB にも残らない。コンソールを開かない限り見えない。

レポート JSON には何が入っているか

ちなみに違反レポートの中身は、

  • blocked-uri — 何を読み込もうとしたか
  • violated-directive — どのディレクティブに違反したか
  • source-file / line-number — どこで起きたか
  • document-uri — どのページで起きたか

みたいな項目。これを集計すると「想定外にどこから何を読み込もうとしているか」が可視化できます。XSS の試みが入ってきたとき、攻撃者が混入させたスクリプトの送信先(evil.com 等)も blocked-uri に出るので、攻撃検知の材料にもなります。

XSS 防御は 3 層構造

ここまで整理すると「CSP めちゃくちゃ重要じゃん」という感想になります。実際重要だけど、優先順位の話をすると少し落ち着いて見られます。XSS 対策は基本的に層構造になっていて、

層 何をする 例
1. そもそも入れない 出力エスケープ、危険な API を避ける React の自動エスケープ、dangerouslySetInnerHTML 不使用
2. 入力経路でサニタイズ DB 保存前 / 取り出し後の処理 DOMPurify、HTML サニタイザライブラリ
3. 実行段階で止める ブラウザがリソースの取得/実行を弾く CSP

綴 Photo の現状をこの層に当てはめると、

  • 層 1: React/Next.js は出力をデフォルトで自動エスケープします。JSX に {userInput} と書いた瞬間に < や > がエンティティ化されます。素直に書いてる限り XSS は入りにくいです。危ないのは dangerouslySetInnerHTML を使うとき、<a href={userUrl}> で javascript: スキームを許してしまうとき、window.location = userInput みたいな動的代入をするとき — このあたりだけ気をつければ 1 次防御はだいぶ強いです
  • 層 2: ユーザ HTML 入力を DB に保存する経路があるならサニタイズが必要(過去に XSS 対応の記事 で整理済み)
  • 層 3: 今は CSP enforced も Report-Only も外れている状態
XSS の止め場所は3つ。ユーザ HTML だけが DB。CSP の許可リストはヘッダで、今は外している。層1そもそも入れないReact の描画メモリ・保存しない層2入力経路でサニタイズphotos_albumcustompagehtml_content・DB層3実行段階で止めるnext.config.ts のヘッダ今は外している・DB に無い下段が最後の安全網。いま効いているのは上の2段である。

加えて、CSP 以外のセキュリティヘッダ — Strict-Transport-Security(HSTS)/ X-Frame-Options / X-Content-Type-Options / Referrer-Policy / Permissions-Policy — はそのまま残してあって、それぞれ別の攻撃面(中間者攻撃、クリックジャック、MIME スニッフィング、リファラ漏れ、不要な API 権限)をカバーしている。

だから「一旦外す」を選んだ

形だけ動いていた Report-Only は、いま「動いていない設定をどうするか」の問題になる。

選択肢は「ある / ない」の 2 択じゃなくて、4 択で並べた:

案 内容 評価
A 自前で直す Django / API Route に /api/csp-report を立てて違反を記録 自前で集計できる。実装+閲覧 UI で半日〜1日
B SaaS で直す Sentry 等の CSP endpoint に送る 設定だけで観測できる。月固定費が乗る
C 一旦外す Report-Only ヘッダ自体を削除 実装ゼロ。enforced 移行時の事前観測は消える
D 放置 形骸化したまま残す 「観測しないなら付けない」原則に反する

このプロジェクトは公開ローンチ前で身内+限定共有レベル、かつ撤退判定期間(〜2026-10)で固定費を増やしたくない。A は公開前マストを圧迫、B は固定費が撤退判定と噛み合わない、D は原則違反。残った **C「一旦外す」**を選んだ。

ポイントは、外しても何も失わないこと。許可リスト(fonts.googleapis / さくらストレージ / connect-src 等)は git log -p next.config.ts で復元できるし、next.config.ts には「なぜ外したか / 何を入れ直すか」を再導入方針としてコメントで残してある。消すのは「いま動いていない設定」であって、再導入の素材ではない。

「ある / ない」ではなく「維持する / 直す / 外す / 後回し」の 4 択で考える癖がつくと、セキュリティ設定の整理がブレにくくなる。

レポートの受け皿は 3 パターン

将来 CSP-Report-Only を再導入するときの通報先の選択肢も整理しておくと、

1. 自前で構える(Next.js の API Route or Django 側)

ブラウザは違反時に指定 URL へ JSON を POST してくるだけなので、受け口自体は小さいです。app/api/csp-report/route.ts に POST ハンドラを生やして DB に突っ込むだけで動きます。

重いのは受け口じゃなくて溜まったレポートを眺める UIのほう。集計しないと「日に数千件の同じ違反」に埋もれて誰も見なくなります。ダッシュボード相当を作るか、既存のログ基盤に流す工夫が要ります。

2. SaaS(Sentry / Report URI / Datadog)

Sentry は元々エラー監視 SaaS だけど、CSP レポート受信にも対応していて、既存の Issue ビューに違反が並ぶので閲覧 UI をタダで手に入れられるのが大きいです。代わりに有料プランの月額固定費が乗ります。

「撤退するかもしれない期間に固定費を払いたくない」みたいなフェーズとは噛み合いません。クラウドの有料サービスを「本気で使う前提」が立つまで契約しないのと同じ判断軸。

3. CDN / エッジ提供のレポート機能(Cloudflare 等)

Cloudflare を前段に置いていると、ダッシュボード側でセキュリティ関連機能としてレポート受信や可視化を提供しているものがあります。ただし提供範囲やプラン区分は変わりやすいので、実際に使うときは最新の公式ドキュメントで確認するのが安全です。「無料で全部入り」かどうかは時期次第。

綴 Photo は今 Vercel + Render の構成で前段に Cloudflare を挟んでいないので、当面この選択肢は候補に入らない。

どれを選ぶか

定石は「最初から完璧に構えない、まず観測の経路だけ通す」。

  1. ローンチ直後〜数週間: 何もしない(他の防御層で十分機能している)
  2. トラフィックが少し出てきたら: 無料枠の広い SaaS(Report URI 無料プラン、Sentry 無料枠)で観測を開始
  3. 違反パターンが収束したら: Enforce に昇格
  4. ボリュームが分かったら: 自前 / 有料プラン / そのまま無料 を選び直す

固定費を払う判断は、観測してみて「この量なら自前 < SaaS」と分かってからで十分。

なぜ早めに観測を始める方がいいか

ローンチ後に CSP を入れる優先度は、感覚的に思っていたより高めに置いておいた方がいいです。理由は、

  • ユーザ投稿(写真のひとことコメント、共有時のメタデータなど)が増えるほど XSS 流入経路の母数が増えます
  • 共有 URL の仕様上、第三者がアクセスする面があるので「自分しか見ない」アプリより被害が広がりやすいです
  • 後から script-src を厳しくすると、既存のインラインスタイル/スクリプトに引っかかって動かなくなる箇所が出ます。早めに Report-Only で観測しておくほど、Enforce 移行が安全です

つまり「Enforce で本番に出す」までの距離を縮めておくこと自体に価値があります。ローンチ前に詰める優先順位ではないけど、ローンチ後の早い段階で再導入するタスクとして記憶しておく。

「重要 ≠ 今すぐやる」の感覚

重要なセキュリティ機能ほど「重要だけど、今すぐやることではない」という判断が要ります。CSP はまさにその典型です。

セキュリティ機能は「あった方がいい」が無限に積み上がる領域で、全部を最初からやろうとするとリリースが遅れます。層構造の上位(出力エスケープ、HTTPS、認証)が機能していれば、最後の安全網が一時的に外れていても致命傷にはなりません。

逆に「形だけある」状態は「何もない」より悪いことがあります。Report-Only ヘッダだけ送って通報先が無いと、開発者は「CSP 設定済み」と認識して安心するけど、実態は何も検知していません。

「何のためにあるか」「今その目的を果たしているか」を定期的に問い直す — このテーマで一番効く視点はここに尽きます。

学びポイント

  • next.config.ts は Next.js の根本設定ファイル。ビルド挙動・HTTP ヘッダ・環境変数・Webpack まで全部ここ
  • CSP 違反 = ブラウザが許可リスト外のリソース読み込み/実行を検知すること。中身の検査ではなく、出所の照合
  • Enforce はブロック、Report-Only は通報のみ。Report-Only は通報先とセットで初めて意味を持ちます
  • XSS 防御は 3 層: 出力エスケープ → サニタイズ → CSP。CSP は最後の安全網
  • レポート受け皿は自前 / SaaS / CDN の 3 パターン。まず観測の経路を通す → 量が分かってから本気の構成にします
  • 動いていない設定は「ある / ない」ではなく「維持 / 直す / 外す / 後回し」の 4 択で判断します。外す時も git log と方針コメントを残せば再導入で困りません
  • 「重要 ≠ 今すぐやる」。層構造の上位が効いている間は、下位の安全網が一時的に外れていても許容できます

おわりに

セキュリティ機能は「入れたら入れっぱなし」になりやすいが、動いていないものを残しておくことにもコストがあります(誤った安心を生みます)。層構造の全体像を持っておけば、「今これは外していい / これは外せない」の判断がブレません。外す時も、外した理由を文書化して git log に素材を残します ── それで再導入の道は閉じません。


この記事は 2026 年 5 月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。