Skip to content

Commit b99905e

Browse files
jdevalkclaude
andauthored
feat(admin): capture and surface the CSP report sample/detail (#69)
The /admin/stats Reports table showed type, path, directive, blocked, and disposition but not the one field that names the offending Trusted Types policy (or the blocked inline snippet): the CSP report's `sample`. reports.ts only populated blob6 from `message`/`reason`, which CSP violation bodies never carry — so the detail was being dropped at write time. Pick `sample` (Reporting API) / `script-sample` (legacy csp-report) as a fallback, and add a "detail" column to the dashboard's recent-reports table so the policy name shows up. Privacy policy updated to disclose the new field. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d188519 commit b99905e

3 files changed

Lines changed: 26 additions & 9 deletions

File tree

functions/admin/stats.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export const onRequest: PagesFunction<Env> = async (context) => {
181181
GROUP BY directive ORDER BY count DESC LIMIT 50
182182
`,
183183
report_recent: `
184-
SELECT timestamp AS time, index1 AS type, blob2 AS path, blob3 AS directive, blob4 AS blocked, blob5 AS disposition
184+
SELECT timestamp AS time, index1 AS type, blob2 AS path, blob3 AS directive, blob4 AS blocked, blob5 AS disposition, blob6 AS detail
185185
FROM ${REPORT}
186186
WHERE ${REPORT_FIRST_PARTY} AND timestamp > NOW() - INTERVAL '30' DAY
187187
ORDER BY time DESC LIMIT 100
@@ -853,13 +853,22 @@ function renderDashboard(results: QueryResults, errors: QueryErrors): string {
853853
</div>
854854
<div id="report-recent-table">
855855
${renderTable(
856-
["time", "type", "path", "directive", "blocked", "disposition"],
856+
[
857+
"time",
858+
"type",
859+
"path",
860+
"directive",
861+
"blocked",
862+
"disposition",
863+
"detail",
864+
],
857865
rowsOrEmpty(results.report_recent),
858866
{
859867
formatters: {
860868
time: (v) => esc(String(v).slice(5, 16)),
861869
path: (v) => `<span class="path">${esc(v)}</span>`,
862870
blocked: (v) => `<span class="path">${esc(v)}</span>`,
871+
detail: (v) => `<span class="path">${esc(v)}</span>`,
863872
},
864873
},
865874
)}

functions/reports.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,14 @@ function writeReport(
124124
pick(body, "blockedURL", "blocked-uri", "sourceFile", "source-file"),
125125
);
126126
const disposition = trunc(pick(body, "disposition"), 16);
127-
const message = trunc(pick(body, "message", "reason"));
127+
// Deprecation/intervention/crash reports carry a human `message`/`reason`.
128+
// CSP violations don't — their detail rides in `sample` (camelCase, Reporting
129+
// API) or `script-sample` (kebab, legacy csp-report): the rejected Trusted
130+
// Types policy name, or the first ~40 chars of the blocked inline content.
131+
// Browsers cap `sample` themselves; keep whichever field is present.
132+
const message = trunc(
133+
pick(body, "message", "reason", "sample", "script-sample"),
134+
);
128135
const ua = trunc(r.user_agent || reqUA, 200);
129136

130137
dataset.writeDataPoint({
@@ -134,7 +141,7 @@ function writeReport(
134141
directive, // blob3
135142
blocked, // blob4
136143
disposition, // blob5
137-
message, // blob6
144+
message, // blob6 — message/reason, or CSP sample (policy name / blocked snippet)
138145
country, // blob7
139146
ua, // blob8
140147
],

src/pages/privacy.astro

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import BaseLayout from "~/layouts/BaseLayout.astro";
33
import Breadcrumbs from "~/components/Breadcrumbs.astro";
44
5-
const updated = "2026-06-16";
5+
const updated = "2026-06-29";
66
---
77

88
<BaseLayout
@@ -165,10 +165,11 @@ const updated = "2026-06-16";
165165
>, so your browser may POST a structured report when a page violates a
166166
security policy (for example a Content Security Policy block) or uses
167167
a deprecated browser feature. We record the report type, the path it
168-
occurred on, the directive or feature involved, and the User-Agent
169-
(truncated). This fires only on a policy violation — never on an
170-
ordinary page view — and carries no cookie, no identifier, and no IP
171-
address.
168+
occurred on, the directive or feature involved, a short detail sample
169+
the browser provides (such as the rejected policy name or a truncated
170+
snippet of the blocked content), and the User-Agent (truncated). This
171+
fires only on a policy violation — never on an ordinary page view —
172+
and carries no cookie, no identifier, and no IP address.
172173
</li>
173174
</ul>
174175
<p>

0 commit comments

Comments
 (0)