CSP for OneTrust cookie consent: a real-world fix
Table of Contents
Cookie consent banners are one of the fastest ways to wreck an otherwise clean Content Security Policy.
I’ve seen teams lock down script-src, remove unsafe-inline, pat themselves on the back, then ship OneTrust and immediately flood the console with CSP violations. The banner does not show, consent does not save, analytics gets weird, and somebody suggests adding https: everywhere until the errors go away. That usually “works,” and also defeats half the reason you had a CSP in the first place.
Here’s a more useful approach: treat OneTrust like any other third-party app. Start from what it actually loads, allow only what it needs, and verify the result in production.
The problem
A typical OneTrust install looks simple enough:
<script
src="https://cdn.cookielaw.org/scripttemplates/otSDKStub.js"
type="text/javascript"
charset="UTF-8"
data-domain-script="YOUR-DOMAIN-SCRIPT-ID">
</script>
<script type="text/javascript">
function OptanonWrapper() {
// runs after consent loads
}
</script>
That tiny snippet hides a bunch of moving parts:
- external script loading from OneTrust domains
- inline JavaScript via
OptanonWrapper - consent API calls
- geolocation and config fetches
- banner UI styles
- possibly iframes, depending on setup
- downstream integrations like Google Tag Manager
If your CSP was written before consent tooling was added, it probably blocks at least one of those.
A real baseline from the wild
A useful real-world reference is the CSP header observed on headertest.com:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-ODQxZmEyOWMtOTE3Zi00MmI3LThmODAtZjU0YzhjYjgxZWFl' '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 one is for Cookiebot, not OneTrust, but it shows the pattern clearly: consent platforms expand your CSP in multiple directives, not just script-src.
OneTrust behaves the same way. If you only patch script-src, you’re probably not done.
Before: the broken policy
Here’s the kind of CSP I usually inherit:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
connect-src 'self';
font-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
And here’s the matching OneTrust embed:
<script
src="https://cdn.cookielaw.org/scripttemplates/otSDKStub.js"
data-domain-script="12345678-abcd-1234-abcd-1234567890ab">
</script>
<script>
function OptanonWrapper() {
console.log('Consent ready');
}
</script>
What breaks?
Typical console errors look like this:
Refused to load the script 'https://cdn.cookielaw.org/scripttemplates/otSDKStub.js'
because it violates the following Content Security Policy directive: "script-src 'self'".
Refused to execute inline script because it violates the following Content Security Policy directive:
"script-src 'self'".
Refused to connect to 'https://cdn.cookielaw.org/consent/...'
because it violates the following Content Security Policy directive: "connect-src 'self'".
Refused to apply inline style because it violates the following Content Security Policy directive:
"style-src 'self'".
That’s the normal failure mode: external script blocked, inline wrapper blocked, XHR/fetch blocked, and banner styling blocked.
The lazy fix that I don’t recommend
A lot of teams do this:
Content-Security-Policy:
default-src 'self' https:;
script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
style-src 'self' 'unsafe-inline' https:;
img-src 'self' data: https:;
connect-src 'self' https:;
frame-src 'self' https:;
Yes, the banner will probably load.
No, this is not a good policy.
You’ve basically converted CSP from a defense into a checkbox. https: in script-src is wildly permissive. unsafe-inline for scripts means injected inline JS can run. unsafe-eval is often there because somebody got desperate.
I’ve done emergency rollbacks like this during incidents. I would not leave it in place.
After: a production-friendly OneTrust CSP
A better policy is explicit and boring. That’s what you want.
Here’s a practical starting point for OneTrust:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' https://cdn.cookielaw.org;
style-src 'self' 'unsafe-inline' https://cdn.cookielaw.org;
img-src 'self' data: https://cdn.cookielaw.org;
font-src 'self' data:;
connect-src 'self' https://cdn.cookielaw.org;
frame-src 'self' https://cdn.cookielaw.org;
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none';
And the updated markup:
<script
nonce="{{ .CSPNonce }}"
src="https://cdn.cookielaw.org/scripttemplates/otSDKStub.js"
data-domain-script="12345678-abcd-1234-abcd-1234567890ab">
</script>
<script nonce="{{ .CSPNonce }}">
function OptanonWrapper() {
console.log('Consent ready');
}
</script>
This is the core fix:
- allow the OneTrust script host in
script-src - use a nonce for your inline
OptanonWrapper - allow OneTrust API/config calls in
connect-src - allow whatever styles the banner injects
- keep everything else tight
If your stack supports nonces, use them. For developer-owned inline JS, nonces are much cleaner than falling back to unsafe-inline.
Why style-src 'unsafe-inline' may still be necessary
I don’t love unsafe-inline anywhere, but style-src is where many teams end up compromising for third-party widgets. Consent tools often inject styles dynamically or depend on inline style attributes. You can try hashes or strict style nonces if your integration is stable, but in practice many teams accept:
style-src 'self' 'unsafe-inline' https://cdn.cookielaw.org;
That’s not perfect, but it’s a lot better than throwing unsafe-inline into script-src.
The policy usually grows once GTM enters the room
The minute marketing asks for Google Tag Manager after consent is granted, your CSP stops being “just OneTrust.”
A more realistic version looks like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://cdn.cookielaw.org https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline' https://cdn.cookielaw.org;
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://cdn.cookielaw.org https://www.google-analytics.com https://region1.google-analytics.com https://www.googletagmanager.com;
frame-src 'self' https://cdn.cookielaw.org;
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none';
This is where that real Cookiebot example is useful. The shape is similar:
- consent vendor domains in
script-src,connect-src,frame-src - analytics domains in
connect-src - tag manager in
script-src
If you want ready-made patterns for combining CSP with common third-party services, the examples at https://csp-examples.com are handy. For OneTrust-specific behavior, check the official OneTrust implementation docs and validate against what your tenant actually loads.
Report-Only first, always
If you’re changing CSP around consent, deploy in Content-Security-Policy-Report-Only first.
Example:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' https://cdn.cookielaw.org;
style-src 'self' 'unsafe-inline' https://cdn.cookielaw.org;
img-src 'self' data: https://cdn.cookielaw.org;
connect-src 'self' https://cdn.cookielaw.org;
frame-src 'self' https://cdn.cookielaw.org;
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none';
Then test these flows:
- first visit with no consent
- accept all
- reject all
- reopen preferences center
- region-specific behavior if geolocation changes the banner
- GTM/analytics firing only after consent
This catches the usual “works on homepage, breaks in modal reopen” bug.
Common mistakes I keep seeing
1. Forgetting the inline wrapper
This one gets people constantly:
<script>
function OptanonWrapper() {}
</script>
If you don’t nonce or hash that script, CSP blocks it even if the main OneTrust loader is allowed.
2. Allowing the script host but not connect-src
OneTrust often needs network calls for config and consent state. If the script loads but the UI hangs, connect-src is usually the next place to look.
3. Using default-src as a crutch
Don’t assume default-src will carry you. Be explicit in the directives that matter: script-src, style-src, connect-src, and frame-src.
4. Over-allowing all of Google
If you add GTM later, don’t jump straight to https://*.google.com https://*.google-analytics.com https://*.googletagmanager.com everywhere. Add only the domains you actually observe.
A sane final version
If I were shipping a modern site with OneTrust and a small set of post-consent Google services, I’d start here:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://cdn.cookielaw.org https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline' https://cdn.cookielaw.org;
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://cdn.cookielaw.org https://www.googletagmanager.com https://www.google-analytics.com https://region1.google-analytics.com;
frame-src 'self' https://cdn.cookielaw.org;
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none';
Then I’d tighten it based on actual reports, not guesswork.
That’s the whole game with CSP and cookie consent: don’t cargo-cult someone else’s giant allowlist, and don’t panic-allow https: just because the banner is broken. Start small, watch the violations, and let the policy grow only where the integration proves it has to.