How to Audit Your Existing CSP Policy

Table of Contents

A CSP audit is mostly about answering one question: does this policy actually reduce risk, or does it just look security-ish in a scanner?

I’ve seen plenty of CSPs that technically exist but barely help. The usual pattern is a giant allowlist, unsafe-inline hanging around forever, and a team that’s scared to touch it because analytics or consent tooling might break. Fair enough. CSP can get messy fast.

Here’s a practical way to audit the policy you already have, using a real-world header as the running example.

The example policy

This is the CSP header from headertest.com:

content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-MTE0OGI5OGYtZTMxZS00ZTZkLWFjOWQtM2IzMmFkYzNjODhk' '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://tallycdn.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'

That’s already better than a lot of production CSPs. It has object-src 'none', frame-ancestors 'none', base-uri 'self', and a nonce-based script-src. Good signs.

Still, there are things I’d audit hard.


1. Start by formatting the policy

Don’t audit a one-line CSP. Split it out so you can actually reason about it.

default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-MTE0OGI5OGYtZTMxZS00ZTZkLWFjOWQtM2IzMmFkYzNjODhk' '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://tallycdn.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';

I usually paste it into a scratch file and review it directive by directive.


2. Check the high-value directives first

These are the ones I care about first because they have the biggest security impact.

object-src

object-src 'none'

This is exactly what you want unless you have some ancient plugin requirement. Most apps don’t.

base-uri

base-uri 'self'

Good. This blocks malicious <base> injection from rewriting relative URLs.

frame-ancestors

frame-ancestors 'none'

Strong clickjacking protection. If your site never needs to be embedded, keep it.

form-action

form-action 'self'

Also good. It limits where forms can submit.

If your CSP is missing any of those, fix that before you obsess over tiny allowlist tweaks.


3. Audit script-src like you mean it

This is the big one.

The example:

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

What’s good here

  • Uses a nonce
  • Uses 'strict-dynamic'
  • No 'unsafe-inline'
  • No 'unsafe-eval'

That’s solid.

What I’d verify

Is the nonce generated per response?

A CSP nonce must be unpredictable and unique per request. If the nonce is static, reused, or guessable, you’ve got fake security.

Server example in Express:

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

