メインコンテンツへスキップ

CSP違反が本番だけで出る|Cloudflare Pagesのプレビューで確かめても漏れた理由

Cloudflare PagesでCSPを入れ、プレビューで違反0件を確かめてから本番に出したところ、本番だけで違反が1件出た。計測用のスクリプトが本番のドメインにしか差し込まれていなかったためで、自社のサイト2つで同じことが起きた。原因と、確かめ方の直し方をまとめた。

Cloudflare Pages で CSP(ページが読み込んでよい先をブラウザに伝えるヘッダー)を入れ、プレビューで違反 0 件を確かめてから本番に出したところ、本番だけで違反が 1 件出た。

原因は、Cloudflare Web Analytics の計測用スクリプトが、本番のドメインにしか差し込まれていなかったことである。

今は計測用の読み込み元と送り先を最初から CSP に入れ、CSP を変えたら本番のドメインで確かめ直している。プレビューは CSP の合否には使わない。

目次

この記事で扱うこと

CSP は、書いた後に「違反が 0 件か」をブラウザで確かめるところまでが 1 つの作業になる。AI に書かせた場合も、人が書いた場合も同じである。

編集部では自社のサイト 2 つで、同じ形の見落としを 2 回踏んだ。プレビューで違反 0 件を確かめたのに、本番で違反が出た。確かめた場所が本番と同じではなかった。

この記事はその記録と、確かめ方をどう直したかである。CSP の書き方の全体ではなく、「どこで確かめれば本番の合否になるか」に絞る。AI に危ない操作をさせない対策はClaude Codeのセキュリティ対策、AI が外部に情報を送っていた件はAIエージェントの情報漏洩対策に書いた。

CSPは読み込んでよい先をブラウザに伝える仕組み

主な用途はXSSの被害を抑えること

CSP(Content Security Policy)は、ページが読み込んでよいスクリプトや画像、送信してよい先などを、HTTP のレスポンスヘッダーでブラウザに伝える仕組みである。MDN の解説によると、主な用途はクロスサイトスクリプティング(XSS。攻撃者が他人のサイトの上で自分のスクリプトを動かす攻撃)からの保護である。

同じ解説は、CSP は入力の無害化の代わりにはならず、無害化と CSP を重ねて多層の防御にするものだと書いている。

指示の単位はディレクティブ

CSP はディレクティブと呼ぶ指示を並べて書く。この記事に出てくるのは次の 2 つである。

ディレクティブ 決めること
script-src JavaScript を読み込んでよい先
connect-src スクリプトから通信してよい先(計測データの送信など)

ディレクティブの一覧は MDN のヘッダーの資料に、仕様は W3C の CSP Level 3 にある。

書いていないディレクティブにはdefault-srcが効く

同じ MDN の資料によると、default-src は、ほかの読み込みのディレクティブを書かなかったときに代わりに使われる指定である。たとえば connect-src を書いていなくても、default-src 'self' があれば、自分のドメイン以外への送信は止まる。

計測データの送信が止められるのは、connect-src を自分で書いたときだけではない。default-src を絞った CSP を入れた時点で、ページに差し込まれる外部のスクリプトや送信は、すべて止められる側に入る。

違反するとブラウザが読み込みを止める

許可していない先からスクリプトを読もうとすると、ブラウザはその読み込みを止め、開発者ツールのコンソールに違反を出す。ページの見た目は変わらないことも多く、計測のスクリプトが止まった程度では、画面を眺めても気づかない。

MDN によると、script-src などを指定したページでは、eval() のように文字列をコードとして実行する機能も既定で動かなくなる。これは後の節の、検査の道具が止められた件に関わる。

止めずに報告だけさせるReport-Only

Content-Security-Policy-Report-Only というヘッダーで指定すると、ブラウザは読み込みを止めず、違反を報告だけする。MDN は新しい CSP を本番に適用する前の試験にこの方法を挙げている。

Cloudflare Pagesでは_headersに書く

Cloudflare Pages では出力先のディレクトリに _headers というファイルを置くと、静的なファイルの応答にヘッダーを付けられる。公式の資料によると、規則は 100 個まで、1 行は 2,000 文字までである。

