All posts

Content-Security-Policy: Build It Right, Then Audit It

July 31, 2026 · DevTools

csp
content-security-policy
security-headers
xss
web-security

A Content-Security-Policy (CSP) is the single most effective browser-side defense against cross-site scripting. It tells the browser which origins are allowed to execute scripts, load styles, connect over the network, and frame your pages. Done well, it stops an injected script dead even when a bug lets it into your HTML. Done badly, it looks present in the response headers while doing almost nothing.

This guide covers the two halves of a healthy CSP lifecycle: building one that is strict but workable, and auditing the one you ship so a weak directive doesn't slip through.

The directive that matters most: default-src

Every CSP should start from a default-src and narrow from there. default-src is the fallback for every resource type that does not have its own directive.

Content-Security-Policy: default-src 'self'

Without default-src, any resource type you forgot to restrict is allowed from anywhere. With it set to 'self', you start locked down and open up only what you need: script-src, style-src, img-src, connect-src, and so on.

The three penalty box entries

A policy earns a poor risk score for three recurring reasons, and each has a real fix:

  • 'unsafe-inline' in script-src. This permits inline <script> tags and event handlers, which is exactly the vehicle most XSS rides on. Replace it with per-request nonces ('nonce-<random>') or hashes ('sha256-<…>'), or use 'strict-dynamic' so a trusted loader can pull in its own scripts.
  • The * wildcard in a fetch directive. img-src * reads as "images from anywhere", which is tolerable for images but dangerous in script-src or connect-src. Replace * with explicit origins.
  • A missing frame-ancestors. Without it, any site can embed yours in an invisible iframe (clickjacking). Add frame-ancestors 'none' unless you genuinely need to be framed.

Build visually, then validate

When you're constructing a policy, a visual builder keeps the directives and their sources in one place and exports the header for Nginx, Apache, or a Next.js middleware. The CSP Builder & Validator does this on its Build tab and shows a live security score that drops as you add unsafe sources.

But the more valuable half is the Validate tab. Paste the policy you actually ship — copied from your response headers — and it parses the directives and grades the policy 0–100, listing every weakness with a concrete fix. A policy that looks strict often scores 55 because of a leftover 'unsafe-inline' or a wildcard you forgot.

default-src 'self'; script-src 'self' 'unsafe-inline' *; img-src 'self'

That policy is present, it has several directives, and it still scores poorly: 'unsafe-inline' and the * in script-src, plus no frame-ancestors. The validator names all three.

Report-Only: the safe rollout

If you're hardening an existing site, ship with Content-Security-Policy-Report-Only first. Configure a Reporting-Endpoints response header that names your reporting group, then reference that group with the CSP report-to directive (keep report-uri as a compatibility fallback where needed). The browser logs violations without blocking anything, so you can watch real traffic, fix the breakages, and flip to enforcement once the reports go quiet.

A strong default

If you want a starting point that scores 100 in the validator and works for most server-rendered apps:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'

Paste it into the validator to confirm, then open up only the origins your app actually needs. Build, audit, enforce — in that order — and your CSP will do the job it's there for.