const app = express();

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString("base64");
  res.locals.nonce = nonce;

  res.setHeader(
    "Content-Security-Policy",
    [
      `default-src 'self'`,
      `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
      `style-src 'self'`,
      `object-src 'none'`,
      `base-uri 'self'`,
      `frame-ancestors 'none'`
    ].join("; ")
  );

  next();
});

Template usage:

<script nonce="{{nonce}}">
  window.appConfig = { env: "prod" };
</script>

Are host allowlists still needed?

When you use nonce + 'strict-dynamic', host-based sources in script-src become less meaningful in supporting browsers. That’s not always bad, but it often means the policy is carrying old baggage.

I’d ask:

  • Do we still need https://www.googletagmanager.com in script-src?
  • Is it there for older browser behavior?
  • Is the team aware of how 'strict-dynamic' changes trust?

If the answer is “we copied it from somewhere,” that’s a red flag.

Read the official CSP docs on directive behavior if you want the exact browser semantics: MDN Content-Security-Policy.


4. Treat style-src 'unsafe-inline' as debt

The example has:

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

This is probably the weakest part of the policy.

'unsafe-inline' in style-src is common because third-party widgets and older frontend code love inline styles. But don’t pretend it’s harmless. It weakens XSS mitigation.

What to audit

  • Are inline style="" attributes used across the app?
  • Are there inline <style> blocks in templates?
  • Is a third-party tool forcing this?
  • Can some inline styles move into static CSS files?
  • Can you use nonces or hashes for inline style blocks?

A better target looks like this:

style-src 'self' 'nonce-{RANDOM_NONCE}' https://consent.cookiebot.com;

Or, if you need examples to work from, use a ready-made baseline from https://csp-examples.com.

Quick browser audit trick

Open DevTools and search your rendered DOM for inline styles:

[...document.querySelectorAll('[style]')].length

And inline style blocks:

document.querySelectorAll('style').length

That won’t tell you what’s safe, but it gives you a fast estimate of how hard removing 'unsafe-inline' will be.


5. Look for overbroad sources

Some parts of the example are intentionally broad:

img-src 'self' data: https:;

This means images can load from any HTTPS origin. That’s convenient, but broad.

Is that always bad?

Not necessarily. img-src is lower risk than script-src. Still, broad sources can hide accidental dependencies or data exfil paths.

Questions to ask:

  • Do we really need https: for all images?
  • Are user-generated image URLs expected?
  • Could we narrow to a CDN or a known image host list?

Same idea applies to wildcard sources like:

https://*.cookiebot.com
https://*.google-analytics.com
https://*.googletagmanager.com

Wildcards are often legitimate, but they deserve proof. During an audit, I want a reason for each one.

Make a table like this:

Source Directive Why needed Verified? Can narrow?
https://*.cookiebot.com script-src Cookie consent scripts Yes Maybe
https: img-src Unknown legacy image loads No Yes
https://tallycdn.com connect-src Form/analytics endpoint Yes No

If nobody can explain a source, it probably shouldn’t be there.


6. Audit connect-src carefully

Example:

connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;

connect-src controls fetch, XHR, WebSocket, Beacon, and more. It’s easy to forget, and it matters a lot for data flow.

What I check

  • Every API domain in use
  • Analytics collection endpoints
  • WebSocket endpoints
  • Error reporting endpoints
  • Third-party SDK background traffic

Browser-side quick check:

performance.getEntriesByType("resource")
  .filter(r => ["fetch", "xmlhttprequest"].includes(r.initiatorType))
  .map(r => r.name);

That gives you a rough list of active network destinations.

For WebSockets, inspect your app code or network panel for ws:// or wss://.

If connect-src is too broad, an attacker with script execution gets more outbound paths.


7. Verify fallback behavior from default-src

Example:

default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;

default-src is a fallback for directives you didn’t explicitly set.

That means part of your audit is checking what’s missing.

In this policy, there’s no explicit:

  • media-src
  • manifest-src
  • worker-src

That might be fine. But if your app uses web workers, PWAs, audio/video, or manifests, you should define those explicitly instead of letting default-src decide.

A tighter version often reads better because it makes intent obvious.


8. Check whether the policy matches real app behavior

The easiest way to audit CSP is to run the site and watch for violations.

If you can, deploy a temporary Content-Security-Policy-Report-Only header while testing changes.

Example:

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-abc123' 'strict-dynamic'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; report-to csp-endpoint

And define a reporting group:

Reporting-Endpoints: csp-endpoint="https://example.com/api/csp-reports"

Server example for collecting reports:

app.post("/api/csp-reports", express.json({ type: ["application/json", "application/reports+json"] }), (req, res) => {
  console.log("CSP report:", JSON.stringify(req.body, null, 2));
  res.sendStatus(204);
});

Official references:

During the audit, click through:

  • auth flows
  • checkout/forms
  • consent banners
  • analytics-triggering pages
  • admin pages
  • error states
  • anything loaded after user interaction

CSP problems love hiding behind one modal opened only on Safari after login.


9. Watch for fake-hardening patterns

These show up all the time:

Huge default-src plus barely any specific directives

Feels secure, usually isn’t.

Nonce present, but inline scripts missing the nonce

The policy is modern, the templates are not.

'strict-dynamic' present, but nobody understands it

That’s how policies rot.

style-src 'unsafe-inline' accepted forever

Usually because removing it sounds annoying. It is annoying. Still worth auditing.

Allowlists copied from vendor docs without validation

Vendor docs often optimize for “make it work,” not “minimize trust.”


10. A practical audit checklist

Use this as a copy-paste review list.

[ ] object-src is set to 'none'
[ ] base-uri is set and minimal
[ ] frame-ancestors is set appropriately
[ ] form-action is restricted
[ ] script-src does not use 'unsafe-inline'
[ ] script-src does not use 'unsafe-eval' unless truly required
[ ] nonce values are unique per response
[ ] 'strict-dynamic' usage is understood and intentional
[ ] style-src 'unsafe-inline' is tracked as debt or removed
[ ] wildcard domains are documented and justified
[ ] img-src is not broader than necessary
[ ] connect-src covers real API/WebSocket/reporting needs only
[ ] missing directives (worker-src, manifest-src, media-src) were reviewed
[ ] policy tested in Report-Only mode
[ ] violation reports are collected and reviewed
[ ] every third-party origin has an owner and a reason

11. What I’d say about the example policy

My short audit of the headertest.com policy:

Strong parts

  • Good use of object-src 'none'
  • Good anti-framing with frame-ancestors 'none'
  • Nonce-based script control
  • No unsafe-eval
  • Reasonable separation of directives

Things I’d review next

  • Why style-src 'unsafe-inline' is still required
  • Whether script-src host allowlists are still needed with 'strict-dynamic'
  • Whether img-src https: is broader than necessary
  • Whether every connect-src origin is still active and owned
  • Whether missing directives should be explicit

That’s a healthy CSP, but not a finished one. Most CSPs aren’t finished. They’re living infrastructure.

If you want to tighten yours, start with the stuff that actually moves risk: remove unsafe script behavior, validate nonce generation, cut unnecessary third-party trust, and treat inline styles as a cleanup project instead of a permanent exception.