同じ資料は、Pages Functions が作った応答には _headers のヘッダーが付かないと注意している。Functions を使うページでは、CSP を Functions の応答に直接付ける。

プレビューは0件で本番は1件

プレビューで違反0件を確かめた

2026-09-03、自社のサイトの _headers に CSP を入れた。Cloudflare Pages のプレビュー(本番に出す前に、*.pages.dev のアドレスで新しいバージョンを見られる仕組み)を開き、ブラウザのコンソールで CSP の違反が 0 件であることを確かめてから、本番へ出した。

本番で違反が1件出た

本番のドメインで開くと、コンソールに CSP の違反が 1 件出た。プレビューでは出ていなかった違反である。もう 1 つのサイトでも、同じことが起きた。

原因は本番にだけ入る計測用スクリプト

違反していたのは、Cloudflare Web Analytics(Cloudflare のアクセス解析)の計測用スクリプトだった。本番のページには static.cloudflareinsights.com から読む計測用スクリプトと、cloudflareinsights.com への計測データの送信が入っていた。

この計測用スクリプトは本番のドメインにしか差し込まれていなかった。プレビューには入っていないので、プレビューで確かめても違反は出ない。CSP に読み込み元と送り先が無いまま本番に出たため、本番でだけ止められた。

プレビューと本番は配信の途中で差が出る

公式はプレビューを本番の写しとして説明している

Cloudflare Pages のプレビューの資料は、プレビューを本番の環境の写しとして作られたものと説明している。ファイルの中身はたしかに同じである。

プレビューのアドレスには deploy ごとに付く <hash>.<project>.pages.dev と、枝の名前から作る別名の 2 種類がある。どちらも本番のドメインとは別のアドレスである。

それでも、ブラウザが受け取る物は同じとは限らない。同じ資料は、プレビューには既定で X-Robots-Tag: noindex(検索エンジンに載せないよう伝えるヘッダー)が付くと書いている。本番には付かないヘッダーである。

違い プレビュー 本番のドメイン 出どころ
X-Robots-Tag: noindex 既定で付く 付かない 公式の資料
Web Analytics の計測用スクリプト 入っていなかった 入っていた 編集部の 2 件

Cloudflareが配信の途中で差し込む物がある

計測用スクリプトはサイトのファイルには書かれていない。Cloudflare が配信の途中で、ページに差し込む。Web Analytics の始め方の資料によると、Pages のプロジェクトでは、設定を有効にすると次の deploy から Cloudflare が自動でスクリプトを加える。

ファイルを見ても、プレビューを開いても、差し込まれる物は分からない。編集部の場合、差し込まれていたのは本番のドメインだけだった。

差し込まれるかどうかは条件でも変わる。同じ資料によると、応答に Cache-Control: public, no-transform を付けていると、Cloudflare は中身を書き換えられないので、計測用スクリプトは自動では入らない。設定で、EU からの訪問者には差し込まない選び方もできる。同じサイトでも、見る人や応答のヘッダーによって、ページの中身が変わりうる。

同じ仲間にメールアドレスの難読化がある

同じ仲間に、Email Address Obfuscation(メールアドレスの難読化)がある。ページの中のメールアドレスを、収集する bot から隠す機能で、公式の資料によると登録時から自動で有効になっている。

有効になっていると、Cloudflare はページの中のメールアドレスを /cdn-cgi/l/email-protection へのリンクに書き換え、元に戻すスクリプトを差し込む。このリンクを単体で開くと 404 になるので、リンクの検査では壊れたリンクに見える。人がブラウザで見るときは、差し込まれたスクリプトが元のアドレスに戻す。

止めたいときは、ダッシュボードでの設定のほか、特定のホスト名やパスだけで切る設定ルールや、HTML のコメントで囲んで除外する方法が公式に案内されている。Cache-Control: no-transform を付けた応答や、Worker が加えた HTML には、難読化はかからない。

機能ごとにCSPへ足す値の一覧がある

