Content Security Policy has a reputation problem.

A lot of teams either treat CSP like a magic anti-XSS shield, or they avoid it because they assume it will break everything. Both takes are wrong. CSP is powerful, but it’s also easy to misuse in ways that give you a nice-looking header and very little real protection.

I’ve seen plenty of production policies that look serious because they’re long, but collapse the moment an inline script sneaks in or a third-party script gets too much trust.

Here are the most common CSP myths I keep running into, the mistakes behind them, and what to do instead.

Myth #1: “If I set any CSP header, I’m protected”

This is the biggest one.

A CSP header is not automatically a good CSP header. You can absolutely deploy a policy that checks a compliance box while doing almost nothing for actual exploit resistance.

Take this real header:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-MzgxZmQzMDYtMjU0ZC00YTRlLWFhYWQtODZmZmY0MjYwMTQ3' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.headertest.com https://u.headertest.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

This is not a bad policy. It has solid pieces:

  • object-src 'none'
  • frame-ancestors 'none'
  • base-uri 'self'
  • form-action 'self'
  • nonce-based script-src
  • strict-dynamic

That said, “we have CSP” still doesn’t mean “we solved script injection.” The moment your app relies on dangerous patterns elsewhere, or your trusted nonce-bearing script loads attacker-controlled code, the protection gets weaker fast.

Fix

Judge the policy by what it actually blocks:

  • Are inline scripts blocked unless nonce/hash approved?
  • Are dangerous fallback directives locked down?
  • Are you avoiding broad wildcards?
  • Are you reducing trust in third-party origins?

If you want examples of sane starting points, https://csp-examples.com is useful for ready-to-use policies.

Myth #2: “default-src 'self' covers everything”

Nope. This is one of the most common misunderstandings.

default-src is only a fallback for fetch directives that are not explicitly set. Once you define script-src, style-src, img-src, connect-src, and friends, those directives take over.

Developers often assume this:

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

means all scripts, styles, images, frames, and connections are locked to self forever.

That’s only true until you start adding exceptions elsewhere. And in real apps, you always do.

Mistake

People add a narrow default-src and later bolt on permissive directives:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https: 'unsafe-inline' 'unsafe-eval';
  img-src * data:;

Now the strict-looking default barely matters.

Fix

Treat each directive as its own policy decision. Review the ones that matter most first:

  • script-src
  • style-src
  • connect-src
  • frame-src
  • img-src

The official docs are worth reading here because the fallback behavior trips people up constantly: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

Myth #3: “'unsafe-inline' is fine if the rest of the policy is strict”

Usually it isn’t.

If you allow inline script with 'unsafe-inline', you’re punching a giant hole in the main thing CSP is best at stopping: script injection through HTML.

For styles, teams often tolerate 'unsafe-inline' because modern frontend tooling and third-party widgets make strict style policies annoying. The header above does exactly that:

style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com

That’s a tradeoff. Not ideal, but common.

For scripts, though, 'unsafe-inline' is where policies go to die.

Mistake

A policy like this gives false confidence:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-inline';

Any injected inline <script> block can now run.

Fix

Use nonces or hashes instead.

Nonce example:

Content-Security-Policy:
  script-src 'self' 'nonce-r4nd0m123';
  object-src 'none';
  base-uri 'self';

HTML:

<script nonce="r4nd0m123">
  window.appConfig = { env: "prod" };
</script>
<script nonce="r4nd0m123" src="/static/app.js"></script>

In a real app, the nonce must be generated fresh for every response.

Node/Express example:

import crypto from "node:crypto";
import express from "express";

const app = express();

app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString("base64");
  res.setHeader(
    "Content-Security-Policy",
    `default-src 'self'; script-src 'self' 'nonce-${res.locals.nonce}'; object-src 'none'; base-uri 'self'`
  );
  next();
});

