Back to Blog
Dec 10, 20253 min readOnuzulike Anthony

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.

SecuritySecurityCSPWeb

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.

http
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:

ts
// 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:

tsx
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:

http
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:

  1. Per-request nonces — if AdSense respects nonces (it increasingly does in newer versions).
  2. Hash-based allowlist — fragile; breaks when AdSense updates their script.
  3. 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:

http
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /api/csp-report

Report-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

http
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.