Cloudflare は CSP と Cloudflare の機能の関係をまとめた資料を出している。Web Analytics のほか、スクリプトの読み込みを遅らせる Rocket Loader など、ページに手を加える機能ごとに、CSP に足す値の一覧がある。

ゾーンで有効にしている機能を一度書き出し、この一覧と照らすと、本番にだけ現れる物の見当がつく。

CSPを緩めるか機能を止めるか

同じ一覧ではメールアドレスの難読化を含む Scrape Shield の欄に、script-src 'self' 'unsafe-inline' が挙がっている。'unsafe-inline' はページの中に直接書いたスクリプトを許す指定で、MDN の解説も XSS への守りを弱めるので避けるよう書いている。

機能を使い続けるために CSP を緩めるか、機能を止めて CSP を保つかは、その機能が要るかどうかで決める。CSP を入れる目的が XSS の被害を抑えることなら、緩める前に、その機能を止める道を先に確かめる。

入れた対策

計測の読み込み元と送り先を最初から入れる

Web Analytics を使うゾーンでは、CSP に次の 2 つを最初から入れることにした。

1
2
script-src  https://static.cloudflareinsights.com
connect-src https://cloudflareinsights.com

実際には、それぞれのディレクティブにある既存の値に、この先を足す。

送り先は設定の方法で変わる

Web Analytics の FAQ は、計測データの送り先が設定の方法で変わると書いている。自動で差し込む場合は自分のドメイン('self')、手で埋め込む場合は cloudflareinsights.com である。

どちらの先が止められるかは、設定の方法で変わる。足す値を資料から決め打ちせず、本番のコンソールで、実際に止められている先を見てから足す。

CSPを変えたら本番のドメインで確かめ直す

CSP を触ったら、本番のドメインで開き直して、コンソールの違反を数える。プレビューは CSP の検証には使えない、と決めた。

同じ FAQ は、CSP の変更は本番の前に試験の環境で確かめ、Report-Only で試すことも勧めている。編集部の件は、その試験の環境(プレビュー)に計測用スクリプトが入っていなかった例である。試験の環境で確かめるだけでなく、本番のドメインでも確かめる。

本番で届いているヘッダーも見る

コンソールの違反を数える前に、本番のドメインが返している CSP そのものを見ておくとよい。_headers に書いた値がそのまま届いているとは限らないからである。たとえば Pages Functions が作るページには、前の節のとおり _headers のヘッダーが付かない。

1
curl -sI https://<本番のドメイン>/ | grep -i content-security-policy

ページの種類(静的なページ、Functions が作るページ)ごとに 1 つずつ見ると、付いていないページが見つかる。

訪問者のブラウザから違反を集める

MDN は違反の報告の受け取り方として、Reporting-Endpoints ヘッダーで送り先を決め、CSP の report-to でそれを指す方法を勧めている。古い report-uri は非推奨だが、report-to がすべてのブラウザで使えるまでは、両方を書いておく必要があるとしている。

本番のドメインで Report-Only と報告の送り先を組み合わせれば、本番にだけ差し込まれる物の違反も、訪問者のブラウザから集められる。プレビューでは出ない違反を、読み込みを止めずに拾う方法になる。

違反の報告から分かること

MDN が示す報告の例には、次のような項目が入っている。

項目 分かること
documentURL 違反が起きたページ
blockedURL 止められた読み込み元や送り先
effectiveDirective 止めたディレクティブ
originalPolicy そのとき効いていた CSP の全文
disposition 実際に止めたのか、報告だけか

blockedURL と effectiveDirective を見れば、どの先をどのディレクティブに足せばよいかが分かる。前の節で書いた「足す値は、本番で実際に止められている先を見て決める」を、訪問者のブラウザの記録で行う形である。originalPolicy を見れば、_headers に書いた CSP がそのページに届いていたかも確かめられる。

検査の道具もCSPに止められる

wait_for_functionがeval禁止に当たった

CSP は検査の道具の動きも止めることがある。2026-09-20、手元の画面を Chromium 148.0.7778.96 と Python 版の Playwright で検査していたときのことである。画面は file:// で開くローカルの物で、CSP で script-src 'self' を指定していた。

