Skip to content

fix(csp): close the wss allow-list gap and stop the Sentry report flood - #2519

Merged
Hugo0 merged 8 commits into
mainfrom
fix/csp-wss-allowlist-and-report-noise
Jul 28, 2026
Merged

fix(csp): close the wss allow-list gap and stop the Sentry report flood#2519
Hugo0 merged 8 commits into
mainfrom
fix/csp-wss-allowlist-and-report-noise

Conversation

@Hugo0

@Hugo0 Hugo0 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Two problems, one root area: the report-only CSP is flooding Sentry, and it has a genuine allow-list gap that would break production the moment the policy is enforced.

~70k CSP events across ~1.8k users in 7 days made the 20 newest peanut-ui issues all CSP reports. 99.3% of that volume is production (peanut.me), not staging — the "culprit" shown in Sentry's issue list is only the latest event's URL, which is why staging looks over-represented at a glance. Staging is 0.49%.

a) How CSP reports reach Sentry

Not the browser SDK. next.config.js emitted report-uri / report-to pointing straight at Sentry's /api/<projectId>/security/ ingest endpoint, derived from the public DSN. The browser POSTs violations itself, so these events never pass through Sentry.initbeforeSend and the existing shouldIgnoreError helper are structurally unable to see them. (Worth stating plainly, since "filter it in shouldIgnoreError" is the obvious first instinct and it cannot work.)

The only place to filter is an endpoint we own, so report-uri / Reporting-Endpoints now point at a same-origin collector, POST /api/csp-report, which forwards to the exact same Sentry endpoint minus the noise.

b) The real gap — wss://

connect-src had https://api.peanut.me and https://*.peanut.me. CSP treats wss: as a scheme distinct from https:, so neither authorized the charges websocket (NEXT_PUBLIC_PEANUT_WS_URL) that every logged-in session opens. It is 84% of the violations still firing (2,377 of 2,834 in the last 24h; 6,177 events / 1,780 users over 7d, still escalating) and it would have broken the socket for every user on promotion to enforcing.

Everything added below is evidence-backed — either a live Sentry violation or a confirmed call site. Nothing speculative.

Directive Added Why
connect-src wss://api.peanut.me, wss://*.peanut.me The charges websocket. The headline gap.
connect-src wss://*.sumsub.com Identical scheme trap. Sumsub's WebSDK opens a socket for liveness/video-ident. Silent in the reports only because liveness is a rare path — it would still break KYC under enforcement.
connect-src https://api.coingecko.com TransactionDetailsReceipt.tsx:162 token-metadata fallback. Only the two image CDNs were allowed.
connect-src https://www.googletagmanager.com Present in script-src but not connect-src; gtag XHRs back to it. 44 events / 21 users per day.
connect-src https://www.google-analytics.comhttps://*.google-analytics.com GA4 shards collection by region (region1.…). Same sharding is already observed on analytics.google.com. The wildcard covers www., so this is a simplification, not a widening.
frame-src https://*.bridge.xyz Would dead-end KYC. BridgeTosStepIframeWrapper renders a Bridge-hosted ToS URL (compliance.bridge.xyz, 18 events / 11 users). Wildcarded like the existing *.sumsub.com because the URL is chosen by the provider at runtime, not by us.
frame-src https://client.crisp.chathttps://*.crisp.chat assets.crisp.chat was blocked; matches what connect-src already does.

Verified NOT needed (so the policy isn't widened for nothing): WalletConnect/Reown is not wired in at all — wagmi.config.tsx passes no connectors, no @walletconnect/@reown dependency, and NEXT_PUBLIC_WC_PROJECT_ID has zero references. PostHog, Sentry and the ZeroDev passkey server are all same-origin via existing rewrites. Every chain RPC (ours and viem's per-chain defaults for all 10 configured chains) is already covered.

c) Stopping the flood

