Skip to content

Web Development · Frameworks

Astro Content Security Policy: Configuring security.csp

Astro's stable CSP hashes its scripts and styles into a per-page meta tag, with no nonces. How to set up security.csp in Astro 6 and 7, and where it stops.

Anurag Verma

Anurag Verma

7 min read · Updated Oct 19, 2026

Floating blue crystal islands in a dark space, each glowing softly and linked to the others by thin threads of light

Sponsored

Share

The first version of this post was written against the Astro 6 beta and described a nonce-based CSP design. That isn’t what shipped. Stable Astro uses hashes, and the policy lives in a meta tag. This is a rewrite for the feature as it exists in Astro 6 and 7, which is the version you’ll actually configure.

How does Astro’s CSP work?

Astro 6.0 made Content Security Policy stable under security.csp. When enabled, Astro hashes every script and style it emits and writes a <meta http-equiv="content-security-policy"> tag into each page. It uses hashes, not nonces, so it works on static pages as well as on-demand ones. It’s off in astro dev.

That’s the whole mental model. The rest of this post covers configuration, the runtime API, and the places where it won’t do what you expect.

Why hashes instead of nonces

A nonce-based policy needs a fresh, unpredictable value on every response. MDN spells out the consequence: with nonces, “the server cannot serve static HTML, because it must insert a new nonce each time.” That rules out prerendered pages served from a CDN, which is most of what Astro sites are.

Hashes don’t have that problem. A hash identifies the exact bytes of a script, so the policy and the page can both be static. Astro computes hashes at build time for prerendered pages. For on-demand pages, whose content can differ per request, the Astro 6 release post says Astro hashes at request time and injects the result.

The tradeoff is the one MDN also notes: change a script and its hash changes too. That’s fine when Astro owns the script, because it recomputes on every build. It’s a chore for third-party scripts you hash by hand.

Turning it on

Current Astro is 7.4 (released October 15, 2026). CSP has been stable since 6.0, so the config below works on either major.

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  security: {
    csp: true,
  },
});

With csp: true, Astro adds script-src and style-src directives containing hashes for the scripts and styles it processes. That covers hydration scripts for islands, bundled <script> tags in components, and component styles. The Astro 6 post also confirms that responsive images work without extra setup, because their styles are computed at build time and hashed like any other.

If you were on the Astro 5.9 experimental flag, move the config from experimental.csp to security.csp.

Configuring directives, hashes and resources

Pass an object instead of true to customize it.

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  security: {
    csp: {
      algorithm: 'SHA-384',
      directives: [
        "default-src 'self'",
        "img-src 'self' https://images.example.com",
        "connect-src 'self' https://api.example.com",
      ],
      scriptDirective: {
        resources: ["'self'", 'https://cdn.example.com'],
        hashes: ['sha384-yourExternalScriptHash'],
      },
      styleDirective: {
        resources: ["'self'"],
      },
    },
  },
});

Here is what each option does, per the configuration reference:

  • algorithm picks the hash function: 'SHA-256' (default), 'SHA-384' or 'SHA-512'.
  • directives is a list of extra directives applied to every page, on top of the script-src and style-src Astro writes.
  • scriptDirective.resources and styleDirective.resources replace Astro’s default sources. 'self' is not added for you, so include it if you want it. Forgetting this is the most common way to break a site on the first try.
  • scriptDirective.hashes and styleDirective.hashes add hashes for things Astro didn’t generate. Each must start with sha256-, sha384- or sha512-.
  • scriptDirective.strictDynamic adds 'strict-dynamic', which lets a trusted script load further scripts. MDN warns this weakens the policy if those scripts build <script> elements from untrusted input, so only turn it on if a loader script needs it.

To get the hash for an external file you pin, hash the exact bytes you serve:

openssl dgst -sha384 -binary vendor/widget.js | openssl base64 -A
# prefix the output with sha384-

Scoping with -elem and -attr (Astro 7.1+)

Astro 7.1 added support for script-src-elem, script-src-attr, style-src-elem and style-src-attr. Resource and hash entries can now be objects with a kind of 'element', 'attribute' or 'default'.

The practical use is allowing inline style="..." attributes without loosening the policy for stylesheets:

styleDirective: {
  resources: [
    "'self'",
    { resource: "'unsafe-inline'", kind: 'attribute' },
  ],
},

