Cloudflare×KVでCSPレポートを効率的に受信・保存する手順
Cloudflareの変換ルールを利用してContent Security Policy(CSP)の設定を行いました。
設定したCSPのレポートを受信する仕組みをCloudflareのFree契約で構築します。CSPのためにnonce値をCloudflare Workersで付与しようとしたのですが、Workersの制限と力不足で上手くは行きませんでした。今回のレポートをWorkersで受信してCloudflare KVでデータとして保存する方法は、受信データーの容量が非常に小さく、WorkersのCPU動作時間10ms以下という制限は大丈夫だと思います。
作業の流れと必要コードの内容だけを掲載していますが、詳細な方法は検索するとたくさん見つかります。
目次OpenClose
CloudFlare WorkersとKVを利用したCSPレポートの受取機能の構築手順wrangler.jsoncwrangler.tomlindex.jsCloudFlare WorkersとKVを利用したCSPレポートの受取機能の構築手順
まずはじめにCloudflare Workersを使えるようにします。
- node.jsとnpmをインストールする。
- wranglerをインストールする。
次にWorkersとKVの設定をします。
- Cloudflare ダッシュボードで KV Namespace を作成する。 (例:REPORTS_KV)。
- Wrangler CLI を使用して Worker プロジェクトを作成・設定します。
- 上記のwrangler.tomlとsrc/index.jsをプロジェクトに配置する。
- wrangler.tomlのidを作成した KV Namespace の ID に置き換える。
- wrangler.jsonc;(またはwrangler.toml) ファイルにルート設定を記述する。
- wrangler deployコマンドで Worker をデプロイする。
このとき、Workersで自動で生成されるドメインは無効にすることをおすすめします。CSPレポートはwrangler.jsoncで設定で設定したアドレスに送られるのでWorkersドメインを公開する必要はありません。また、デプロイするたびにドメインはアクティブになるので注意が必要です。
最終的にはCloudflare KVの「KV ペア」タブにCSPレポートが表示されます。

wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "your-domein-csp",//worlersの名前
"main": "src/index.js",
"compatibility_date": "2025-06-01",// 最初の作成日
"kv_namespaces": [
{
"binding": "REPORT_KV",
"id": "XXXXXXXXXXXXXXXXXXXXXXX" // KV Namespace の ID
}
],
"compatibility_flags": [
"nodejs_compat",
"global_fetch_strictly_public"
],
"observability": {
"enabled": true
},
"routes": [//ルートの設定ここから
{
"pattern": "your-domein.com/reports",
"zone_name": "your-domein.com"
}
]//ルートの設定ここまで
}
wrangler.toml
name = "your-domein-csp" // Workerの名前
main = "src/index.js"
compatibility_date = "2025-6-01" // 作成日に近い日付
[[kv_namespaces]]
binding = "REPORT_KV" //KVの名前
id = "XXXXXXXXXXXXXXXXXXXX"// KV NamespaceのID
index.js
export default {
async fetch(request, env, ctx) {
// POSTリクエストのみを処理
if (request.method !== 'POST') {
return new Response('Expected POST request', { status: 405 });
}
const contentType = request.headers.get('content-type');
// CSPレポートのContent-Typeをチェック
// report-uri は 'application/csp-report'
// report-to は 'application/reports+json' (または単に 'application/json')
if (
!contentType ||
(!contentType.includes('application/csp-report') &&
!contentType.includes('application/reports+json') &&
!contentType.includes('application/json'))
) {
return new Response(
'Invalid Content-Type. Expected application/csp-report or application/reports+json.',
{ status: 415 }
);
}
try {
const reportData = await request.json();
const timestamp = new Date().toISOString();
// ユニークなIDを生成してキーとして使用
const reportId = `csp-report-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
// Workerのログに出力 (Cloudflareダッシュボードで確認可能)
console.log(`CSP Report Received (${reportId}) at ${timestamp}:`);
console.log(JSON.stringify(reportData, null, 2));
// KVストアに保存 (REPORTS_KV が wrangler.toml でバインドされている必要あり)
if (env.REPORTS_KV) {
await env.REPORTS_KV.put(
reportId,
JSON.stringify({
receivedAt: timestamp,
clientIp: request.headers.get('CF-Connecting-IP'), // クライアントIP
userAgent: request.headers.get('User-Agent'), // ユーザーエージェント
report: reportData,
}),
{
// オプション: KVエントリの有効期限 (例: 60日間)
expirationTtl: 60 * 24 * 60 * 60,
}
);
console.log(`Report ${reportId} stored in KV.`);
} else {
console.warn('REPORTS_KV binding not found. Report not stored in KV.');
}
// レポート受信成功時は 204 No Content を返すのが一般的
return new Response(null, { status: 204 });
} catch (error) {
console.error('Error processing CSP report:', error.message, error.stack);
// エラー発生時は 500 Internal Server Error
return new Response('Error processing report', { status: 500 });
}
},
};
まずは「Content-Security-Policy-Report-Only」ヘッダーを公開します。これは「Content-Security-Policy」と違いポリシー違反があってもブラウザの動作に制限がかかりません。文字通りレポートだけを表示するので、当面は様子を見るべきです。
ここまでで、ポリシーレポートを受け取りKVで保存できるようになりました。レポートを見やすくするために、一覧表を表示するページをCloudflare Workersで作ります。
最後までお読みいただき、誠にありがとうございます。