The noise is overwhelmingly repetition, not variety: Sentry groups CSP issues by directive + blocked origin, so one missing entry produced 14,378 identical events. So the collector:

  1. Forwards the first sighting of each (directive, blocked-origin) group unconditionally — a genuinely new violation always creates its issue and fires its alert, so no future signal is hidden — then samples repeats at 1%.
  2. Drops browser-extension / browser-internal schemes. Note this is defense-in-depth, not the main lever: Sentry already drops that class server-side (zero chrome-extension:// reports have ever reached the project). Keeping it in code means the guarantee is ours rather than a Sentry project setting nobody remembers exists.

Deliberately not filtered, because they are the signal the report-only phase exists to collect: inline, eval, data:, blob: — these are what has to reach zero before script-src can be enforced.

Extension-injected violations that arrive over plain https (Google/Brave Translate's fonts.gstatic.com, a coupon extension's images.simplycodes.com, connect.facebook.net) are not allow-listed — they aren't ours. No scheme check can catch them, but the per-group de-duplication reduces each to ~1 event, which is why the generic mechanism beats a hand-maintained host denylist.

Projected effect: from ~2,800 events/day to roughly one event per distinct violation.

d) Base branch — and the companion action

Targeted main, because that is where the flood is (99.3% of volume) and this is a hotfix. Prod gets both the wss fix and the collector on merge.

A maindev back-merge is required and closes the staging side at the same time. dev is missing PR #2479's entire allow-list (chain RPC hosts, OneSignal, the Google hosts, ipapi, justaname) because that fix also went straight to main — so staging currently runs the old policy. The standard post-release back-merge carries #2479 and this PR to dev in one go; no second PR needed. Flagging it explicitly since it is the only thing standing between staging and the same gaps.

Risks

  • Reporting path changes owner. Reports now traverse our route instead of going directly to Sentry. Mitigated: the handler always returns 204 whatever happens (a non-2xx would make browsers retry and log console errors — a second, self-inflicted noise source), everything is wrapped in try/catch, and the forward has a 3s timeout. Worst case is losing violation reports, which are already report-only diagnostics.
  • Not a user-facing change. The enforcing header (frame-ancestors/object-src/base-uri) is untouched; the policy this PR edits is still Content-Security-Policy-Report-Only, so there is zero user impact today — the value is that enforcement later won't break the websocket, KYC, or the ToS iframe.
  • Cold starts reset the de-dup Set (per-serverless-instance, bounded at 500 groups), so an occasional extra first-sighting gets through. Same trade-off the health route's in-memory Discord cooldown already documents.
  • Adds serverless invocations proportional to violation volume — currently ~2.8k/day and falling steeply once the gaps close.

Design notes / accepted trade-offs

  • De-duplication mirrors Sentry's csp:v1 grouping, including its one special case. Sentry drops the blocked URI and keys on the keyword when script-src is violated by 'unsafe-inline' / 'unsafe-eval', raising two issues where a URI-based key sees one. An earlier revision of the group key collapsed them, so whichever arrived second would have been sampled away and its issue possibly never created — in exactly the category this policy exists to eliminate. Fixed and covered by tests.

  • The forward loop is capped at 20 per request. The endpoint is unauthenticated and a Reporting-API payload is an array. Sentry bills CSP reports against the error quota under a per-DSN-key rate limit, so an uncapped fan-out is a way to starve real exception reporting, not just a noise problem.

  • report-uri and Reporting-Endpoints are both root-relative. The Reporting API resolves an endpoint "with base URL set to response's url" (W3C Reporting §3.3), so a relative path lands on whichever origin served the page. An absolute URL would have to come from NEXT_PUBLIC_BASE_URL, which is unset on preview deploys — previews would have pointed at production's collector, cross-origin, and Chromium ignores report-uri whenever report-to is present, so those reports would be dropped rather than falling back. There is a comment in next.config.js saying not to "fix" this to an absolute URL.

  • Sentry attributes forwarded reports to the server, not the browser. Relay reads the user agent and IP from the forwarding request, and its own edge sets X-Sentry-Forwarded-For, which outranks anything we could send. So the "users affected" count on CSP issues after this lands is not a real user count — don't read it as one. The blocked URI, directive and document URL (the fields that actually drive the fix) are unaffected.

  • Why not shouldIgnoreError: structurally impossible, as above. The new filter is a pure function in src/utils/csp-report.utils.ts with 32 unit tests; the route is a thin transport over it.

  • sentryCspReportUri() moved rather than duplicated — out of next.config.js (CommonJS) into the collector as sentryCspIngestUrl(dsn), so the DSN→ingest-URL derivation still lives in exactly one place. next.config.js now just points at a static same-origin path.

  • Alternative considered and rejected: a dedicated Sentry project for CSP reports. Smaller diff, but it only moves the 70k events (still billed, still needing an out-of-repo project + env var in three environments) instead of eliminating them.

  • www.google.<ccTLD> long tail (~50 events/day across 45 ccTLDs) is left alone: CSP host-sources allow subdomain wildcards but not TLD wildcards, so it is literally inexpressible. Analytics-only, no functional risk. De-dup reduces it to ~45 one-off events.

