Skip to content

feat(web): Content-Security-Policy + Permissions-Policy#52

Merged
RyRy79261 merged 2 commits into
mainfrom
feat/security-headers-csp
Jun 23, 2026
Merged

feat(web): Content-Security-Policy + Permissions-Policy#52
RyRy79261 merged 2 commits into
mainfrom
feat/security-headers-csp

Conversation

@RyRy79261

@RyRy79261 RyRy79261 commented Jun 23, 2026

Copy link
Copy Markdown
Owner

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/local fonts, next/og images; 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 style attributes 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' — there was no CSP before, and the non-script directives above are the bulk of the value. A nonce-based script-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-action is 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's getUserMedia({ audio })).

Verification

  • Full e2e suite (7 specs) passes UNDER the CSP — board, dialogs (inline-styled pills), hydration, status cycling all work — plus a new security-headers.spec asserting the headers are present.
  • typecheck + lint clean; CodeRabbit CLI: only the expected unsafe-inline posture-gap note (tracked above).

https://claude.ai/code/session_01HgjBcWAo4VLxXMf22NPBZx

Summary by CodeRabbit

  • Tests

    • Added end-to-end tests to validate proper configuration and enforcement of security headers across all application routes.
  • New Features

    • Strengthened application security with implementation of Content-Security-Policy, Permissions-Policy, and enhanced frame-options headers to protect against unauthorized access and script injection attacks.

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
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
ops-board-web Ready Ready Preview, Comment Jun 23, 2026 8:28am

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@RyRy79261, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f69f2cc3-95f7-4adb-820e-2ce21dce5bcb

📥 Commits

Reviewing files that changed from the base of the PR and between b9df745 and 3bc8a49.

📒 Files selected for processing (1)
  • apps/web/next.config.ts
📝 Walkthrough

Walkthrough

Adds Content-Security-Policy and Permissions-Policy HTTP response headers to all routes in next.config.ts, including a dev-only unsafe-eval directive. A Playwright e2e test verifies the header values and confirms the board renders correctly. A minor next-env.d.ts type path is also updated.

Changes

Security Headers Implementation and Verification

Layer / File(s) Summary
CSP and Permissions-Policy header configuration
apps/web/next.config.ts
Defines cspDev, contentSecurityPolicy, and permissionsPolicy string locals (with unsafe-eval conditionally included in non-production), then injects Content-Security-Policy and Permissions-Policy entries into the headers() hook for all routes alongside the existing baseline headers.
E2e header verification test and type path fix
apps/web/e2e/security-headers.spec.ts, apps/web/next-env.d.ts
Adds a Playwright test asserting CSP contains default-src 'self', frame-ancestors 'none', and object-src 'none'; Permissions-Policy contains microphone=(self); and x-frame-options equals DENY, plus a UI render check. Updates the Next.js route types import path from .next/types to .next/dev/types.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A hop, a skip, a policy set,
CSP guards every request!
frame-ancestors 'none' — no sneaky iframe,
Permissions locked, a secure dream.
The rabbit checked headers and smiled with glee,
The board renders safe — just as should be! 🔒

🚥 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 PR title clearly and concisely summarizes the main changes: adding Content-Security-Policy and Permissions-Policy security headers to the web application.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/security-headers-csp

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.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)
apps/web/e2e/security-headers.spec.ts (1)

19-20: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Strengthen 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

📥 Commits

Reviewing files that changed from the base of the PR and between 00df180 and b9df745.

📒 Files selected for processing (3)
  • apps/web/e2e/security-headers.spec.ts
  • apps/web/next-env.d.ts
  • apps/web/next.config.ts

Comment thread apps/web/next.config.ts Outdated
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
@RyRy79261

Copy link
Copy Markdown
Owner Author

Good catch — fixed in the latest commit: cspDev now gates on process.env.NODE_ENV === "development" so 'unsafe-eval' is only added under a real next dev, not test/other non-prod modes. Verified the full e2e suite still passes under the CSP.

@RyRy79261 RyRy79261 merged commit 6e268b3 into main Jun 23, 2026
14 checks passed
@RyRy79261 RyRy79261 deleted the feat/security-headers-csp branch June 23, 2026 08:29
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