Report URI サービスの有料化に伴い CSP レポートのエンドポイントを自作
当サイトでは Content Security Policy (CSP) のレポート収集に Report URI(report-uri.com
) のサービスを長年使ってきたのですが、Report URI の作成者のブログ記事 Report URI: Simplifying pricing and changes to free accounts(scotthelme.co.uk
) にあるように2025年2月1日をもって無料プランが終了することになりました。
無料プラン終了そのものに対しては記事にあるようにやむを得ないことであり、現行の無料ユーザー向けに格安プランが用意されるといった対応もなされているためその運営判断に不満はないのですが、いかんせん私自身がこの個人サイトの CSP レポートを普段はほとんど活用していないため、月額 9.99 ドルといえども負担は厳しいと思った次第です。そもそもユーザー情報の保管とか一切していない当サイトに CSP を設定しているのはセキュリティ施策というよりは単なる技術的興味からのことであり、これを機に CSP ヘッダーを設定するだけでなくレポート収集のエンドポイントも自作しようと考えた次第です。
ところで CSP のレポートに使われるディレクティブや Reporting API に関係する HTTP ヘッダーはこれまで何回か仕様が大きく変わっており、送信される JSON 形式も変化しています。またブラウザによってどの時点の仕様を実装しているかに差異があったり、ブラウザの設定(アドオン)によってはサイト制作側の意図しないエラーがレポートされたりするなど状況は複雑なため、最新仕様を見るだけではなく実際に送られてくるエラーを見ながらの調整が必要でした。
- この記事で書くこと
- ブラウザごとの CSP レポートの JSON 形式の違い
- ブラウザのアドオンなどによる変わったレポートへの対処
- この記事で書かないこと
- CSP の基礎的な解説
- CSP や Reporting API 仕様の変遷(ざっくりした流れには触れるものの詳細には踏み入らない)
- 蓄積されたエラーレポートの活用方法
- Reporting API(v0)で定められていた
Report-To
ヘッダー(今となっては考慮する必要がないため)
ソースコード
今回作成したエンドポイントのソースコードは report.w0s.jp
で公開しており、主要な処理は /node/src/controller/csp.ts
に書かれています。
CSP 関連のレスポンスヘッダー
CSP レポートを送るためには Content-Security-Policy
ないし Content-Security-Policy-Report-Only
ヘッダーに専用のディレクティブを指定する必要があります。これはもともと CSP 専用の report-uri
ディレクティブ(developer.mozilla.org
)であったものが、CSP 以外を含めた様々なレポートに対応した Reporting API を利用するように変更され、今は report-to
ディレクティブ(developer.mozilla.org
) と Reporting-Endpoints
ヘッダー(developer.mozilla.org
)を組み合わせる方式となっています。
Chrome と Safari は現行仕様に対応しているのですが、Firefox は最新の 136 Nightly においても古い report-uri
ディレクティブしかサポートしていないため、実際は両方を指定する必要があります。
Content-Security-Policy: default-src 'self'; report-uri https://example.com/endpoint; report-to csp
Reporting-Endpoints: csp="https://example.com/endpoint"
なお Reporting API に対応したブラウザであっても report-uri
ディレクティブのサポートを取り止めたわけではないため、たとえ report-to
ディレクティブを書かなくてもレポートの送信自体は行われます。ただし report-uri
ディレクティブではレポートが即時配信されるのに対し、Reporting API ではレポートのリストがいったんキューに入れられ、ブラウザが送信タイミングを制御することとされている違いがあります[1]。すなわち、とくに複数のレポートが送信される際におけるパフォーマンス向上のメリットがあるので、多少レスポンスヘッダーが長くなるにせよ最新仕様へ準拠したほうがよいでしょう。
3種類の JSON 形式
CSP ヘッダーは前述のとおり新旧2種類に対応した記述を行ったのですが、実際にブラウザが送信する JSON 形式は Firefox, Safari, Chrome でそれぞれ異なるため、エンドポイント側では3種類のフォーマットに対応する必要があります。
Firefox 136(report-uri
ディレクティブ)
Firefox は report-to
ディレクティブに対応していないため、report-uri
ディレクティブで指定したエンドポイントに JSON が POST されます。
Content-Type
は application/csp-report
であり、CSP Level 3 仕様の 5.3. Obtain the deprecated serialization of violation で JSON 形式が規定されています。便宜的に TypeScript フォーマットで表現するとこうなります。
{
'csp-report': {
'document-uri': string;
referrer?: string;
'blocked-uri'?: string;
'effective-directive': string;
'violated-directive': string; // `effective-directive` の旧名称(同じ値)
'original-policy': string;
disposition?: 'enforce' | 'report';
'status-code': number;
'script-sample'?: string;
'source-file'?: string;
'line-number'?: number;
'column-number'?: number;
};
}
なお、仕様では source-file
, line-number
, column-number
の3つのプロパティのみが省略されうるとあるのですが、実際に Firefox から送信されてくるデータを見ると他のプロパティも存在しないケースがあるため、上記は現実の状況を踏まえたものとしています。
Safari 18.2(report-to
ディレクティブ + Reporting-Endpoints
ヘッダー)
Safari は Reporting API に対応しているのですが、送られる JSON 形式はやや古い仕様のものとなっています。詳しくは追っていないのですが、2018年9月25日版の 5.1. Interface ReportingObserver にある Report
インターフェースと同様に見えます(実際に Safari がその時期の仕様を元に実装したのかどうかは未確認です)。
同様に TypeScript フォーマットで表現するとこうなります。
{
type: `csp-violation`;
url: string;
body: {
documentURL: string;
referrer: string;
blockedURL: string;
effectiveDirective: string;
originalPolicy: string;
sourceFile: string;
sample: string;
disposition: "enforce" | "report";
statusCode: number;
lineNumber: number;
columnNumber: number;
}
}
また Reporting API では Content-Type
は application/reports+json
と定められているのですが、Safari は Firefox と同じく application/csp-report
で送ってくることに注意する必要があります。
Chrome(report-to
ディレクティブ + Reporting-Endpoints
ヘッダー)
Chrome は Reporting API の最新仕様に準拠しています。
Content-Type
は application/reports+json
であり、Reporting API 仕様の 2.4. Serialize Reports および CSP Level 3 仕様の 5. Reporting で規定されているとおりの JSON が送られます。TypeScript フォーマットで表現するとこうなります。
[
{
age: number;
type: string;
url: string;
user_agent: string;
body: {
documentURL: string;
referrer?: string;
blockedURL?: string;
effectiveDirective: string;
originalPolicy: string;
sourceFile?: string;
sample?: string;
disposition: 'enforce' | 'report';
statusCode: number;
lineNumber?: number;
columnNumber?: number;
};
}
]
Safari と似ているものの、配列形式となっているのが特徴です。実際には以下のように CSP 以外のレポートも含まれるケースがあるので、必要に応じてフィルタリング処理を掛けましょう。
[
{
"age": 0,
"type": "csp-violation",
"url": "http://example.com/",
"user_agent": "Mozilla/5.0 ...",
"body": {
...
}
},
{
"age": 999,
"type": "another-type",
"url": "http://example.com/",
"user_agent": "Mozilla/5.0 ...",
"body": {
...
}
}
]
ブラウザのアドオンにより発生するエラーレポート
このように Web サイト側では CSP ヘッダーのディレクティブを2種類記述し、エンドポイント側では JSON 形式を3種類受け入れるように対応し、あとは送られてきたデータをデータベース等に保存する処理を作れば最低限の機能としては完成します。
しかしそれだけでは意図しないエラーレポートが大量に蓄積されてしまうため、実際に送られてきたデータを見ながらの調整が必要となります。以下に当サイトにおいて対応したものを記載します。あくまで当サイトで頻出したエラーレポートであり、他のサイトでは状況が異なる可能性はあるので参考程度に……。
-
blockedURL: 'data', effectiveDirective: 'media-src'
- 当サイトでは画像ファイルなどに "data" URL スキームは使用していない(HTTP/1.1 時代は小さなアイコンに使用していたこともあった)ため、ユーザー環境に起因して発生するエラーと言える
- Firefox にアドオン NoScript(
noscript.net
)を入れていると発生することを確認した - NoScript が不要になる世界が理想だが、世の中には「Cookie 無効かつスクリプト有効」の設定では閲覧できないサイトがあり、スクリプト設定を無効にすることではじめて閲覧可能になる状況が存在するので、ドメインごとにスクリプト設定を切り換えられるアドオンがないと生きていけない
media-src
にdata:
を追加して対応した
-
blockedURL: 'inline', effectiveDirective: 'script-src-elem', sourceFile: 'moz-extension'
- 当サイトではインライン JavaScript(
developer.mozilla.org
) は使用していないため、これもユーザー環境に起因して発生するエラーと言える - Firefox にアドオン Violentmonkey(
violentmonkey.github.io
)を入れていると発生することを確認した script-src-elem
に'unsafe-inline'
を追加して対応した- セキュリティ観点では
'unsafe-inline'
の指定は避けるべきとされているが、一方で当サイトのようにそこまでセンシティブでないサイトではユーザースタイルシートやユーザースクリプトの活用を妨げたくはないため、リスクを踏まえたうえで許容する
- 当サイトではインライン JavaScript(
-
blockedURL: 'trusted-types-policy', effectiveDirective: 'trusted-types', sourceFile: 'chrome-extension', sample: 'dompurify'
- Chrome のアドオン?
sample
の値からしてセキュリティ関係のアドオンだろうが詳細は把握していない- そこそこ頻繁にレポート発生するので著名なアドオンなのだろうか
trusted-types
にdompurify
を追加して対応した
その他のエラーレポート
対応するほどの頻度ではないものの、ちょっと変わったエラーレポートをいくつか紹介します。
-
*-src-elem
未対応環境- 当サイトではスタイルシートとスクリプトの設定は
script-src-elem
,style-src-elem
を記述しているが、これらは CSP Level 3 で登場した後発のディレクティブであり、古いブラウザは対応していない - フォールバックとして
script-src
,style-src
も指定すれば解消するかもしれないが、ただでさえ長大になりがちなヘッダー値をこれ以上長くしたくない(本当に解消するのか検証もしていない)
- 当サイトではスタイルシートとスクリプトの設定は
-
effectiveDirective: 'fenced-frame-src'
-
<fencedframe>
要素(wicg.github.io
)に関連するディレクティブだが、当サイトではその要素は使っていない - リクエストに
blockedURL
やsample
が付いていないので詳細はよく分からない
-
-
effectiveDirective: 'trusted-types', sample: 'default2'
- なんだその投げやりな名前は
脚注
-
1.
Chrome は最大1分の遅延で送信される(
developer.chrome.com
)とされています。 ↩ 戻る