再生の位置や DOM の変化を、page.wait_for_function() で待っていた。すると、待機の処理の中で eval が使われ、EvalError と unsafe-eval の禁止のエラーで失敗した。最初の 2 回の検査はこの理由で落ちた。

同じ画面で、通常の DOM を読む処理は動いた。画面の機能が壊れているわけではないと、これで切り分けられた。

画面のCSPは緩めずに検査の側を変えた

直したのは検査の側である。画面の CSP はそのまま保ち、page.evaluate() で状態を読み、ホスト側で短く待つ処理を、期限を付けて繰り返す形にした。この形で、再生、区間の移動、保存、再開など 60 項目が通った。

最初の 2 回の失敗も記録に残し、合格は検査の道具を変えた結果であると書いた。

CSPを緩めた環境の合格を元の画面の合格にしない

Playwright にはページの CSP を無視して動かす bypass_csp という設定もある(Browser の資料)。CSP を緩めたり無視したりした環境で検査が通っても、それは CSP の付いた元の画面が動く証拠にはならない。

名前から実装を推測しない

最初に気づけなかったのは、状態を読むための待機なので、通常の DOM を読む処理と同じ条件で動くと思い込んでいたからである。実際には、使っていた待機の実装はページの中で文字列をコードとして実行しており、画面の CSP に止められた。

この件はこの環境(上の Chromium のバージョンと、手元の Python 版 Playwright)で確かめたことで、Playwright のすべてのバージョンに当てはまるとは限らない。エラーが出たら、名前から実装を推測せず、例外がどこで出たかと、ページの CSP を見る。

CSPを本番で確かめる手順

編集部の 2 件から、CSP を入れるときの手順をまとめる。

  1. ゾーンで有効にしている Cloudflare の機能を書き出し、CSP に足す値の一覧と照らす
  2. Web Analytics を使うなら、計測の読み込み元と送り先を最初から CSP に入れる
  3. 足す値は本番で実際に止められている先を見て決める。送り先は設定の方法で変わる
  4. 初めて入れる CSP は Report-Only で試す
  5. CSP を変えたら、本番のドメインで開き直し、コンソールの違反を数える。プレビューの 0 件を本番の合否にしない
  6. Pages Functions が作るページは、_headers ではなく Functions の応答にヘッダーを付ける
  7. 検査の道具が CSP に止められたら、画面の CSP を緩めず、検査の側を変える

書いた CSP の弱い所の点検には Google の CSP Evaluator が使える。nonce やハッシュで読み込みを絞る厳格な CSP の考え方は web.dev の解説、書くときの注意点は OWASP のチートシートにまとまっている。

よくある誤解

「プレビューで違反が0件なら、本番も0件」

編集部ではプレビューで 0 件、本番で 1 件だった。Cloudflare が配信の途中で差し込む物は、本番のドメインにだけ現れることがある。

「サイトのファイルに無いスクリプトは、CSPに関係ない」

計測用スクリプトもメールアドレスを戻すスクリプトも、サイトのファイルには無い。ブラウザが受け取るページに入っていれば、CSP の対象になる。

「検査が落ちたら、CSPを緩めて通せばよい」

緩めた環境の合格は CSP の付いた画面の合格ではない。検査の道具の側を、CSP の下で動く形に変える。

「違反が出ても、画面を見れば分かる」

計測のスクリプトが止まっても、画面の見た目はほとんど変わらない。違反はコンソールで数える。

まとめ

Cloudflare Pages で CSP を入れ、プレビューで違反 0 件を確かめてから本番に出したところ、本番だけで違反が 1 件出た。Cloudflare Web Analytics の計測用スクリプトが本番のドメインにしか差し込まれておらず、自社のサイト 2 つで同じことが起きた。

今は計測の読み込み元と送り先を最初から CSP に入れ、CSP を変えたら本番のドメインで確かめ直している。プレビューは CSP の合否には使わない。

検査の道具も CSP に止められる。page.wait_for_function() が eval の禁止に当たったときは、画面の CSP を緩めず、page.evaluate() と期限付きの待機に変えて 60 項目を通した。

明日から試せる3つ