Follow-ups (not in this PR — pre-existing, out of blast radius)

  • /dev/kyc-flows loads Mermaid from cdn.jsdelivr.net, and /dev/ routes are reachable in prod. Better fixed by dropping the CDN or blocking /dev/ in prod than by weakening script-src.
  • Allow-list entries that appear unused: dolarapi.com (only reached from 'use server' code), rpc.ankr.com (every Ankr URL is commented out), www.google.com (only window.open navigation; reCAPTCHA isn't wired).
  • NEXT_PUBLIC_ONESIGNAL_WEBHOOK is blank in .env.example and fetched from the service worker — its deployed origin couldn't be determined from the repo. If it points anywhere other than api.peanut.me it needs an entry. No violation has been reported for it.

QA

  • npx jest src/utils/__tests__/csp-report.utils.test.ts — 35/35 pass, covering both wire formats (application/csp-report and the Reporting-API reports+json batch), the group key, malformed payloads, and DSN parsing.
  • Rendered the actual header out of next.config.js and diffed the directives — report-uri /api/csp-report, report-to csp-endpoint, Reporting-Endpoints: csp-endpoint="/api/csp-report", and the new wss: / frame-src entries all present.
  • Full suite: 2,130 pass. Two suites fail to load locally on missing @capacitor/* packages (declared in package.json, absent from this machine's shared node_modules) — same pre-existing cause as the 10 local typecheck errors, none in changed files. CI installs them.

Post-deploy check (needed — a green CI cannot prove this). Sentry's Relay returns 200 before it parses the body and discards anything it can't read as an Invalid outcome, so a broken forward would look identical to a working one from our side. After deploy, confirm a CSP event actually lands in the peanut-ui project (and that wss://api.peanut.me stops appearing). That is the only real proof the pipeline works end to end.

Screenshots: N/A (no UI change — HTTP headers and a report collector).

Summary by CodeRabbit

  • New Features

    • Added a same-origin endpoint for collecting Content Security Policy violation reports.
    • Normalized, filtered, de-duplicated, sampled, and safely forwarded CSP events for monitoring.
    • Improved CSP configuration to report to a fixed collector and expanded WebSocket scheme coverage.
  • Bug Fixes

    • Reduced noise from browser/extension-origin reports while preserving meaningful CSP keyword signals.
  • Tests

    • Added comprehensive test coverage for CSP report parsing, ignore rules, grouping behavior, and forwarding URL construction.

The report-only CSP has been posting violations straight to Sentry's
security endpoint, which put ~70k events across ~1.8k users into
peanut-ui in a week and buried real signal. Those reports never pass
through the Sentry browser SDK, so no beforeSend/shouldIgnoreError
filter could ever have touched them — the only place to filter is an
endpoint we own, so reports now go to a same-origin collector that
forwards to Sentry minus the noise.

The noise is overwhelmingly repetition: Sentry groups CSP issues by
directive + blocked origin, so one missing allow-list entry generated
14k identical events. The collector forwards the first sighting of each
group unconditionally — a genuinely new violation still creates its
issue and fires its alert — and samples the repeats.

Separately, connect-src was missing wss://api.peanut.me. CSP treats
wss: as a scheme distinct from https:, so neither https://api.peanut.me
nor https://*.peanut.me authorized the charges websocket every logged-in
session opens. That single gap is 84% of the violations still firing,
and it would have broken the socket for every user the moment the
policy was promoted to enforcing. The Sumsub WebSDK's liveness socket
had the identical scheme trap, and the Bridge terms-of-service iframe
was missing from frame-src, which would have dead-ended KYC.
@cursor

cursor Bot commented Jul 27, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jul 28, 2026 9:42am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

CSP report-only headers now target a same-origin collector. The new route normalizes and filters legacy and Reporting API payloads, de-duplicates and samples violations, then forwards selected reports to Sentry. WebSocket CSP allowlists include wss:// variants.

Changes

CSP reporting pipeline

Layer / File(s) Summary
CSP reporting configuration
next.config.js
CSP reporting unconditionally uses /api/csp-report and a shared reporting group, while report-only connection sources include WebSocket schemes.
Report normalization and grouping
src/utils/csp-report.utils.ts, src/utils/__tests__/csp-report.utils.test.ts
Utilities normalize supported payload formats, filter extension-generated reports, create de-duplication keys, derive Sentry ingest URLs, and test these behaviors.
CSP ingestion route
src/app/api/csp-report/route.ts
The dynamic POST route validates content types, samples and caps forwarding, sends reports to Sentry with timeouts, suppresses errors, and returns 204.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant CSPReportRoute
  participant normalizeCspReports
  participant Sentry
  Browser->>CSPReportRoute: POST CSP report
  CSPReportRoute->>normalizeCspReports: normalize payload
  normalizeCspReports-->>CSPReportRoute: normalized reports
  CSPReportRoute->>Sentry: forward sampled reports
  CSPReportRoute-->>Browser: HTTP 204
Loading

Possibly related PRs

Suggested reviewers: kushagrasarathe

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes: closing the WSS CSP gap and reducing Sentry CSP report volume.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/csp-wss-allowlist-and-report-noise

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6284.43 → 6301.48 (+17.05)
Findings: +6 net (+6 new, -0 resolved)

🆕 New findings (6)

  • high complexity — src/utils/csp-report.utils.ts — CC 32, MI 60.73, SLOC 87
  • medium complexity — src/app/api/csp-report/route.ts — CC 15, MI 52.7, SLOC 78
  • low high-dlt — src/app/api/csp-report/route.ts:75 — POST: DLT 19 (calls 19 distinct functions — high context load)
  • low high-mdd — src/utils/csp-report.utils.ts:131 — cspReportGroupKey: MDD 16.9 (uses across many lines from declarations)
  • low high-mdd — src/app/api/csp-report/route.ts:75 — POST: MDD 14.0 (uses across many lines from declarations)
  • low high-mdd — src/utils/csp-report.utils.ts:75 — fromReportingApi: MDD 10.5 (uses across many lines from declarations)

📈 Painscore deltas (top movers)

File Before After Δ
src/app/api/csp-report/route.ts 0.0 10.2 +10.2
src/utils/csp-report.utils.ts 0.0 6.8 +6.8

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2231 ran, 0 failed, 0 skipped, 36.8s

📊 Coverage (unit)

metric %
statements 61.3%
branches 45.0%
functions 50.8%
lines 61.6%
⏱ 10 slowest test cases
time test
4.1s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.6s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/components/Global/Drawer/__tests__/Drawer.test.tsx › renders a visually hidden DialogTitle from accessibleTitle
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.2s src/utils/__tests__/url.utils.test.ts › uses the public BASE_URL in Capacitor, not the localhost WebView origin
0.2s src/utils/__tests__/demo-balance.test.ts › keeps a spent-down balance across a cold start within the TTL
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@Hugo0

Hugo0 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Chromium prefers report-to and ignores report-uri whenever both are
present, so an endpoint value it declines to parse would cost us every
Chromium report — silently, with no error anywhere to notice it by.
The spec says an endpoint URL is resolved against the document, but MDN
and Chrome's own docs both describe the value as absolute. An absolute
URL is valid under either reading, so stop betting on the ambiguity.
report-uri keeps the relative path, which is unambiguously a
URI-reference and stays correct on preview origins too.
Flagged by the code-analysis bot: the exported route handler had no
return type annotation.
…p fan-out

Two defects found reviewing the collector against Sentry's actual
ingest behaviour.

The group key was coarser than Sentry's csp:v1 grouping strategy, which
drops the blocked URI and keys on the keyword when script-src is
violated by 'unsafe-inline' or 'unsafe-eval'. Sentry raises two issues
there; the key collapsed them into one, so whichever arrived second
looked like a duplicate and was sampled away — its issue possibly never
created. Those two keywords are the top of this policy's "tighten before
enforcing" list, so that was exactly the signal the filter must not eat.

The forward loop was also uncapped. The endpoint is unauthenticated and
a Reporting-API payload is an array, so one POST could fan out
arbitrarily many events — and Sentry bills CSP reports against the
*error* quota under a per-key rate limit, making an uncapped fan-out a
way to starve real exception reporting rather than merely noisy.
Reverts the absolute Reporting-Endpoints URL from 2a5fda9. That change
was made on the strength of MDN describing the value as absolute, but
the Reporting API spec resolves an endpoint "with base URL set to
response's url" (W3C Reporting §3.3) — root-relative is valid and is
resolved against whichever origin served the page.

Relative is also strictly safer here. An absolute URL has to come from
NEXT_PUBLIC_BASE_URL, which is unset on preview deploys, so previews
would have pointed at production's collector — cross-origin, and
Chromium ignores report-uri whenever report-to is present, so those
reports would have been dropped rather than falling back.
@Hugo0

Hugo0 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/utils/csp-report.utils.ts (1)

123-150: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

directive === 'script-src' misses Chrome/Firefox's granular script-src-elem/script-src-attr reporting.

Per the CSP spec/CSPViolationReport.effectiveDirective, Chrome and Firefox report the specific sub-directive that was checked (script-src-elem for inline <script> blocks, script-src-attr for inline event handlers) rather than the general script-src, whenever those sub-directives aren't explicitly declared — which appears to be the case for this policy. Real-world CSP reports confirm this (e.g. Sentry issue reports commonly show "effectiveDirective": "script-src-elem" for inline-script violations). Only eval() violations remain reported under plain script-src.

That means the directive === 'script-src' keyword branch will rarely fire for unsafe-inline violations in Chrome/Firefox — it'll typically fall through to the blocked-uri-based key instead. The practical fallout is limited today because blocked-uri is the literal keyword (inline/eval), so inline and eval still end up in different groups by coincidence — but the code's documented intent ("mirrors how Sentry groups CSP issues") is less precise than the tests suggest, since every test here uses the idealized literal 'script-src' value rather than the value browsers actually send.

Consider matching the whole script-src* family instead of an exact match:

♻️ Proposed fix
-    if (directive === 'script-src') {
+    if (directive === 'script-src' || directive.startsWith('script-src-')) {
         for (const keyword of ['unsafe-inline', 'unsafe-eval']) {
             if (violated.includes(keyword)) return `${directive}|${keyword}`
         }
     }

It'd also be worth adding a test case with 'effective-directive': 'script-src-elem' to lock in the intended behavior, since none of the current tests exercise the value browsers actually emit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/csp-report.utils.ts` around lines 123 - 150, Update
cspReportGroupKey so the unsafe-inline/unsafe-eval keyword handling applies to
the entire script-src directive family, including script-src-elem and
script-src-attr, rather than only an exact script-src match. Preserve the
existing keyword-specific grouping and fallback behavior, and add coverage for a
script-src-elem report.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/api/csp-report/route.ts`:
- Around line 82-85: Move the MAX_FORWARDS_PER_REQUEST cap in the forwardable
pipeline so it is applied before shouldForward(cspReportGroupKey(report)) runs,
while preserving the shouldIgnoreCspReport filter and existing ordering. Ensure
shouldForward only mutates seenGroups for reports that remain within the
forwarding candidate limit.

---

Nitpick comments:
In `@src/utils/csp-report.utils.ts`:
- Around line 123-150: Update cspReportGroupKey so the unsafe-inline/unsafe-eval
keyword handling applies to the entire script-src directive family, including
script-src-elem and script-src-attr, rather than only an exact script-src match.
Preserve the existing keyword-specific grouping and fallback behavior, and add
coverage for a script-src-elem report.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 60663317-03c3-4ec5-badb-841c630b09a3

📥 Commits

Reviewing files that changed from the base of the PR and between e52eb69 and 27bdf7d.

📒 Files selected for processing (4)
  • next.config.js
  • src/app/api/csp-report/route.ts
  • src/utils/__tests__/csp-report.utils.test.ts
  • src/utils/csp-report.utils.ts

Comment thread src/app/api/csp-report/route.ts Outdated
…amily

Both from CodeRabbit review.

The fan-out cap ran after the de-duplication filter, but shouldForward
records every group it inspects as seen. Truncating afterwards meant
groups past the cap were marked seen without ever being sent, so their
real first sighting was lost and later repeats were sampled away as
duplicates — the exact failure the first-sighting guarantee exists to
prevent. Cap the candidates first, then de-duplicate.

The keyword grouping also only matched a bare `script-src`, but browsers
report the sub-directive they actually checked: inline <script> arrives
as script-src-elem and inline handlers as script-src-attr, with only
eval left under plain script-src. script-src-elem is the second-largest
directive in the current reports, so this branch was mostly not firing.
Numbers predated the Sentry query: ~70k events / ~1.8k users over 7d,
not 65k/1.3k over 4d.
…ty scripts

Our own script-src contains 'unsafe-inline' and 'unsafe-eval', and
Firefox/Safari echo the entire source list back in violated-directive.
The keyword branch matched that text without checking the violation was
local, so EVERY script-src report — including a genuinely blocked
third-party script — keyed as the ubiquitous inline violation and got
sampled away at 1%. That is the exact issue-loss the de-duplication was
written to prevent, in the directive that matters most for XSS.

Gate the branch on a local blocked-uri and match the quoted keyword, as
Sentry's csp:v1 strategy does. Normalize the directive to its first
token too: Firefox omits effective-directive, so keys were embedding the
whole policy and churning on every edit.

Also, forwarding through our own route made Sentry attribute every event
to one Vercel egress IP running undici, deleting the browser/user
dimension these reports are read for — pass the original User-Agent and
X-Forwarded-For through. De-duplicate within a batch so repeats of one
violation cannot crowd a new group out of the per-request cap, and evict
the oldest seen group instead of dumping all 500 at the ceiling.
@Hugo0
Hugo0 marked this pull request as ready for review July 28, 2026 09:44
@Hugo0

Hugo0 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Hugo0

Hugo0 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 19 minutes.

@Hugo0

Hugo0 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@Hugo0
Hugo0 merged commit dbcf998 into main Jul 28, 2026
25 of 28 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/api/csp-report/route.ts`:
- Around line 49-57: Add request-level throttling to the CSP report handler
using the existing in-memory cooldown or rate-limit pattern, keyed per client
IP, before any Sentry forwarding occurs. Keep MAX_FORWARDS_PER_REQUEST for
batch-size limiting and ensure repeated unauthenticated requests from the same
IP are rejected or suppressed during the limiter window.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ca54d986-69b3-4fc8-9bfc-7726954ac606

📥 Commits

Reviewing files that changed from the base of the PR and between e52eb69 and 88d271f.

📒 Files selected for processing (4)
  • next.config.js
  • src/app/api/csp-report/route.ts
  • src/utils/__tests__/csp-report.utils.test.ts
  • src/utils/csp-report.utils.ts

Comment thread src/app/api/csp-report/route.ts
innolope-dev added a commit that referenced this pull request Jul 29, 2026
…s, PIX exit, verification tasks

Cherry-picks the open native-facing work onto the release branch:

- KYC now routes every native entry point through the Sumsub Cordova SDK
  instead of the WebSDK (peanut-ui #2562) — the one change here that genuinely
  requires a new binary rather than an OTA.
- Deferred deep linking survives the store install, so a link followed before
  install lands on its destination on first launch (#2560, TASK-20772).
- PIX deposit "Done" exits to home from both the completed and processing
  states instead of dropping the user into a new deposit (#2548).
- Pending Bridge verification tasks card on home, dismissible and resurfaced
  under Unlocked regions (#2549).
- Native demo/passkey follow-ups: the awaited token clear in clearAuthState
  plus the two callers it changed (#2517 — the rest shipped in 1.0.40).
- Documents left running on a superseded deployment now reload (#2563).
- Android safe zone sized from natively measured insets.

Not carried: the CSP allow-list gaps (#2564). It builds on the #2519 collector
rework — a /api/csp-report route and ten commits that are not on this branch —
and the policy is report-only, so the gaps cost report fidelity, not behaviour.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant