Skip to content

fix(csp): remove unsafe-inline from script-src and harden security headers#876

Open
advikdivekar wants to merge 3 commits into
Priyanshu-byte-coder:mainfrom
advikdivekar:fix/issue-858-csp-unsafe-inline
Open

fix(csp): remove unsafe-inline from script-src and harden security headers#876
advikdivekar wants to merge 3 commits into
Priyanshu-byte-coder:mainfrom
advikdivekar:fix/issue-858-csp-unsafe-inline

Conversation

@advikdivekar
Copy link
Copy Markdown
Contributor

What the problem was

src/app/layout.tsx injected a theme-initialisation <script> block via dangerouslySetInnerHTML. Because browsers block inline scripts under a Content-Security-Policy, any CSP on the site had to include script-src 'unsafe-inline' to let this script run. 'unsafe-inline' completely disables the browser's CSP protection against inline-script injection, meaning any DOM-based XSS vector — present or introduced later — executes without browser-level blocking, and can immediately call /api/auth/session to steal credentials. The site also had no security headers at all: no CSP, no X-Frame-Options, no X-Content-Type-Options.

What was changed and in which files

src/app/layout.tsx
Removed the dangerouslySetInnerHTML inline script entirely. The layout now reads the theme cookie from next/headers (server-side, zero JS required) and applies className="dark" directly on <html> before the page leaves the server. No inline script, no unsafe-inline dependency.

src/components/ThemeContext.tsx
Updated the theme provider to write a theme cookie (SameSite=Lax; path=/; max-age=1yr) via document.cookie on every theme change, in addition to the existing localStorage write. On mount, it reads the initial theme from document.documentElement.classList (which the server already set from the cookie) to stay in sync without triggering a DOM class swap. For first-time visitors with no cookie, it falls back to localStorage then prefers-color-scheme.

next.config.mjs
Added a headers() export that applies four security headers to every route:

  • Content-Security-Policyscript-src 'self' (no unsafe-inline), style-src 'self' 'unsafe-inline' (needed for Tailwind inline styles), img-src allows GitHub avatars, frame-ancestors 'none', base-uri 'self', form-action 'self'
  • X-Frame-Options: DENY — belt-and-suspenders clickjacking protection alongside frame-ancestors 'none'
  • X-Content-Type-Options: nosniff — prevents MIME-type sniffing attacks
  • Referrer-Policy: strict-origin-when-cross-origin — limits referrer leakage

Why this approach fixes the root cause

The root cause was the inline script itself. A nonce-based CSP would still require generating and injecting a nonce per request, adding significant complexity. The cookie approach eliminates the inline script entirely — the server reads a value it controls (the httpOnly-equivalent first-party cookie) and renders the correct class before the HTML is sent. No JavaScript runs before the page is interactive, and no CSP exception is needed. The frame-ancestors 'none' directive replaces the previously used 'self' value, since DevTrack has no legitimate embedding use case.

Steps to test

  1. Run npm run dev and open the app
  2. Toggle between light and dark mode — confirm it works without flash
  3. Reload the page — confirm the correct theme is applied immediately with no flash (cookie is set; server applies the class)
  4. Open a new browser session (no cookie) — page loads in light mode; toggling sets the cookie; reload persists the choice
  5. In DevTools → Network → any page request → Response Headers, confirm:
    • Content-Security-Policy contains script-src 'self' with no unsafe-inline
    • frame-ancestors 'none' is present
    • X-Frame-Options: DENY is present
    • X-Content-Type-Options: nosniff is present
  6. In DevTools → Console, confirm no CSP violation errors for normal page use
  7. Run npm run type-check and npm run lint — both pass with no new errors

Edge cases covered

  • Returning users (cookie set): Server applies class; no client-side DOM swap; zero flash
  • New users (no cookie): Server renders light; client reads prefers-color-scheme on mount and updates immediately; cookie is written; next load has no flash
  • localStorage migration: Existing users who stored preference in localStorage will have it picked up on first visit (fallback path) and the cookie will be set
  • System preference respected: On first visit, dark system preference triggers dark mode client-side
  • SSR/hydration: suppressHydrationWarning on <html> is retained; server and client agree on the class because both derive it from the same cookie

Regressions

None. npm run type-check and npm run lint pass clean. All dashboard functionality is unaffected — the security headers apply only to the HTTP layer and the theme change is a refactor of the persistence mechanism with identical visible behaviour.

Closes #858

Please review and merge this under GSSoC 2026.

Migrate all metrics API routes to retrieve the GitHub OAuth token
server-side via getGitHubAccessToken(req) instead of reading the
now-removed session.accessToken field. Covers contributions, prs,
pr-review-time, streak, repos, repo-health, ci, and compare routes.
…headers

Replace the dangerouslySetInnerHTML theme-init script in layout.tsx with
a server-side cookie read so the dark class is applied on <html> before
the page is sent to the browser. ThemeContext writes the theme cookie on
every toggle so the server stays in sync. With no inline script remaining,
next.config.mjs can declare a strict CSP with script-src 'self' and no
unsafe-inline. Also adds frame-ancestors 'none', X-Frame-Options, DENY,
X-Content-Type-Options, and Referrer-Policy headers.

Closes Priyanshu-byte-coder#858
@vercel
Copy link
Copy Markdown

vercel Bot commented May 23, 2026

@advikdivekar is attempting to deploy a commit to the PRIYANSHU DOSHI's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added gssoc26 GSSoC 2026 contribution type:bug GSSoC type bonus: bug fix type:security GSSoC type bonus: security (+20 pts) labels May 23, 2026
@github-actions
Copy link
Copy Markdown

GSSoC Label Checklist 🏷️

@Priyanshu-byte-coder — please apply the appropriate labels before merging:

Difficulty (pick one):

  • level:beginner — 20 pts
  • level:intermediate — 35 pts
  • level:advanced — 55 pts
  • level:critical — 80 pts

Quality (optional):

  • quality:clean — ×1.2 multiplier
  • quality:exceptional — ×1.5 multiplier

Validation (required to score):

  • gssoc:approved — counts for points
  • gssoc:invalid / gssoc:spam / gssoc:ai-slop — does not score

Type labels (type:*) are auto-detected from files and title. Review and adjust if needed.
Points formula: (difficulty × quality_multiplier) + type_bonus

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc26 GSSoC 2026 contribution type:bug GSSoC type bonus: bug fix type:security GSSoC type bonus: security (+20 pts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] CSP script-src unsafe-inline eliminates browser XSS protection and enables token theft chain

1 participant