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.cominscript-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-srcmanifest-srcworker-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-srchost allowlists are still needed with'strict-dynamic' - Whether
img-src https:is broader than necessary - Whether every
connect-srcorigin 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.