All articles
Content Security PolicyWeb SecurityHTTP Headers

Fixing a Missing Content Security Policy Without Breaking Your Site

Learn how to fix a missing Content Security Policy step by step — from audit to rollout — without breaking scripts, styles, or third-party integrations.

WebSentry TeamMay 12, 20266 min read

A missing Content Security Policy (CSP) is one of the most common findings on security scans, and it's also one of the riskiest to ignore. Without a CSP, your site has no browser-level defence against cross-site scripting (XSS), malicious script injection from compromised third-party tags, or clickjacking via untrusted iframes. The fix isn't hard — but adding a strict CSP carelessly will break your site in seconds. Here's how to roll one out properly.

Why a Missing CSP Matters

Content Security Policy is an HTTP response header that tells the browser exactly which sources of scripts, styles, images, fonts, and frames it's allowed to load. If an attacker manages to inject a <script> tag through a comment field, a vulnerable dependency, or a compromised CDN, a properly configured CSP stops the browser from executing it.

Without CSP, you're relying entirely on input sanitisation and luck. With it, you have a hard browser-enforced allowlist. That's why scanners like WebSentry flag a missing CSP as a major grade reducer — it's a baseline modern site should have.

Step 1: Audit What Your Site Actually Loads

Before writing a single directive, you need to know what your site loads and from where. Open Chrome DevTools, go to the Network tab, reload the page, and group by domain. Make a list of every external origin serving:

  • Scripts (analytics, tag managers, chat widgets, A/B tests)
  • Stylesheets (fonts, design systems, CDN-hosted CSS)
  • Images (CDNs, S3 buckets, user content)
  • Fonts (Google Fonts, Typekit, self-hosted)
  • iframes (YouTube, Vimeo, Stripe, reCAPTCHA, maps)
  • XHR/fetch endpoints (your API, third-party APIs)
  • WebSocket connections

Also check for inline <script> blocks, inline style="..." attributes, and onclick="..." handlers. These are the most common things that break when a CSP goes live.

Step 2: Start in Report-Only Mode

Never deploy a strict CSP directly to production. Use Content-Security-Policy-Report-Only first. The browser will evaluate the policy and log violations, but it won't actually block anything.

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; frame-src https://www.youtube.com; report-uri /csp-report

Set up a reporting endpoint (or use a service like report-uri.com) and let it run for at least a week. Real users on real browsers will surface violations you'd never catch on your own machine — especially from browser extensions, legacy pages, and email-triggered flows.

Step 3: Build the Policy Directive by Directive

A CSP is a list of directives. Here's a practical breakdown of the ones that matter most:

default-src

The fallback for any directive you don't specify. Start with default-src 'self' — only resources from your own origin are allowed unless explicitly overridden.

script-src

The most security-critical directive. Avoid 'unsafe-inline' and 'unsafe-eval' if at all possible. Instead, use one of:

  • Nonces: Generate a per-request random value and add nonce="abc123" to legitimate inline scripts and 'nonce-abc123' to your CSP.
  • Hashes: Compute the SHA-256 of each inline script and add 'sha256-...' to the policy. Best for static inline blocks.
  • Strict-dynamic: Combined with a nonce, lets trusted scripts load further scripts without listing every domain.

style-src

Trickier than scripts because many frameworks (Tailwind JIT in dev, styled-components, MUI) inject inline styles. If you can't eliminate them, 'unsafe-inline' for styles is a more acceptable compromise than for scripts — but try hashes or nonces first.

img-src, font-src, connect-src, frame-src

List specific origins. Use data: for inline base64 images if needed, and blob: for generated content. For connect-src, include every API host your frontend talks to, including WebSockets (wss://).

frame-ancestors

Replaces the older X-Frame-Options header. Use frame-ancestors 'none' unless you specifically need to be embeddable.

upgrade-insecure-requests

Automatically rewrites http:// resource requests to https://. A cheap win on legacy codebases.

Step 4: Eliminate Inline Code Where Possible

The biggest barrier to a strict CSP is inline JavaScript. Common offenders:

  1. Analytics snippets pasted directly into <head> — move them to an external file or use a tag manager with a nonce.
  2. onclick, onload, and other inline event handlers — replace with addEventListener in an external script.
  3. Server-rendered config like <script>window.__INITIAL_STATE__ = {...}</script> — add a nonce attribute server-side.
  4. Third-party widgets (Intercom, Hotjar, Drift) — most now support nonce propagation; check their docs.

Step 5: Deploy the Header Properly

Where you set the header depends on your stack:

  • Nginx: add_header Content-Security-Policy "..." always; in your server block.
  • Apache: Header always set Content-Security-Policy "..." in your vhost or .htaccess.
  • Cloudflare: Use Transform Rules or Workers to inject the header at the edge.
  • Next.js: Set it in next.config.js under headers(), or use middleware to inject per-request nonces.
  • Express: Use helmet with contentSecurityPolicy options.

Set the header at the application or edge layer — not via a <meta> tag. Meta-tag CSPs don't support frame-ancestors, report-uri, or sandbox directives, and they apply too late in the loading process.

Step 6: Switch from Report-Only to Enforcing

Once your reports are quiet for several days across real traffic, rename the header from Content-Security-Policy-Report-Only to Content-Security-Policy. Keep the report-uri (or modern report-to) in place — you'll want ongoing visibility when new third-party tags are added or when an attacker probes the policy.

Common Pitfalls to Watch For

  • Wildcards everywhere: script-src * is barely better than no policy at all. Be specific.
  • Forgetting subdomains: https://example.com does not cover https://cdn.example.com. Use https://*.example.com or list each one.
  • Tag manager sprawl: Once GTM is allowed, anything it loads is effectively trusted. Audit what's inside it.
  • Dev tools and previews: Staging environments often need looser policies — handle this with environment-aware config, not by weakening production.

Verify the Fix

After deployment, validate the header is being served on every route — not just the homepage. Check with curl -I https://yoursite.com, run it through a scanner, and review the headers tab in DevTools. WebSentry will pick up the new header, parse the directives, and flag remaining weaknesses like 'unsafe-inline' usage or missing frame-ancestors, so you can keep tightening the policy over time.

Run a free scan at websentry.dev to see exactly which directives are missing, which are too permissive, and how your CSP compares to the rest of your security headers.

Check your own site

Run a free security scan and see if your site has the issues covered in this article. Results in under 30 seconds.