app.get("/", (req, res) => {
  res.send(`
    <!doctype html>
    <html>
      <body>
        <script nonce="${res.locals.nonce}">
          console.log("trusted inline script");
        </script>
        <script nonce="${res.locals.nonce}" src="/app.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000);

Myth #4: “Whitelisting script domains is enough”

This used to be the default CSP mindset: just list the script domains you trust.

The problem is that domain allowlists are brittle. Third-party providers host lots of scripts, change infrastructure, load follow-on resources, and sometimes include user-controlled content in places you didn’t expect.

That’s why strict-dynamic exists, and the real header above uses it:

script-src 'self' 'nonce-...' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com

With a nonce and strict-dynamic, the browser trusts nonce-approved scripts and scripts they load, instead of relying only on static host allowlists.

Mistake

Relying only on this pattern:

script-src 'self' https://cdn.example.com https://analytics.example.net

This is better than nothing, but it’s not as strong as nonce- or hash-based trust.

Fix

Prefer nonce- or hash-based script-src, and add strict-dynamic when you can support it.

Example:

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-r4nd0m123' 'strict-dynamic';
  object-src 'none';
  base-uri 'self';

If you need compatibility-oriented fallbacks, keep host sources too, but don’t pretend the host allowlist is the real control.

For the directive behavior details, use the official reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src

Myth #5: “CSP can fix unsafe JavaScript patterns for me”

I wish.

CSP reduces exploitability. It does not clean up dangerous code. If your app does this:

element.innerHTML = userInput;

or this:

setTimeout(userControlledString, 0);

you still have a serious problem. CSP may block some payloads, but now your security depends on your policy being perfect across every page, every browser behavior, and every third-party integration. That’s not where I’d want to be.

Fix

Use CSP as a backup layer, not as your primary defense.

You still need to:

  • avoid DOM XSS sinks
  • use safe templating
  • sanitize untrusted HTML properly
  • remove inline event handlers
  • stop evaluating strings as code

If you’re modernizing an app, fixing onclick=, innerHTML, and string-to-code patterns usually gives you a much cleaner path to a strong CSP.

Myth #6: “Report-Only means I’m basically done”

Content-Security-Policy-Report-Only is useful, but teams often get stuck there for months.

They collect violation reports, silence noisy entries, and never enforce the policy. At that point, CSP becomes observability instead of protection.

Mistake

Leaving production on report-only forever:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-abc123';

This blocks nothing.

Fix

Use report-only as a rollout phase, then switch to enforcement.

A practical rollout looks like this:

  1. Start with report-only.
  2. Fix legitimate breakage.
  3. Remove dead exceptions.
  4. Enforce.
  5. Keep a reporting channel for future regressions.

If you need the exact header syntax and reporting directives, the official documentation covers it: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri

Myth #7: “A bigger policy is a stronger policy”

Longer is not stronger. Sometimes it just means you’ve accumulated years of exceptions nobody wants to touch.

I’ve seen policies with dozens of domains copied from old tickets, vendor docs, and emergency fixes. Nobody knows which ones are still needed. That’s how you end up with a policy that permits half the internet and protects very little.

The header from headertest.com is fairly disciplined, but even there you can see the complexity third parties introduce:

  • Google Tag Manager
  • Google Analytics
  • Cookiebot
  • WebSocket endpoints
  • consent frames
  • inline styles allowed for compatibility

That’s normal. The mistake is assuming every added source is harmless.

Fix

Trim aggressively.

Ask for each source:

  • What exact feature needs this?
  • Is it still used?
  • Can I scope it to a narrower directive?
  • Can I replace a host allowlist with a nonce or hash?
  • Can I self-host the asset?

A smaller script-src is usually a better script-src.

Myth #8: “CSP is only about XSS”

XSS is the headline use case, but CSP helps with more than that.

A solid policy can also reduce risk around:

  • clickjacking via frame-ancestors
  • plugin abuse via object-src 'none'
  • base URL manipulation via base-uri
  • form exfiltration via form-action
  • untrusted framing and embedded content via frame-src

The real header includes several of these defensive directives:

frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';

That’s good security hygiene. A lot of teams obsess over script-src and forget the rest.

Fix

Don’t ship a script-only CSP and call it done.

A practical baseline often looks like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{NONCE}}' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  form-action 'self';

Then adapt based on actual app needs.

What I’d actually tell a team doing CSP today

Don’t chase a perfect policy on day one. Chase a real policy.

That means:

  • remove inline scripts where possible
  • use nonces or hashes for the inline code that remains
  • avoid 'unsafe-inline' for scripts
  • use strict-dynamic when you trust nonce-bearing bootstrap scripts
  • keep third-party domains on a short leash
  • enforce, don’t just report
  • include non-script directives like object-src, base-uri, form-action, and frame-ancestors

If your current policy mostly consists of copied vendor domains and old exceptions, you probably don’t have a CSP strategy. You have a truce with your dependencies.

That’s fixable. Start with the dangerous myths, stop treating the header as a checkbox, and make every directive earn its place.