CSPをサイトに入れようとすると、必ずぶつかる問いがあります。違反レポートを送る先をどうするか。
3つの選択肢がありました。SaaSの無料プランを使う、自前でendpointを立てる、endpointを持たずブラウザのConsoleで目視する。最初はSaaSが楽そうに見えますが、Cloudflareアカウントを既に持っているなら、自前で立てるのが学習効率と将来の引き出しの両方で得です。
3つの選択肢
SaaSの無料プラン
report-uri.comの無料枠は月10,000件まで受け取れます。アカウントを作ってendpoint URLをもらってヘッダーに貼るだけ。所要時間5〜10分。
メリットはダッシュボードが既にあること。月次レポートをPDFで出してくれるSaaSなら、客への「やってますよ報告」が省力化できます。
デメリットは外部SaaSにレポート(URL、User-Agent、違反directive)が送られること。公開サイトのデータなのでプライバシー問題は薄いですが、金融や大企業の客で「データを国外SaaSに送れない」と言われると即アウト。
自前でCloudflare Workerを立てる
CFアカウントが既にあるなら、追加SaaS契約なしでendpointを作れます。所要時間1〜1.5時間。
メリットは生のCSP違反JSONを自分の目で見ること。CSPの仕様、CORS preflightの挙動、新旧2形式(application/csp-reportとapplication/reports+json)のフォーマット差まで触れます。SaaSダッシュボードを眺めるだけでは絶対に身に付かない領域。
デメリットは保守責任。受託先のために構築して引き渡すと、orphan化のリスクが残ります。けれど自社サイトなら自分で保守できる。
endpointなしで運用
Content-Security-Policy-Report-Onlyをヘッダーに入れて、report-uriやreport-toディレクティブを省略します。違反はChromeやSafariのConsoleにRefused to load ... because it violates ...として出るので、開発者が各ページを巡回して目視。
6〜20ページ規模のサイトなら現実的。コーポレートサイトで継続監視の必要が薄い時はこれで十分です。
どれを選ぶか
| シナリオ | 推奨 |
|---|---|
| 中小〜中堅、社内に技術運用チームなし | SaaS |
| 既にCloudflareを使ってる客 (金融・大企業含む) | 自前Worker |
| 6〜20ページの静的サイト、継続監視不要 | endpointなし |
| 自社サイト、学習目的兼ねる | 自前Worker |
このサイトでは「学習と将来のクライアント提案の引き出し」を理由に自前Workerを採用しました。一度組んでおけば、次回の提案で「自社サイトでこの構成で動かしてます」と言える。
最小構成のWorkerコード
worker/src/index.ts:
interface Env {}
const ALLOWED_ORIGINS = new Set<string>([
'https://example.com',
'https://example-staging.pages.dev',
]);
function isAllowedOrigin(origin: string | null): boolean {
if (!origin) return false;
return ALLOWED_ORIGINS.has(origin);
}
export default {
async fetch(request: Request): Promise<Response> {
const origin = request.headers.get('Origin');
const corsOrigin = isAllowedOrigin(origin) ? origin! : 'null';
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': corsOrigin,
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Max-Age': '86400',
'Vary': 'Origin',
},
});
}
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
let body: unknown;
try {
body = await request.json();
} catch (e) {
return new Response('Invalid JSON', { status: 400 });
}
console.log(JSON.stringify({
kind: 'csp-report',
timestamp: new Date().toISOString(),
contentType: request.headers.get('Content-Type'),
origin,
userAgent: request.headers.get('User-Agent'),
report: body,
}));
return new Response(null, { status: 204 });
},
} satisfies ExportedHandler<Env>;
50行未満で完成です。wrangler.tomlは3行:
name = "example-csp-report"
main = "src/index.ts"
compatibility_date = "2026-05-30"
npx wrangler deployでデプロイ完了。デフォルトのworkers.devサブドメイン(例: example-csp-report.your-subdomain.workers.dev)が割り当てられます。
ストレージは最初なし、確認はwrangler tail
KVやD1やR2は使いません。console.logした内容をnpx wrangler tailでストリーミング表示します。
$ npx wrangler tail --format pretty
ターミナルに違反が届くたびに整形されたログが流れる。開発期間中はこれで十分。後から「過去の違反を一覧で見たい」となったらKVに移行できます。最初から作り込まないのが小さく始めるコツ。
CORS preflightへの対応
application/csp-report(旧形式)はSimple Requestなのでpreflightなし。application/reports+json(Reporting API、新形式)はpreflightを要求します。OPTIONSハンドラがないと違反が届きません。
Access-Control-Allow-Originはワイルドカード(*)ではなくorigin allowlistで具体的に返します。クライアント側がそのoriginをCSPで指定するため、ワイルドカードで受けても困らないですが、CSP report endpointとしては送信元を絞っておくのが筋。
ヘッダー設定
サイト側の_headers(Cloudflare Pages)に:
Reporting-Endpoints: csp-endpoint="https://example-csp-report.your-subdomain.workers.dev/"
Content-Security-Policy-Report-Only: default-src 'self'; ...; report-uri https://example-csp-report.your-subdomain.workers.dev/; report-to csp-endpoint
Reporting-Endpoints(HTTPヘッダー)で送信先を定義し、CSPのreport-to csp-endpointで参照。後方互換のためreport-uriも同じURLで併記します。
副次的に得られるもの
Worker endpointを動かしておくと、Permissions-Policy違反、Network Error Logging、Document-Policy違反なども同じReporting APIで受けられます。CSPだけのために作ったendpointが、将来の他の監視にも使い回せる。
Reporting-Endpointsにendpointを増やすだけで、Workerコードはそのままで複数種類のレポートを受信可能。
CSP report endpointの選択は「外注するか自前で持つか」の判断です。SaaSは楽だけどデータ越境とベンダーロックインがある。自前Workerは保守責任が残るけど、CSP仕様への理解とクライアント提案の引き出しが手に入る。Cloudflareアカウントがあるなら、最小30行のWorkerで足りる構成から始められます。