Skip to main content

HTML Advanced

Content Security Policy

What c15t needs from your policy

The script tag loads one file, sends requests to your backend, adds a <style> element for the stock UI, and runs your gated scripts and iframes when they are allowed. Each needs a directive:

DirectiveAllowWhy
script-srcThe host that serves c15t.js, such as https://your-project.inth.app. Add https://cdn.jsdelivr.net for c15t.gpp.js, c15t.devtools.js, or a bundle you load from the CDN.The c15t bundle.
script-srcA nonce or hash for each inline scriptYour queued config and on calls, and inline gated snippets.
script-srcEach vendor's script originGated <script src> tags and scripts entries.
connect-srcYour backend's origin, such as https://your-project.inth.app/init, /subjects, and /manifest in manifest mode.
style-srcThe same nonce as the c15t tag, or the host you serve c15t.css fromThe stock UI's <style> element, or the stylesheet file.
frame-srcEach embed's origin, such as https://www.youtube-nocookie.comGated iframes.
img-src, connect-srcEach vendor's collection endpointsWhat the vendor scripts send once they run.

A missing connect-src entry is the most common failure. The /init request fails with a CSP error in the console, no policy resolves, and no banner shows.

Use a nonce

With a nonce-based policy, put the nonce on the c15t tag, on every inline script that talks to window.c15t, and on every gated inline script:

<script nonce="RANDOM_PER_RESPONSE">
  window.c15t = window.c15t || [];
  c15t.push(['config', { legalLinks: { privacyPolicy: { href: '/privacy' } } }]);
</script>
<script
  nonce="RANDOM_PER_RESPONSE"
  src="https://your-project.inth.app/c15t.js"
  defer
></script>
<script nonce="RANDOM_PER_RESPONSE" type="text/plain" data-c15t-category="measurement">
  // The vendor's snippet
</script>

Your server generates RANDOM_PER_RESPONSE for each response and sends the same value in the header, such as Content-Security-Policy: script-src 'nonce-RANDOM_PER_RESPONSE' 'strict-dynamic'; style-src 'nonce-RANDOM_PER_RESPONSE'. A static host that cannot vary the response cannot use a nonce; list origins and hashes instead.

The c15t tag reads its own nonce and puts it on the stock UI's <style> element and on every script that a scripts entry in config loads. An entry's own nonce wins. To give c15t a nonce without putting it on the tag, for example when script-src allows the tag by its origin, set data-nonce="RANDOM_PER_RESPONSE" on the tag instead. data-nonce wins over the tag's nonce.

Once the c15t tag has a nonce, c15t runs only the gated text/plain tags that carry the same nonce. c15t runs a gated tag by creating a new <script>, and 'strict-dynamic' lets scripts that trusted code creates run without a nonce, so without this check a tag injected into your page would run once its category was allowed. c15t skips a gated tag without the nonce, logs a console warning and marks the tag data-c15t-activated="untrusted". It never copies the nonce onto a gated tag, so add nonce="RANDOM_PER_RESPONSE" to every gated tag you write, including tags your own code inserts later.

When c15t runs a gated tag, it copies that tag's nonce to the tag it creates, so the browser accepts the snippet. With 'strict-dynamic', scripts that c15t creates are trusted because the c15t tag itself carries the nonce. Without 'strict-dynamic', each vendor origin must be in script-src.

Allow the stock UI's styles

c15t.js renders its CSS in a <style> element inside the UI's shadow root. Theme tokens and ui.css go into the same element. The element carries the nonce from the c15t tag, or from data-nonce, so style-src 'nonce-RANDOM_PER_RESPONSE' allows it without 'unsafe-inline'.

A host that cannot send a nonce can render into the page and serve the stylesheet as a file instead:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@c15t/browser@alpha/dist/c15t.css" />
<script>
  window.c15t = window.c15t || [];
  c15t.push(['config', { ui: { shadow: false, styles: false } }]);
</script>

With styles: false and no theme or css option, c15t adds no <style> element. Put your token overrides in your own stylesheet. Your site's CSS now reaches the banner, so check it after the switch. Use the same version for the stylesheet and the script.

c15t.headless.js adds no styles at all. The DevTools panel adds its own <style> element, so remove its tag before you apply a strict policy.

IAB TCF

c15t.iab.js loads the Global Vendor List through your backend in hosted mode, so the backend's origin in connect-src covers it. It adds dist/c15t.iab.css rules to the same <style> element as the stock UI.

Check it works

  1. Send your policy as Content-Security-Policy-Report-Only first, and open the page with the console open.
  2. The console shows no CSP violation for the c15t tag, /init or the banner's styles. The banner renders styled.
  3. Allow each category. Each vendor's script and requests load without a violation.
  4. Switch the header to Content-Security-Policy and repeat.