1 つ目は自社のサイトを本番のドメインで開き、開発者ツールのコンソールに CSP の違反が出ていないかを見ることである。

2 つ目は Cloudflare のゾーンで有効にしている機能を書き出し、ページに何かを差し込む機能が無いかを確かめることである。

3 つ目は検査の設定で CSP を無視していないか(Playwright の bypass_csp など)を確かめることである。

Cloudflare の運用で、HTTP 200 を返しながら壊れていた例はHTTP 200なのに壊れている、deploy が成功したのに本番が変わらなかった例はデプロイは成功、本番は変わらず、Playwright の検査が落ちた原因が検査の側にあった例はE2Eテストが16件落ちたに書いた。

編集部では AI導入の支援と、こうした本番での確かめ方の設計も行っている。変更は入れたが本番で何を確かめればよいか決まっていない、という段階の相談については、サービス案内を参照してほしい。

参考にした一次資料

よくある質問

CSPとは何ですか。

Content Security Policyの略で、ページが読み込んでよいスクリプトや送信してよい先などを、ブラウザに伝える仕組みです。HTTPのレスポンスヘッダーで指定します。主な用途は、クロスサイトスクリプティング(XSS)の被害を抑えることです。許可していない先からの読み込みは、ブラウザが止めます。

プレビューで違反が無ければ、本番も大丈夫ですか。

編集部の例では大丈夫ではありませんでした。Cloudflare Pagesのプレビュー(pages.devのアドレス)で違反0件を確かめた後、本番で違反が1件出ました。Cloudflare Web Analyticsの計測用スクリプトが、本番のドメインにしか差し込まれていなかったためです。今は、CSPを変えたら本番のドメインで確かめ直しています。

Cloudflare Web Analyticsを使うとき、CSPに何を足せばよいですか。

Cloudflareの資料は、計測用スクリプトの読み込み元(static.cloudflareinsights.com)をscript-srcに、計測データの送り先をconnect-srcに足すよう案内しています。送り先は設定の方法で変わり、自動で差し込む場合は自分のドメイン、手で埋め込む場合はcloudflareinsights.comとされています。足す前に、本番で実際の読み込み元と送り先を確かめてください。

Cloudflare Pagesでは、CSPをどこに書きますか。

静的なファイルには、出力先のディレクトリに置く_headersというファイルで書けます。ただしPages Functionsが作った応答には_headersのヘッダーが付かないので、その場合はFunctionsの応答に直接付けます。

リンクの検査で、/cdn-cgi/l/email-protectionが404と出るのはなぜですか。

CloudflareのEmail Address Obfuscation(メールアドレスの難読化)が、ページの中のメールアドレスをこのリンクに書き換えているためです。単体で開くと404になりますが、ブラウザでは差し込まれたスクリプトが元のアドレスに戻します。止めたいときは、ダッシュボードの設定、特定のホスト名やパスだけで切る設定ルール、HTMLのコメントで囲む方法が公式に案内されています。

PlaywrightのテストがCSPのせいで落ちることはありますか。

あります。編集部では、script-srcを’self’に絞った画面で、page.wait_for_function()の待機がeval禁止に当たって失敗しました。画面のCSPは緩めず、page.evaluate()で状態を読み、期限付きで短く待つ形に検査を変えて、60項目を通しました。CSPを緩めた環境での合格は、元の画面の合格の証拠にしません。

CSPを本番に入れる前に試す方法はありますか。

Content-Security-Policy-Report-Onlyというヘッダーで指定すると、ブラウザは読み込みを止めずに違反を報告だけします。MDNやCloudflareの資料も、本番に適用する前の試験にこの方法を挙げています。ただし、試す場所が本番のドメインでなければ、本番にだけ差し込まれる物の違反は出てきません。

関連する取り組み

CONNECTED SERIES
AIで投資の壁を越える
18 本の実装記録。AI 投資の「予測不能」と言われる 9 つの壁を、コードと実データで検証した連載。
note で読む →
触れるデモ ALLFORCES の自社サービス
旅館AIコンシェルジュ
施設のナレッジで即答し、空室照会は裏で並行して走らせる。架空データで動く汎用版。
触ってみる →