Content Security Policy in Production: The Hard Parts
Getting CSP right in a real app — nonces, hash-based policies, streaming iframes, and why 'unsafe-inline' kills your whole policy.
Content Security Policy is one of the highest-leverage security headers available to web developers. A correctly configured CSP prevents XSS payloads from exfiltrating data or executing arbitrary code even if an attacker manages to inject script. But a misconfigured CSP either blocks legitimate functionality or creates a false sense of security.
What CSP Does
The browser only executes scripts, loads resources, and renders frames from sources you explicitly permit. A CSP violation generates an error in the console and, if you set up a reporting endpoint, a network report.
Content-Security-Policy: default-src 'self'; script-src 'self'; img-src 'self' data:The unsafe-inline Problem
The most common mistake: adding 'unsafe-inline' to script-src to fix an error, which immediately negates the XSS protection that CSP was added for. Any injected <script> tag now executes.
The alternative is nonces (per-request random tokens) or hashes (SHA-256 of known inline scripts).
Nonces in Next.js
Next.js middleware can generate a nonce and pass it to the document:
// middleware.ts
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const cspHeader = [
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}'`,
].join('; ');
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', cspHeader);
response.headers.set('x-nonce', nonce); // read in layout.tsx
return response;
}In your root layout, apply the nonce to any inline scripts:
const nonce = headers().get('x-nonce');
<script nonce={nonce} dangerouslySetInnerHTML={{ __html: `window.__ENV__ = ...` }} />The browser only executes scripts that match the nonce or the hash — injected scripts without the nonce are blocked.
Streaming and frame-src
NodWatch's CSP allows five external streaming embed domains in frame-src:
frame-src 'self' vaplayer.ru streamimdb.ru vidsrc.net vidsrc.cc embed.su;This is a real tension: to embed third-party streaming players, you have to allowlist their domains in frame-src. The mitigation is keeping the list as tight as possible — only the exact domains used — and reviewing it when you add or remove providers.
frame-src controls which domains can be loaded in iframes. It's separate from connect-src (XHR/fetch), script-src (scripts), and img-src (images). Getting the granularity right means an attacker who somehow injects an iframe pointing to a malicious domain (not in your allowlist) will have it blocked.
Google AdSense and Third-Party Scripts
AdSense is the classic example of a third-party script that resists strict CSP. It injects inline scripts, loads from multiple CDNs, and creates iframes. The realistic options:
- Per-request nonces — if AdSense respects nonces (it increasingly does in newer versions).
- Hash-based allowlist — fragile; breaks when AdSense updates their script.
- Isolated iframe — load AdSense in a sandboxed iframe that has its own relaxed CSP, keeping the main document under a strict policy.
NodWatch pre-configures AdUnit components with the known AdSense domains in script-src and frame-src. It's an explicit trade-off: ad revenue requires a less-strict policy for those pages.
Reporting
Add a report-uri or report-to directive to collect violations without blocking:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /api/csp-reportReport-Only mode logs violations without blocking — use it to audit your policy before enforcing it. Production violations reveal real blocked resources you haven't accounted for.
Practical Starting Point
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-NONCE';
style-src 'self' 'nonce-NONCE';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://your-api.com;
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';Start here, then add domains as you discover actual violations in Report-Only mode. Every addition to the allowlist is a conscious decision, not a reaction to a broken page.