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:
| What | Where it comes from | Directive |
|---|---|---|
| The color-scheme script | Inline, in <head>, with colorScheme 'system' or 'dark' | script-src |
| The banner reveal script | Inline, after a banner on a prerendered page | script-src |
<style data-c15t-styles="c15t-first-paint">, the banner's rules | Inline, in <head> from ConsentScript, or from the banner on a layout without it, unless styles: false or the site uses Tailwind CSS 3 | style-src |
<style data-c15t-styles="c15t-iab-first-paint">, the IAB banner's rules | Inline alongside the base rules when IAB is configured | style-src |
<style id="c15t-theme"> | Inline, when the integration sets theme | style-src |
| The page script that starts the consent runtime, and the banner's click handler | Bundled module scripts from your own origin | script-src 'self' |
| The dialog's stylesheet, IAB panel rules for the IAB dialog, and Svelte primitives | Stylesheets from your own origin, linked before the dialog mounts | style-src 'self' |
Full base and configured IAB stylesheets on a Tailwind CSS 3 site, or the stylesheets you import with styles: false | Stylesheets from your own origin | style-src 'self' |
Vendor <script> elements | Created by the script loader when a category is allowed | script-src, plus each vendor's hosts |
| Gated inline scripts | Your own type="text/plain" scripts, copied into live scripts | script-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:
| Policy | Use it when |
|---|---|
| A nonce from your own middleware | Pages render on each request, and you set the policy header yourself |
Astro's CSP, security.csp | You 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:
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:
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:
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
ConsentBannerandIABConsentBanner. - Base and configured IAB banner rules, unless
styles: falseor Tailwind CSS 3 uses external stylesheets. - The theme stylesheet.
- The inline
textContentof each entry in the integration'sscriptsoption, 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 tosecurity.csp.scriptDirective.hashes. - Scripts registered from the
clientEntrypointmodule. 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.
Allow the consent backend in connect-src
The browser sends consent to ${backendURL}/subjects, and requests /init
when the server did not resolve the policy. Allow those origins:
| Mode | Browser requests go to |
|---|---|
manifest(), the default | Your 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 |
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
- Load a server-rendered page with the policy enforced, not report-only, and
open the DevTools Console. No
Refused to execute inline scriptorRefused to apply inline styleerror appears. - 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 anonceattribute that matches thecontent-security-policyresponse header. The Elements panel shows the attribute empty, because browsers hide a nonce's value after checking it. - The banner renders with your theme colors, and its buttons record a choice.
- Open the preference dialog. It is styled, and the Console shows no
style-srcviolation. - Allow a category that gates a vendor. The vendor's script loads, and no
connect-srcorscript-srcviolation mentions its host. - Save a choice. The request to
/subjectssucceeds in DevTools Network.