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.

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-src
  • style-src
  • connect-src
  • img-src
  • frame-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-src carrying service hosts that probably belong in more specific directives

Consent tools tend to push teams toward broader policies than they actually need.

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' in style-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:

  1. keep default-src 'self'
  2. use nonces for all first-party inline scripts
  3. use strict-dynamic if your app already supports it
  4. add only vendor hosts required by reports
  5. accept 'unsafe-inline' in style-src if the CMP forces it
  6. test geolocation, preference center, reject-all, accept-all, and consent persistence
  7. 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.