Two rules from the reference are easy to miss. Attribute-scoped sources are limited to 'none', 'unsafe-hashes', 'unsafe-inline' and 'report-sample'. And once a scoped directive exists, browsers use it instead of the generic one with no fallback. Astro moves its own generated hashes into the scoped directive, but it doesn’t move your generic resources, and it logs a warning when you mix the two.

Per-page changes with the runtime API

Some pages need something the rest of the site doesn’t, like a payment page that loads a provider’s script. Astro 6 added a csp object on Astro (in .astro files) and on the API context (in endpoints and middleware).

---
// src/pages/checkout.astro
Astro.csp?.insertScriptResource('https://js.payments.example.com');
Astro.csp?.insertDirective("frame-src https://js.payments.example.com");
---

The available methods are insertDirective(), insertScriptResource(), insertScriptHash(), insertStyleResource() and insertStyleHash(). Since 7.1, the resource and hash methods also accept the { resource, kind } and { hash, kind } object forms. Whatever you add is merged and deduplicated with the config into the page’s single meta tag. The csp property is typed as possibly undefined, hence the optional chaining.

Keep these calls narrow. A page-level exception is far easier to audit than a site-wide resource that was added for one page two years ago.

Limits to know before you ship

It does nothing in dev. CSP isn’t supported in astro dev. Test with astro build followed by astro preview, and put that in your CI checks so a broken policy doesn’t reach production unnoticed.

It’s a meta tag, not a header. MDN says meta delivery “does not support all CSP features,” and that a report-only policy can’t be delivered in a meta element. If you want report-only mode while you tune the policy, or violation reporting, set a Content-Security-Policy-Report-Only header yourself in your host config or middleware. Our HTTP security headers guide covers the header syntax.

<ClientRouter /> is unsupported. Astro’s client-side view transitions router is on the list of things CSP doesn’t work with. If you rely on it, you have to pick one or the other for now.

Shiki is unsupported too. Syntax highlighting with Shiki is also listed as incompatible. Code-heavy sites like docs and technical blogs should check this first.

External scripts and styles aren’t automatic. Astro only hashes what it processes. Anything from another origin needs a resource entry or a hash you supply.

A rollout order that avoids surprises

  1. Upgrade to Astro 6 or later (Astro 6 requires Node 22+).
  2. Turn on security: { csp: true } with no other options, build, and run astro preview.
  3. Open the browser console on your main page types. Every blocked resource shows up as a CSP violation with the directive that blocked it.
  4. Add origins to directives or the resources lists for each legitimate third party. Include 'self' whenever you set resources.
  5. Move one-off exceptions into Astro.csp calls on the specific pages that need them.
  6. If you need reporting, add a report-only header at the host while you watch for violations, then remove it.

The hash-based design is less flexible than nonces in one respect: it can’t vouch for a script it has never seen. In return you get a strict policy on fully static pages, which a nonce-based framework can’t give you. For most Astro sites, that’s the better trade.

Frequently asked questions

Does Astro use nonces for its Content Security Policy?
No. The stable `security.csp` feature uses hashes. Astro hashes the scripts and styles it generates (SHA-256 by default, SHA-384 or SHA-512 if you choose) and writes them into the policy. The official configuration and runtime API references describe hashes and resources only, with no nonce option.
Does Astro CSP work with static sites?
Yes, and that's the main advantage of hashes over nonces. A nonce has to change on every response, which a static HTML file can't do. Hashes stay the same as long as the script does, so Astro computes them at build time for prerendered pages and at request time for on-demand pages.
Why isn't my CSP showing up in astro dev?
CSP isn't supported in dev mode. Run `astro build` and then `astro preview` to test the policy.
Can I use Astro CSP with view transitions?
Not with `<ClientRouter />`. The configuration reference lists it as unsupported with CSP. Native cross-document view transitions in CSS don't depend on Astro's client router.
How do I allow a third-party script like an analytics tag?
Add its origin with `scriptDirective.resources` (remember to include `'self'`, because setting resources replaces Astro's defaults), or add its hash with `scriptDirective.hashes`. For one page only, call `Astro.csp.insertScriptResource()` or `Astro.csp.insertScriptHash()` in that page.

Sources

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored