Skip to main content

Astro Advanced

Content Security Policy

What c15t adds to the page

A Content Security Policy for an Astro site with c15t has to allow these:

WhatWhere it comes fromDirective
The color-scheme scriptInline, in <head>, with colorScheme 'system' or 'dark'script-src
The banner reveal scriptInline, after a banner on a prerendered pagescript-src
<style data-c15t-styles="c15t-first-paint">, the banner's rulesInline, in <head> from ConsentScript, or from the banner on a layout without it, unless styles: false or the site uses Tailwind CSS 3style-src
<style data-c15t-styles="c15t-iab-first-paint">, the IAB banner's rulesInline alongside the base rules when IAB is configuredstyle-src
<style id="c15t-theme">Inline, when the integration sets themestyle-src
The page script that starts the consent runtime, and the banner's click handlerBundled module scripts from your own originscript-src 'self'
The dialog's stylesheet, IAB panel rules for the IAB dialog, and Svelte primitivesStylesheets from your own origin, linked before the dialog mountsstyle-src 'self'
Full base and configured IAB stylesheets on a Tailwind CSS 3 site, or the stylesheets you import with styles: falseStylesheets from your own originstyle-src 'self'
Vendor <script> elementsCreated by the script loader when a category is allowedscript-src, plus each vendor's hosts
Gated inline scriptsYour own type="text/plain" scripts, copied into live scriptsscript-src

The per-visitor boot payload is not on this list. ConsentScript and the banners render it as a <script type="application/json" data-c15t-config> data block. The browser never runs a data block, so a policy does not need to allow it.

The dialog islands can render style attributes, for example on animated sections. If your policy blocks style attributes, allow them with a separate style-src-attr 'unsafe-inline' directive.

Choose one way to allow the inline code:

PolicyUse it when
A nonce from your own middlewarePages render on each request, and you set the policy header yourself
Astro's CSP, security.cspYou want Astro to write the policy, including on prerendered pages, and the site does not use ClientRouter

Allow c15t with a nonce

Set Astro.locals.c15t.nonce in your own middleware. The c15t middleware runs first, with order: 'pre', so Astro.locals.c15t exists by the time yours runs. Generate a nonce per request, set it, render, then send the policy header with the same nonce:

src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

export const onRequest = defineMiddleware(async (context, next) => {
	const nonce = crypto.randomUUID();
	if (context.locals.c15t) {
		context.locals.c15t.nonce = nonce;
	}
	const response = await next();
	response.headers.set(
		'content-security-policy',
		`script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'`
	);
	return response;
});

With the nonce set, every inline <script> and <style> that ConsentScript, ConsentBanner and IABConsentBanner render carries it. The browser runtime reads the nonce from the data-c15t-config data block and puts it on:

  • The vendor scripts the script loader injects, unless a script entry sets its own nonce.
  • The gated type="text/plain" scripts it activates.
  • The stylesheet links it adds the first time a dialog opens.

Put the nonce on your gated scripts

When the page has a nonce, c15t activates a gated type="text/plain" script only if the tag carries that same nonce. Add it to each gated tag you write:

src/pages/index.astro (partial)
<script
  is:inline
  type="text/plain"
  data-c15t-category="measurement"
  nonce={Astro.locals.c15t?.nonce}
>
  console.log('Measurement allowed');
</script>

c15t skips a gated tag without the nonce, logs a console warning, marks the tag data-c15t-activated="untrusted" and never tries it again. The check exists because c15t activates a tag by creating a new <script>, and a policy with 'strict-dynamic' runs a script that trusted code creates without checking its nonce. Without the check, a tag injected through an HTML injection hole would run once its category was allowed.

Gated tags you insert later from your own code need the nonce too. When you call activateGatedScripts(snapshot, container, nonce) from c15t/astro/client for that markup, pass the page's nonce as the third argument so the same check applies. Pages without a nonce activate every gated tag.

