Skip to content

feat(security): report-only CSP with a script-src allow-list - #2462

Merged
kushagrasarathe merged 3 commits into
mainfrom
feat/csp-report-only
Jul 22, 2026
Merged

feat(security): report-only CSP with a script-src allow-list#2462
kushagrasarathe merged 3 commits into
mainfrom
feat/csp-report-only

Conversation

@innolope-dev

@innolope-dev innolope-dev commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Why

peanut-ui has no script-src today — the only CSP anywhere is frame-ancestors. That means an XSS anywhere in the app runs unconstrained and can exfiltrate to any origin it likes. Combined with a JS-readable session token, one injection is a full account takeover.

This is item E of the session-security series. Companion PRs: the native app lock (#2461) and, on the API, session lifetime bounds and step-up auth.

Heads up for reviewers: SECURITY-DEFERRED-ISSUES.md records the audit's UI fixes as already shipped, referencing UI commit 110dea71d. That commit does not exist in this repo and there is no vercel.json — the header work never landed. The doc's premise that the policy belongs in vercel.json is also wrong: only the native build is output: 'export', so next.config.js headers() works fine for web, which is what this PR uses.

What

  • Report-only Content-Security-Policy-Report-Only with a full first-draft allow-list — script-src, connect-src, style-src, img-src, font-src, frame-src, worker-src, form-action.
  • Reports go to Sentry via a report-uri derived from NEXT_PUBLIC_SENTRY_DSN. No DSN → the policy still ships, just without reporting (better than a malformed directive).
  • Two directives added to the enforcing policy now: object-src 'none' and base-uri 'self'. The app embeds no plugins and sets no <base>, so neither can break a working page.

Nothing in the report-only policy is enforced, so this PR cannot break the app. That's the point — enforcing a guessed allow-list would blank it.

Rollout

  1. Merge; watch Sentry CSP reports across web and native for 1–2 weeks.
  2. Widen until legitimate traffic stops reporting.
  3. Promote to the enforcing header.

Known-loose, to tighten before promotion

  • 'unsafe-inline' + 'unsafe-eval' in script-src — Next's inline bootstrap and the wallet SDKs need them today. Moving to nonces is its own change, and until it happens script-src limits where script can come from but not inline injection.
  • connect-src can't enumerate every chain RPC (env-driven, varies by network). The report stream is what completes that list.

Testing

Rendered both headers out of the real config and asserted the Sentry report-uri derivation, plus the DSN-absent fallback. Not covered by an automated test — next.config.js isn't in the jest project.

Follow-up not in this PR

Strict-Transport-Security is still missing and was part of the same lost audit branch. Left out to keep this reviewable; worth a one-line PR.

Summary by CodeRabbit

  • Security
    • Strengthened browser security policies by restricting embedded content and tightening document base URL rules.
    • Expanded Content Security Policy coverage to include additional directive protections.
    • Added a non-blocking Content Security Policy “Report-Only” header to surface CSP violation events for monitoring, with optional reporting endpoints derived from existing monitoring configuration.

The app ships no script-src today, so an XSS anywhere runs unconstrained
and can exfiltrate to any origin. Enforcing a guessed allow-list would
blank the app, so this ships REPORT-ONLY: it collects violations from
real traffic until the list is known complete, then gets promoted to the
enforcing header.

Reports go to Sentry via a report-uri derived from the browser DSN; the
policy still ships (without reporting) when the DSN is absent.

object-src 'none' and base-uri 'self' are added to the ENFORCING policy
now — the app embeds no plugins and sets no <base>, so neither can break
a working page.

Known-loose and to be tightened before promotion: 'unsafe-inline' and
'unsafe-eval' in script-src (Next's bootstrap and the wallet SDKs), and
a connect-src that cannot enumerate every env-driven chain RPC.
@innolope-dev innolope-dev self-assigned this Jul 21, 2026
@vercel

vercel Bot commented Jul 21, 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 22, 2026 9:49am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 563d1b62-269f-4a41-9652-3911ca580634

📥 Commits

Reviewing files that changed from the base of the PR and between 331f206 and e3b0778.

📒 Files selected for processing (1)
  • next.config.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • next.config.js

📝 Walkthrough

Walkthrough

next.config.js adds Sentry-backed CSP report-only policy generation and applies it to catch-all route headers while strengthening the enforced CSP with object-src 'none' and base-uri 'self'.

Changes

CSP header configuration

Layer / File(s) Summary
Report-only CSP generation
next.config.js
Validates NEXT_PUBLIC_SENTRY_DSN, derives an optional Sentry report-uri, and constructs the report-only CSP directives and reporting endpoint header.
Security header wiring
next.config.js
Adds the report-only CSP and reporting endpoint headers, and expands the enforced CSP with object-src 'none' and base-uri 'self'.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 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 accurately reflects the main CSP reporting change and specifically highlights the new script-src allow-list.
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 feat/csp-report-only

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


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

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6212.38 → 6212.38 (0)
Findings: 0 net (+0 new, -0 resolved)

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2000 ran, 0 failed, 0 skipped, 34.1s

📊 Coverage (unit)

metric %
statements 59.5%
branches 43.3%
functions 48.4%
lines 59.8%
⏱ 10 slowest test cases
time test
4.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.2s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
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.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 › auto-refills a wallet older than the TTL on cold start
0.2s src/utils/__tests__/demo-balance.test.ts › auto-refills a stored balance that has no timestamp (legacy install)
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@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 `@next.config.js`:
- Around line 18-21: Update the DSN-to-CSP report URI construction around the
URL parsing logic to preserve the parsed protocol and complete pathname, rather
than stripping the path and hardcoding HTTPS. Use Sentry’s parsed DSN fields to
build the endpoint while retaining the existing validation of host, username,
and project information.
🪄 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: 6d23caa4-b046-48af-af7f-a2d0f9d6c635

📥 Commits

Reviewing files that changed from the base of the PR and between 4d23ef9 and c0fbae4.

📒 Files selected for processing (1)
  • next.config.js

Comment thread next.config.js Outdated
The report URI hardcoded https and dropped everything but the last path
segment, so a path-prefixed DSN (self-hosted Sentry under a sub-path)
posted reports to an endpoint that doesn't exist, and an http DSN was
silently upgraded.

Verified against a standard ingest DSN, a path-prefixed one, and a plain
http localhost DSN.

@kushagrasarathe kushagrasarathe 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.

Requesting changes — report-only measurement is fine; two things before it's useful/enforceable

Confirmed Content-Security-Policy-Report-Only — can't break the app, and provides no protection today (measurement only). The Sentry report-URI derivation is correct. No new dep, no attacker-controlled origin, no backdoor.

Required:

  • Add report-to + a Reporting-Endpoints header alongside the deprecated report-uri. report-uri is being phased out; without report-to the violation stream this PR exists to collect will be partial/empty in non-Chromium browsers, and you could promote on false confidence.
  • Before any promotion to enforcing (don't let this slip): remove 'unsafe-inline' and 'unsafe-eval' from script-src (nonce/hash + strict-dynamic). As written, enforcing this policy would not stop inline-script XSS — the dominant vector — so it would be theater.
  • Confirm Sentry PII scrubbing covers CSP report bodies — Peanut puts amount/step in URLs, which land in document-uri.

report-uri alone is deprecated and Chromium is where most violations
will come from once report-to is the only mechanism; shipping both (plus
the Reporting-Endpoints header the report-to group resolves against)
keeps the violation stream complete across engines, so the eventual
promotion to enforcing isn't decided on partial data.
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

Point by point:

report-to + Reporting-Endpoints — added in e3b0778. The report-only policy now carries both report-uri (what Firefox/Safari send today) and report-to csp-endpoint, with a Reporting-Endpoints: csp-endpoint="…" header resolving the group — both pointing at the same Sentry security endpoint, which accepts either delivery format per Sentry's security-policy-reporting docs. Verified the generated headers with and without a DSN present (no DSN → no reporting directives, policy still ships). Agreed on the false-confidence risk — that was worth blocking on.

unsafe-inline/unsafe-eval before enforcement — fully agreed, and it's already called out in the config comment as a pre-promotion requirement. Nonce/strict-dynamic is its own change (Next inline bootstrap + wallet SDKs); it belongs to the promotion PR, and promotion should be blocked on it. I'd treat "remove them or don't enforce" as the acceptance criterion for that PR.

Sentry PII scrubbing of CSP report bodies (document-uri carries amount/step) — I can't verify this one: it's an org-level Sentry setting and the only local token is upload-scoped. Needs someone with org access to confirm Data Scrubbing + document-uri/URL fields under Security & Privacy for the peanut-ui project (org peanut-c34d84c05), or add amount/step to sensitive fields. Leaving this box to you/Hugo.

@kushagrasarathe kushagrasarathe 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.

Re-reviewed — report-to condition RESOLVED. Policy now emits report-to csp-endpoint paired with a consistent Reporting-Endpoints: csp-endpoint="…" header (same group, both gated on a non-null DSN). Still Report-Only; enforced header stays minimal/safe (frame-ancestors/object-src/base-uri). unsafe-inline/unsafe-eval correctly remain flagged for the enforcement phase. SAFE. Advisory (not gating): confirm Sentry server-side PII scrubbing of CSP report bodies before ramp.

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.

2 participants