1. はじめに
ソーイ株式会社の西浦です。入社1年目でWebアプリケーションの開発に携わっています。
先日、OWASP ASVS 3.4.3基準のセキュリティ診断を受け、「CSP(Content-Security-Policy)ヘッダがない」との指摘を受けました。
本記事では、セキュリティ診断でCSP導入を指摘された方や、Laravelで運用中のシステムにCSPを追加したい方に向けて、既存システムを壊さずに段階的に導入する方法と、Sentryを活用したレポート収集の実装手順を、実例を交えて解説します。特にSentryを既に導入しているプロジェクトであれば、追加コストなしで始められる内容になっています。
2. CSP(Content-Security-Policy)とは何か?
指摘を受けて調査した内容の要約です。
2.1 CSPの仕組みと動作
CSPは、Webサイトのサーバーがブラウザに対して「どのリソース(スクリプトや画像など)の実行を許可するか」を伝える指示書(HTTPレスポンスヘッダ)です。
動作のイメージは以下の通りです。
- ホワイトリスト形式:「許可したもの以外はすべて拒否する」というスタンスで動作します。
- ブラウザによる強制制限:HTMLを受け取ったブラウザは、CSPヘッダを確認します。もし許可リストにない不正なスクリプトが紛れ込んでいても、ブラウザがその実行を強制的にブロックします。
- 多層防御:万が一アプリケーションに脆弱性があり、悪意のあるコードを埋め込まれても、ブラウザ側で「最後の砦」として攻撃を防ぐ(=XSSの影響を最小限にする)ことが目的です。
2.2 指摘された要件(OWASP ASVS 3.4.3)
今回の診断では、このCSPの仕組みを使って以下の構成を実現するよう求められました。
-
基本制限:
object-src 'none'(プラグインの禁止)およびbase-uri 'none'(基準URL変更の禁止)の定義。 -
script-srcにおける「NonceベースのCSP」の採用:- なぜNonceか: 従来のドメイン指定(許可リスト形式)は、外部サービスの増加に伴い管理が煩雑になり、設定ミスも起こりやすいためです。
- 仕組み: リクエストごとに発行する使い捨ての合言葉(nonce)をヘッダとHTMLタグの両方に付与し、一致したものだけを実行許可します。
-
strict-dynamicの併用: Nonceで許可されたスクリプトが読み込む「子スクリプト」も、連鎖的に信頼する設定です。これにより、依存関係の多いモダンなライブラリも柔軟に動作可能になります。
3. 既存システムへの導入手順とリスク
調査を進める中で、いきなり制限を開始する Content-Security-Policy ヘッダを付与することには、非常に大きなリスクがあると分かりました。
導入に伴うリスク
CSPを導入すると、ブラウザは「許可リストにないもの」をすべて敵とみなして遮断します。そのため、以下のような事態が起こり得ます。
- 機能停止: 決済(Stripe)やリアルタイム通信(Pusher, Agora)などの外部API通信がブロックされ、主要機能が動かなくなる。
- デザイン崩れ: Google FontsやCDN上のスタイルシート、アイコンフォントが読み込めなくなる。
- 分析不能: Google AnalyticsやGTMなどの計測タグが動作しなくなる。
特に、自社システムが「どのドメインと通信しているか」を漏れなく把握することは、規模が大きくなるほど困難です。
標準的な導入フロー
そのため、以下のステップで「許可設定の精度」を上げながら進めるのが定石です。
-
最低限の許可設定(ポリシー)の作成
まずは診断の要件を満たす最低限のポリシー(nonce の利用や object-src 'none' など)を定義した、ベースとなる許可設定を用意する。 -
Report-Onlyモードでの運用(今回の実施内容)
ブロックは行わず、ポリシー違反があった場合にレポートのみを収集する。 -
レポートの分析と許可リストの修正
送られてくる違反レポートを解析し、正当な通信やスクリプトを特定して、ミドルウェアなどの許可リスト(Allow-list)に順次追加していく。 -
強制モードへの移行
レポートによる違反通知が完全になくなった段階で、はじめてブロックを有効化する。
今回は、このプロセスの中心となる「レポートの収集」を安全かつ確実に実現することを目標としました。
4. レポート収集の仕組み化
ブラウザから送られてくるCSP違反レポートは、report-uri(または report-to)に指定されたURLへJSON形式で送信されます。このレポートをどのように収集・分析するかについて、いくつかの候補を検討しました。
レポート収集の主な候補
-
開発環境で全ページの想定動作を行い、自分で収集する
開発者がブラウザのデベロッパーツール(Consoleパネル)を開きながら、サイト内の全ページ・全機能を操作して、違反が出ていないかを手動で確認する方法です。- メリット: 実装コストがゼロで、開発中に即座に修正できる。
- 懸念点: ページ数が多い場合に網羅するのが困難。また、ユーザーのブラウザ環境や、特定の条件下でのみ読み込まれる外部スクリプト(広告や計測タグなど)の違反を見落とすリスクがある。
-
自前でレポート受け取り用のエンドポイントを構築する
サーバー側にレポート(JSON)を受け取るAPIを作成し、ログファイルやデータベースに保存する方法です。- メリット: 外部サービスに頼らず、自社内でデータを完全に管理できる。
- 懸念点: APIの開発・運用工数がかかる。また、大量の違反レポートが送られてきた際の負荷対策も考慮する必要がある。
-
CSP専用のレポート収集サービスを利用する
「report-uri.com」のような、レポートの収集・可視化に特化した外部サービスを利用する方法です。- メリット: グラフ化やフィルタリングなど、分析機能が非常に充実している。
- 懸念点: 管理すべきツールが一つ増える。
-
既存の監視ツール(Sentryなど)を活用する
プロジェクトですでに導入しているエラー監視ツールを活用し、レポートを集約する方法です。
Sentryを選んだ理由
検討の結果、今回はSentryを採用することにしました。
「全ページの手動確認」は開発時の基本ですが、「実際の運用環境で、予期せぬスクリプトがブロックされていないか」を網羅的に把握するには、自動でレポートを集める仕組みが不可欠です。
Sentryを選んだ具体的な理由は以下の通りです。
- 導入済みのリソースを活用できる: 新たなサービス契約やインフラ構築が不要。
- 「Security Reports」機能: 専用のURLが標準で用意されており、設定するだけでレポート収集が開始できる。
- 分析の効率化: どのページの、どの行にある、どのドメインが違反したのかがダッシュボードで整理されるため、手動でログを追うよりも圧倒的に効率が良い。
ここからは、実際に作成したLaravelのミドルウェアの実装内容を紹介します。
5. 実装:Sentryへのレポート送信設定
具体的に以下の手順で実装を行いました。
Sentry側でのURL取得
Sentryの公式ドキュメントには、設定を簡単にするための非常に便利な機能があります。
Sentry公式ドキュメント: Security Policy Reporting
- Sentryにログインした状態で、上記の公式ドキュメントを開きます。
- ページ内のコード例にあるプルダウン(Search Project)をクリックし、自分のプロジェクトを選択します。
- すると、ドキュメント内のコードに含まれる
report-uriやReport-ToのURLが、自分のプロジェクト専用のURLに自動的に書き換わります。
管理画面からURLを探す手間がなく、コピー&ペーストするだけで設定が完了するため、設定ミスを防ぐことができます。
アプリケーション(Laravel)側の実装
取得したURLを使い、Laravelのミドルウェアを作成しました。
導入の第一歩として、あえて外部サービスのドメインを一切許可しない「最小限の構成」にしています。これにより、Sentryに「どのドメインの許可が必要か」を確実に報告させ、現状を洗い出すことが狙いです。
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\View;
class ApplyContentSecurityPolicy
{
public function handle(Request $request, Closure $next)
{
// 1. リクエストごとにNonce(一度限りのランダム文字列)を生成
$nonce = base64_encode(random_bytes(16));
// 2. 全てのBladeテンプレートで $cspNonce として使えるように共有
View::share('cspNonce', $nonce);
$response = $next($request);
// 指定した環境でのみ有効化
if (app()->environment('production', 'staging', 'dev')) {
// 公式ドキュメントで取得したプロジェクト専用のURL
$sentryUrl = "https://o00000.ingest.sentry.io/api/000000/security/?sentry_key=your_key";
$policy = [
// 基本は自分のドメイン('self')のみ許可
"default-src 'self'",
"script-src " . implode(' ', [
"'nonce-{$nonce}'",
"'strict-dynamic'",
"'self'",
"https:",
]),
"connect-src 'self'",
"frame-src 'self'",
// ASVS 3.4.3 要件の必須項目
"object-src 'none'",
"base-uri 'none'",
// Sentry へのレポート送信先
"report-uri {$sentryUrl}",
];
// 既存機能を壊さないよう「Report-Only」ヘッダとして出力
$response->headers->set('Content-Security-Policy-Report-Only', implode('; ', $policy));
// 最新ブラウザ用の Report-To ヘッダー
$response->headers->set('Report-To', json_encode([
'group' => 'csp-endpoint',
'max_age' => 10886400,
'endpoints' => [['url' => $sentryUrl]],
]));
}
return $response;
}
}
実装のポイント
-
「あえて許可しない」設定:
connect-srcやframe-srcを'self'(自サーバー)のみに限定しています。これにより、決済のStripeやリアルタイム通信のPusher、Google計測タグなどが「違反レポート」としてSentryに次々と上がってくるようになります。 -
Nonceの共有: HTML(Blade)側では
<script nonce="{{ $cspNonce }}">と記述します。これがないスクリプトもレポート対象となります。 -
安全なデバッグ:
Content-Security-Policy-Report-Onlyヘッダを使用しているため、上記のように厳しい制限を書いても、実際のユーザー画面でエラーが起きたり機能が止まったりすることはありません。
6. 現在の状況:Sentryへのレポート送信の確認
今回の実装により、レスポンスヘッダに Content-Security-Policy-Report-Only が付与され、違反内容がSentryへ送信される状態になりました。
Sentryでの検知状況
実際にSentryのダッシュボードを確認すると、詳細な違反レポートが蓄積されています。
この画面では、例えば graph.facebook.com(Facebook Graph API)への通信が、connect-src のポリシーに違反していることが具体的に示されています。このように、「どのページの、どのドメインが、どのルールでブロック対象になるのか」を客観的なデータとして特定できるようになりました。
実務での気づき:開発環境と本番環境の差
また、この仕組みを運用し始めて、実務ならではの重要な気づきがありました。それは、「開発環境でのテストだけでは、許可すべきドメインを網羅するのは極めて難しい」という点です。
-
開発環境での状況:
開発メンバーによる限定的なアクセスのため、主要な設定だけで違反が出なくなり、一見するとポリシーが完成したように見えていました。 -
本番環境適用後の変化:
しかし、本番環境でレポート収集を開始した途端、開発環境では一度も検知されなかった Google Analytics や Google Tag Manager 関連のドメインが次々と報告されました。
実際のユーザーがアクセスすることで初めて「本来許可すべき通信」の実態が浮き彫りになりました。
また、今回は触れませんが、本番環境ではGoogleの国別ドメイン(.adや.aeなど)も膨大な数が検知されています。これらの国別ドメインへの具体的な対応方法については、また別の機会に記事にまとめたいと考えています。
今後の展望
現在は本番環境で「許可リストの精度」を高めている段階です。今後は、Sentryに届くレポートを元にミドルウェアの修正を重ねていきます。
このサイクルを繰り返し、Sentryに違反レポートが届かなくなった段階で、はじめて Report-Only を外した「本運用(強制ブロック)」へ移行させる予定です。

7. まとめ
セキュリティ診断でCSPの不備を指摘されたことをきっかけに、以下の流れで対応を進めました。
- CSPの調査:いきなり制限をかけるリスクを理解し、まずはレポート収集から始める手順(Report-Only)を確認した。
- 収集方法の検討:運用コストを抑えるため、既存のSentryをレポートの受け皿に採用した。
- 実装:日本語情報の少ないSentryへのレポート設定について、公式ドキュメントを活用してURLを取得し、Laravelのミドルウェアに組み込んだ。
CSPの導入は設定一つでサイトの動作を止めてしまうリスクがありますが、Sentryを活用して「現状を可視化する仕組み」を作ったことで、安全に対応を進める準備が整いました。
今後は集まったレポートを一つずつ確認し、正当なスクリプトへのnonce付与や、外部ドメインの許可設定を地道に進めていく予定です。
同様の指摘を受け、既存システムへのCSP導入を検討している方の参考になれば幸いです。
お知らせ
技術ブログを週1〜2本更新中、ソーイをフォローして最新記事をチェック!
https://qiita.com/organizations/sewii