A nonce only works on pages rendered on each request. A prerendered page is built once, so every visitor would share one nonce. Use Astro's CSP for prerendered pages.

The nonce covers what the c15t components render. Scripts and stylesheets of your own that Astro inlines need the nonce or a hash from you.

Pass the nonce to ConsentBanner and ConsentBannerDeferred

ConsentBanner reads Astro.locals.c15t.nonce by default. Its nonce prop sets the value for that banner only.

ConsentBannerDeferred passes the page's nonce to its server island. The island renders in a request of its own, but the page's policy is the one that applies to the markup the island inserts.

Astro loads a server island with inline scripts that carry no nonce, and one of them changes on every render. A nonce policy set from middleware therefore blocks the loader, and the deferred banner never arrives. On pages with ConsentBannerDeferred, use Astro's CSP, which hashes the loader on each render, or render ConsentBanner instead.

Allow c15t with Astro's CSP

Turn on Astro's CSP with security.csp, or experimental.csp in Astro 5:

astro.config.mjs (partial)
export default defineConfig({
	security: { csp: true },
	integrations: [c15t()],
});

The c15t integration adds the hashes of the inline code it can render to that policy, with the hash algorithm you configured:

  • The color-scheme script.
  • The reveal scripts of ConsentBanner and IABConsentBanner.
  • Base and configured IAB banner rules, unless styles: false or Tailwind CSS 3 uses external stylesheets.
  • The theme stylesheet.
  • The inline textContent of each entry in the integration's scripts option, which the script loader injects.

You do not need 'unsafe-inline'. Leave it out, because Astro drops the hashes from a directive that lists it.

Some code still needs your own entry in the policy:

  • Gated type="text/plain" scripts you write yourself. Add the hash of each one's text to security.csp.scriptDirective.hashes.
  • Scripts registered from the clientEntrypoint module. The integration cannot see them when it computes the hashes, so add the hash of each inline one yourself. With Astro's CSP on, c15t logs a console error that names each inline entrypoint script the policy is missing and the hash to add.
  • Vendor script hosts. Add them to security.csp.scriptDirective.resources, together with 'self', which a custom list no longer includes.

Astro's CSP does not support ClientRouter. On a site that uses it, allow the inline code with a nonce instead.

The browser sends consent to ${backendURL}/subjects, and requests /init when the server did not resolve the policy. Allow those origins:

ModeBrowser requests go to
manifest(), the defaultYour own origin for /api/c15t/init, and the backend origin for /subjects
manifest({ resolve: 'browser' })Your own origin for /api/c15t/manifest, the backend origin for /subjects, and geoURL if set
hosted()The backend origin, for /init and /subjects
offline()No consent backend
Content-Security-Policy (partial)
connect-src 'self' https://your-project.inth.app;

Replace https://your-project.inth.app with the URL from your Inth project or your self-hosted backend. Server-side requests from the middleware and the injected routes run in Astro and are not subject to the browser's policy. Under an IAB TCF policy, the browser fetches the vendor list from the /init route or the URL the policy names. Allow that host too.

Vendors add their own hosts to script-src, connect-src, img-src and frame-src. Each integration guide lists the requests to expect.

Check your policy

  1. Load a server-rendered page with the policy enforced, not report-only, and open the DevTools Console. No Refused to execute inline script or Refused to apply inline style error appears.
  2. View the page source. With a nonce policy, the color-scheme <script>, <style id="c15t-theme"> and <script type="application/json" data-c15t-config> carry a nonce attribute that matches the content-security-policy response header. The Elements panel shows the attribute empty, because browsers hide a nonce's value after checking it.
  3. The banner renders with your theme colors, and its buttons record a choice.
  4. Open the preference dialog. It is styled, and the Console shows no style-src violation.
  5. Allow a category that gates a vendor. The vendor's script loads, and no connect-src or script-src violation mentions its host.
  6. Save a choice. The request to /subjects succeeds in DevTools Network.