Cookie consent banners are one of the easiest ways to blow up an otherwise clean Content Security Policy.
I’ve seen teams lock down script-src, remove inline JS, add nonces everywhere, and then ship a consent platform that quietly needs half a dozen extra hosts, a stylesheet exception, iframe support, and a callback script jammed into the page head. Suddenly the CSP report inbox catches fire.
This guide is the practical version: what to allow, where teams usually get it wrong, and copy-paste CSP examples for OneTrust and Osano.
If you want a refresher on directive behavior, csp-guide.com is a solid reference.
The problem with consent managers and CSP
Consent platforms usually need some combination of:
- a remote bootstrap script
- inline initialization code
- remote CSS
- API calls back to the vendor
- an iframe or preference center modal
- integrations with tag managers and analytics tools
That means they commonly touch:
script-srcstyle-srcconnect-srcimg-srcframe-src- sometimes
font-src
If your site uses nonce-based CSP with strict-dynamic, you need to be especially careful. A consent script often loads other scripts dynamically, and that changes how host allowlists behave.
A real-world CSP example
Here’s a real CSP header seen on headertest.com, using Cookiebot plus Google services:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-NTgwNDMwNWItODNhMi00ODYxLWIyNTgtNjg0Nzg0YTE5NzMz' '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://collect.tallytics.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'
A few things I like here:
object-src 'none'base-uri 'self'form-action 'self'frame-ancestors 'none'
A few things I’d review carefully:
style-src 'unsafe-inline'- broad wildcards like
https://*.cookiebot.com default-srccarrying service hosts that probably belong in more specific directives
Consent tools tend to push teams toward broader policies than they actually need.
Baseline CSP pattern for consent banners
Here’s a good starting point if you already run a modern nonce-based CSP:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self' 'nonce-{RANDOM_NONCE}';
img-src 'self' data: https:;
connect-src 'self';
font-src 'self';
frame-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
report-to csp-endpoint;
report-uri /csp-report;
Then add only the hosts your consent platform actually uses.
If your CMP forces inline styles and you can’t nonce them, you may need 'unsafe-inline' in style-src. I don’t love that, but it’s still much better than relaxing script-src.
OneTrust CSP example
OneTrust deployments vary a lot. The exact hostnames depend on your tenant setup, geolocation rules, and whether you load templates or preference centers from OneTrust CDN endpoints.
This is the pattern I usually start from and then trim using reports.
Example: OneTrust with nonce-based CSP
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic'
https://cdn.cookielaw.org
https://cmp-cdn.cookielaw.org;
style-src 'self' 'unsafe-inline'
https://cdn.cookielaw.org
https://cmp-cdn.cookielaw.org;
img-src 'self' data: https:;
font-src 'self' https://cdn.cookielaw.org;
connect-src 'self'
https://cdn.cookielaw.org
https://cmp-cdn.cookielaw.org
https://geolocation.onetrust.com;
frame-src 'self'
https://cdn.cookielaw.org
https://cmp-cdn.cookielaw.org;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Script tag example
<script
src="https://cdn.cookielaw.org/scripttemplates/otSDKStub.js"
data-domain-script="YOUR-DOMAIN-SCRIPT-ID"
nonce="{{ .CSPNonce }}">
</script>
<script nonce="{{ .CSPNonce }}">
function OptanonWrapper() {
console.log("OneTrust loaded");
}
</script>
A couple of practical notes:
- If you use
strict-dynamic, the nonce on the bootstrap script matters. - If OneTrust injects child scripts dynamically, that nonce-based trust chain is usually what makes the setup work cleanly.
- Many OneTrust installs still end up needing
'unsafe-inline'instyle-src. That’s common and usually the least bad compromise.
Nginx example
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'nonce-$request_id' 'strict-dynamic' https://cdn.cookielaw.org https://cmp-cdn.cookielaw.org;
style-src 'self' 'unsafe-inline' https://cdn.cookielaw.org https://cmp-cdn.cookielaw.org;
img-src 'self' data: https:;
font-src 'self' https://cdn.cookielaw.org;
connect-src 'self' https://cdn.cookielaw.org https://cmp-cdn.cookielaw.org https://geolocation.onetrust.com;
frame-src 'self' https://cdn.cookielaw.org https://cmp-cdn.cookielaw.org;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
" always;
I wouldn’t use $request_id as a production nonce without checking how it’s generated in your stack. Treat this as a shape example, not a cryptographic recommendation.
Osano CSP example
Osano is usually a bit simpler, but the same rule applies: start narrow and expand only when reports prove you need more.
Example: Osano with remote script and API access
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic'
https://cmp.osano.com;
style-src 'self' 'unsafe-inline'
https://cmp.osano.com;
img-src 'self' data: https:;
font-src 'self' https://cmp.osano.com;
connect-src 'self'
https://cmp.osano.com;
frame-src 'self'
https://cmp.osano.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Script tag example
<script
src="https://cmp.osano.com/AzyExample12345/osano.js"
nonce="{{ .CSPNonce }}">
</script>
If you register consent callbacks inline, nonce them too:
<script nonce="{{ .CSPNonce }}">
window.addEventListener("osano-cm-initialized", function () {
console.log("Osano initialized");
});
</script>
Report-Only first, always
If you’re rolling out OneTrust or Osano on a site with an existing CSP, use Content-Security-Policy-Report-Only first.
Example:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://cdn.cookielaw.org https://cmp.osano.com;
style-src 'self' 'unsafe-inline' https://cdn.cookielaw.org https://cmp.osano.com;
connect-src 'self' https://cdn.cookielaw.org https://cmp.osano.com https://geolocation.onetrust.com;
frame-src 'self' https://cdn.cookielaw.org https://cmp.osano.com;
report-uri /csp-report;
Do this before you touch production enforcement. Consent tools often load region-specific assets, and those don’t always show up in local testing.
Common breakages
These are the failures I see most often.
1. Missing nonce on the bootstrap script
If your policy uses:
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
but your CMP script tag has no nonce, the browser blocks it.
Bad:
<script src="https://cdn.cookielaw.org/scripttemplates/otSDKStub.js"></script>
Good:
<script
src="https://cdn.cookielaw.org/scripttemplates/otSDKStub.js"
nonce="{{ .CSPNonce }}">
</script>
2. Forgetting connect-src
The banner UI loads, but consent state never saves, geolocation fails, or the preference center breaks.
That’s usually connect-src, not script-src.
3. Allowing too much in default-src
I’m opinionated here: don’t dump CMP domains into default-src and call it done.
This:
default-src 'self' https://cdn.cookielaw.org https://cmp.osano.com;
is lazy. Be explicit:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://cdn.cookielaw.org https://cmp.osano.com;
connect-src 'self' https://cdn.cookielaw.org https://cmp.osano.com;
frame-src 'self' https://cdn.cookielaw.org https://cmp.osano.com;
4. Fighting style-src for too long
Some teams spend days trying to avoid 'unsafe-inline' in style-src for a consent banner. I get it. But if the alternative is weakening script execution rules or shipping a broken consent flow, take the style exception and move on.
I’d rather keep script-src hard and accept a narrowly scoped style compromise.
Minimal copy-paste templates
OneTrust minimal template
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://cdn.cookielaw.org https://cmp-cdn.cookielaw.org;
style-src 'self' 'unsafe-inline' https://cdn.cookielaw.org https://cmp-cdn.cookielaw.org;
connect-src 'self' https://cdn.cookielaw.org https://cmp-cdn.cookielaw.org https://geolocation.onetrust.com;
img-src 'self' data: https:;
font-src 'self' https://cdn.cookielaw.org;
frame-src 'self' https://cdn.cookielaw.org https://cmp-cdn.cookielaw.org;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Osano minimal template
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://cmp.osano.com;
style-src 'self' 'unsafe-inline' https://cmp.osano.com;
connect-src 'self' https://cmp.osano.com;
img-src 'self' data: https:;
font-src 'self' https://cmp.osano.com;
frame-src 'self' https://cmp.osano.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Final advice I’d actually use on a production site
My default approach for consent platforms is:
- keep
default-src 'self' - use nonces for all first-party inline scripts
- use
strict-dynamicif your app already supports it - add only vendor hosts required by reports
- accept
'unsafe-inline'instyle-srcif the CMP forces it - test geolocation, preference center, reject-all, accept-all, and consent persistence
- run Report-Only before enforcement
And don’t trust vendor snippets blindly. Paste them into a page with a strict CSP and watch what actually breaks. That’s usually where the real integration work starts.