NoteStack

This page is a beginner-friendly, step-by-step guide to collecting CSP (Report-Only) violation reports safely: evolving from a simple “received” marker to daily JSONL logs, preventing log bloat, and preparing a clean path to SQLite-based aggregation.

CSPレポートをJSONLで受信・保存する(肥大化対策とSQLite移行の準備)

CSP(Content Security Policy)を導入するとき、いきなりブロックを始めるのは不安ですよね。

そこでよく使われるのが Report-Only(監視モード)です。これは「ブロックはせずに、違反が起きたらブラウザがレポートを送る」仕組みです。

このページのゴール
このページでは“まだやらないこと”

先に結論:おすすめの進め方(3ステップ)

Step 1) 受信できるかだけ確認(receivedログ)

最初は「届いているか」を確実にするのが大事です。ここで詰まると全部進みません。

Step 2) 1段階だけ進める:日次JSONLで“中身の最小セット”を保存

今回の到達点です。“どのページで / 何が / どのURLが”を追えるようになり、CSPの調整が現実的になります。

Step 3) ログ肥大化を止める:ローテーション(圧縮・保持期限)

JSONLにしても、放置すればファイルは増えます。運用としては logrotate などで管理します。

CSP Report-Onlyって何?(やさしい説明 → 正確な説明)

やさしい説明:「危ない動きがあったら、ブラウザが“報告”してくれるモード」です。ブロックはしないので、まずは安全に様子見できます。

正確な説明
  • Content-Security-Policy-Report-Only ヘッダを返す
  • 違反が起きると、ブラウザが report-uri(またはReporting API)へJSONをPOSTする
  • 「違反が起きた」ことを知って、後からポリシーを調整できる

このページでは、受信口(エンドポイント)側の作りと運用に集中します。

なぜ“receivedだけ”では足りないの?

received だけのログは「届いている」確認としては優秀です。

でも、CSPの管理を進めるには次が分からないと調整できません。

分かるようにしたいこと(最小セット)
  • どのページで起きた?(document-uri
  • 何が違反?(effective-directive / violated-directive
  • 何をブロックした?(blocked-uri

そこで「1段階だけ進める」最適解が 日次JSONL です。

今回の1段階:日次JSONLに“最小フィールド”を保存する

JSONL は「1行が1つのJSON」です。ログとして扱いやすく、あとで集計(grep/jq/SQLite)しやすいのが長所です。

保存する最小フィールド(例)
  • ts:受信時刻(UTC推奨)
  • ip:送信元IP(参考用。プロキシ経由なら扱い注意)
  • ua:User-Agent(ノイズも多いので短めに)
  • document_uri / blocked_uri
  • effective_directive / violated_directive
  • source_file, line, col, status(ある場合のみ)

保存先は、例えば次のように「日付で分ける」と運用がラクです。

/var/www/your-site/logs/csp-report-YYYYMMDD.jsonl
なぜ日次ファイル?
  • どの日に増えたかがすぐ分かる
  • 圧縮・削除の単位が明確
  • ログ肥大化の管理がしやすい

受信エンドポイントの設計方針(セキュリティと可用性)

方針1:POSTだけ受ける

攻撃面を狭めます。GETで動く必要はありません。

方針2:想定外入力は“騒がず”204

CSPレポート送信はブラウザ実装に差があり、失敗時に再送が起きることもあります。ここで4xx/5xxを乱発すると、ログと負荷が増えやすいです。

方針3:サイズ上限を持つ(Nginx + PHPで二重化)

Nginxの client_max_body_size と、PHP側の strlen チェックで守ります。

方針4:ログは“1行=1件”を壊さない

改行混入を無害化し、追記は LOCK_EX で競合を避けます。

動作チェック(1つずつ確認する手順)

ブラウザを待たずに、まずは curl で「確実に再現」するのが安全です。

チェック1:エンドポイントが 204 を返す

curl -s -o /dev/null -w "%{http_code}
" \
  -X POST https://example.com/csp-report \
  -H 'Content-Type: application/csp-report' \
  --data '{"csp-report":{"document-uri":"https://example.invalid/","blocked-uri":"https://evil.invalid/x.js","effective-directive":"script-src","violated-directive":"script-src","source-file":"https://example.invalid/app.js","line-number":12,"column-number":34,"status-code":200}}'
期待する結果

204

チェック2:日次JSONLが作られて増えていく

sudo ls -la /var/www/your-site/logs/ | grep csp-report-
sudo tail -n 3 /var/www/your-site/logs/csp-report-$(date -u +%Y%m%d).jsonl
見えたら成功
  • "document_uri""blocked_uri" が見える
  • 1行が途中で折れず、1件が1行で並ぶ

ログ肥大化は大丈夫?(結論:運用で止める)

JSONLは「役に立つログ」ですが、放置するとファイルは増えます。

そこで運用として ログローテーション を入れます。

おすすめ(現実的な最小セット)
  • 日次ローテーション(またはサイズ)
  • 圧縮(gzip)
  • 保持期限(例:14日 / 30日)

SELinux環境では、ローテ後に新規作成されたログのラベルがズレて書き込めなくなることがあるため、必要なら restorecon を組み合わせます(事故予防)。

将来のSQLite化を見据えた“今の勝ち筋”

SQLite化は「保存形式を変える」だけではなく、重複排除日次集計で運用コストを下げる段階です。

なぜJSONLが効く?
  • SQLiteに取り込むときの入力形式として扱いやすい
  • 「まずログで意味ある情報を取る」→「次に集計で軽くする」の順が安全
  • 障害時もファイルが残り、調査に強い

次の段階では、例えば document_uri + effective_directive + blocked_uri をキーにして count++ することで、保存量を劇的に減らせます。

よくある質問(FAQ)

Q. 204で返すのは“エラーを隠している”ことになりませんか?

A. 受信エンドポイントは「落ちないこと」が最優先になりやすいです。壊れたJSONや想定外のContent-Typeで4xx/5xxを返すと、再送やログ増加で負荷が上がることがあります。必要な情報はサーバ側ログ(JSONLやエラーログ)で追う設計にします。

Q. IPは信頼できますか?

A. プロキシ/CDN経由ではヘッダで来るIPは偽装されやすいので注意が必要です。信頼できる経路(例:CDNのヘッダ)を優先し、X-Forwarded-Forは先頭のみ+妥当性検証をする、などが現実解です。

Q. JSONLはどのくらい保持すべきですか?

A. まずは 14日〜30日が現実的です。長期傾向が欲しくなったら、SQLiteの集計テーブルだけを長期保存する方が運用が軽くなります。

Q. Reporting API(reports+json)も受けたいです

A. 入口で application/reports+json を許可し、ペイロード形式差を吸収する処理を追加していくのが安全です。まずは現行の “csp-report” 形式を確実に集めるのがおすすめです。