TLDR: 'unsafe-eval' in your script-src doesn't weaken your CSP — it effectively disables it. It had been sitting there since I first wired up the security headers.

The App

This is an internal tool I built for an ecommerce client — a drag-and-drop webinar scheduling board and KOL (Key Opinion Leader, basically practitioner relationship management) contact manager.

Real app. The ecommerce client's team uses it every day. Next.js App Router, Supabase, Tailwind, deployed to Vercel.

We'd been doing security cycles through it — Zod validation on every API route, closing IDOR gaps, hardening auth. 31 items addressed across the prior cycles.

And then Cycle 19 came back with something embarrassing.

What the Council Found

I run an autonomous development loop I call The Council — AI agents doing a fresh review of the codebase looking for issues the prior cycles missed. This particular pass was a six-agent fresh review.

They flagged this in src/lib/supabase/middleware.ts:

script-src 'self' 'unsafe-inline' 'unsafe-eval'

Three directives. The third one had been sitting there since I first wired up security headers.

The words I wrote in the commit message say it plainly: "was nullifying XSS protection entirely."

NULLIFYING. Not weakening. Not degrading. Nullifying.

What unsafe-eval Actually Does

A Content Security Policy's whole job is to tell the browser what JavaScript is allowed to execute.

script-src 'self' 'unsafe-inline' says: run scripts from this origin, and allow inline <script> tags. That's already not ideal — but it's manageable.

'unsafe-eval' adds: also allow eval(), new Function(), and any method that compiles strings into executable code at runtime.

That's exactly the attack vector XSS exploits. An attacker injects a string. eval() executes it. Game over.

Adding 'unsafe-eval' to your CSP is like installing a deadbolt and leaving the window open.

Why It Was There

Honestly? I put it in early during setup, probably to silence some tooling warning, and never went back to question it.

This is the move that gets you. You paste a permissive CSP to make the dev experience smooth, and then you forget it's still there when everything is in production.

The Fix

One line in middleware.ts.

Before:

"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; ..."

After:

"default-src 'self'; script-src 'self' 'unsafe-inline'; ..."

That's it. Pull the substring. Build passes. Ship.

(I still have 'unsafe-inline' — Next.js needs it for runtime hydration. The right path to drop that too is nonces or strict-dynamic, which is a larger migration. One step at a time.)

Why 31 Items In, It Was Still There

Because I'd looked at that header so many times it became invisible.

I was reviewing validation logic, auth flows, race conditions — real, gnarly stuff. The CSP string in middleware was just… there. Background noise.

Fresh eyes see what familiarity misses.

That's the whole point of a council review. Not to find the complicated thing. To find the obvious thing you stopped seeing.