feat(web): Content-Security-Policy + Permissions-Policy#52
Conversation
Addresses the security-header findings from the 2026-06-23 audit (#42 added the other baseline headers but deferred CSP; Permissions-Policy was absent so the microphone was ungated). CSP — everything this app loads is same-origin (scripts, next/font/local fonts, next/og images; the auth client + voice/research APIs are same-origin /api/* fetches, the AI vendors are server-side, the waveform is a self-contained canvas), so the fetch directives are 'self' (+ data: images). Strong directives: default-src 'self', connect-src 'self' (blocks exfiltration), frame-ancestors 'none', object-src 'none', base-uri 'self'. KNOWN GAP (tracked): script-src keeps 'unsafe-inline' — React's inline style attrs force style-src 'unsafe-inline', and Next's hydration inline scripts need either 'unsafe-inline' or a per-request nonce. This baseline ships 'unsafe-inline' (no CSP existed before; the non-script directives are the bulk of the value); a nonce-based script-src ('strict-dynamic') is the next focused step — it needs middleware nonce plumbing + dev/HMR verification, so it's deliberately separate. 'unsafe-eval' is dev-only (Next HMR); prod omits it. form-action is left unrestricted so a future OAuth POST can't be silently broken (e2e auths via a seam, so it can't verify the real sign-in path). Permissions-Policy — deny every powerful feature except microphone=(self) (the voice capture's getUserMedia({ audio })). Verified: the full e2e suite (7 specs) passes UNDER the CSP — the board, dialogs (inline-styled pills), hydration, and status cycling all work — plus a new security-headers.spec asserting the headers are present. typecheck + lint clean; CodeRabbit CLI: the one expected 'unsafe-inline' posture-gap note (tracked above). Claude-Session: https://claude.ai/code/session_01HgjBcWAo4VLxXMf22NPBZx
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 37 minutes and 59 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds ChangesSecurity Headers Implementation and Verification
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/web/e2e/security-headers.spec.ts (1)
19-20: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winStrengthen the Permissions-Policy assertion to match the intended contract.
Right now this passes as long as
microphone=(self)exists, even if other powerful features are accidentally reopened.✅ Suggested assertion expansion
- expect(headers["permissions-policy"] ?? "").toContain("microphone=(self)"); + const permissionsPolicy = headers["permissions-policy"] ?? ""; + expect(permissionsPolicy).toContain("microphone=(self)"); + expect(permissionsPolicy).toContain("camera=()"); + expect(permissionsPolicy).toContain("geolocation=()"); + expect(permissionsPolicy).toContain("payment=()"); + expect(permissionsPolicy).toContain("usb=()"); + expect(permissionsPolicy).toContain("browsing-topics=()");🤖 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 `@apps/web/e2e/security-headers.spec.ts` around lines 19 - 20, The current assertion using toContain() for the permissions-policy header only verifies that microphone=(self) exists as a substring, which allows other powerful features to be accidentally reopened without detection. Replace the toContain() assertion with toBe() and specify the exact complete permissions-policy value that should be present in the headers["permissions-policy"] check to ensure a strict contract that prevents unintended feature activation.
🤖 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 `@apps/web/next.config.ts`:
- Around line 18-22: The variable cspDev on line 18 uses a condition that
enables 'unsafe-eval' for all non-production environments (test, staging, etc.)
when it should only be enabled strictly in development mode. Change the
condition from process.env.NODE_ENV !== "production" to process.env.NODE_ENV ===
"development" so that 'unsafe-eval' is only appended to the script-src directive
when NODE_ENV is explicitly set to development, not for any other non-production
mode.
---
Nitpick comments:
In `@apps/web/e2e/security-headers.spec.ts`:
- Around line 19-20: The current assertion using toContain() for the
permissions-policy header only verifies that microphone=(self) exists as a
substring, which allows other powerful features to be accidentally reopened
without detection. Replace the toContain() assertion with toBe() and specify the
exact complete permissions-policy value that should be present in the
headers["permissions-policy"] check to ensure a strict contract that prevents
unintended feature activation.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 232c8aa7-ead6-4358-8498-2cecb05dd632
📒 Files selected for processing (3)
apps/web/e2e/security-headers.spec.tsapps/web/next-env.d.tsapps/web/next.config.ts
NODE_ENV !== 'production' also enabled 'unsafe-eval' under NODE_ENV='test' and other non-prod modes that never serve the app. Gate it on === 'development' so only a real 'next dev' (HMR/refresh runtime) gets it. Verified: the full e2e suite still passes under the CSP (dev → eval present); typecheck + lint clean. Claude-Session: https://claude.ai/code/session_01HgjBcWAo4VLxXMf22NPBZx
|
Good catch — fixed in the latest commit: |
What
Adds a Content-Security-Policy and a Permissions-Policy — the security-header findings from the 2026-06-23 audit (#42 shipped the other baseline headers but deferred CSP; Permissions-Policy was absent, so the microphone was ungated).
CSP
Everything this app loads is same-origin (scripts,
next/font/localfonts,next/ogimages; the auth client + voice/research APIs are same-origin/api/*fetches; the AI vendors are called server-side; the waveform is a self-contained canvas). So the fetch directives are'self'(+data:images). Strong directives that are pure wins:default-src 'self'·connect-src 'self'(blocks exfiltration) ·frame-ancestors 'none'·object-src 'none'·base-uri 'self'Known gap (tracked) —
script-src 'unsafe-inline'React's inline
styleattributes forcestyle-src 'unsafe-inline', and Next's hydration inline scripts need either'unsafe-inline'or a per-request nonce. This baseline ships'unsafe-inline'— there was no CSP before, and the non-script directives above are the bulk of the value. A nonce-basedscript-src('strict-dynamic') is the next focused step: it needs middleware nonce plumbing + careful dev/HMR verification, so it's deliberately a separate PR. (CodeRabbit flagged this; it's an acknowledged posture gap, not a regression.)'unsafe-eval'is dev-only (Next HMR); prod omits it.form-actionis left unrestricted so a future OAuth POST flow can't be silently broken — the e2e harness authenticates via a seam, so it can't verify the real sign-in path.Permissions-Policy
Deny every powerful feature except
microphone=(self)(the voice capture'sgetUserMedia({ audio })).Verification
security-headers.specasserting the headers are present.unsafe-inlineposture-gap note (tracked above).https://claude.ai/code/session_01HgjBcWAo4VLxXMf22NPBZx
Summary by CodeRabbit
Tests
New Features