Skip to content
S W SafeWorld
WorkServicesHostingNotes
Sign inCreate account
← Notes

A Content Security Policy that broke our own logo

19 August 2026 · 4 min read

We wanted this site to have a genuinely strict Content Security Policy — not the kind that technically exists but permits everything. The first version looked reasonable:

Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'

Every header scanner was happy. The site looked fine at a glance. Two things were broken anyway.

Inline JSON-LD is still a script

The first problem was structured data. Search engines read an Organization block embedded in the page:

<script type="application/ld+json">{ "@type": "Organization", ... }</script>

That is a <script> element. CSP does not care that the browser will never execute it as JavaScript — script-src 'self' forbids inline script content, and the block is inline. Under a strict policy the structured data is refused, which means the SEO work quietly stops existing.

The fix is a hash. Astro’s built-in CSP support computes a SHA-256 for every inline block it renders and emits them into the policy, so the JSON-LD is allowed by its exact content rather than by a blanket exception.

Hashes do not cover style attributes

The second problem was subtler, and it is the one worth remembering.

Our logo component sized itself with a CSS custom property on the element:

<span style="--sw-mark-h:24px;--sw-mark-w:14px">

CSP hashes apply to inline <style> blocks. They do not apply to style="" attributes. There is no hash you can add that permits one. The only options are 'unsafe-inline', 'unsafe-hashes', or not using the attribute.

So the declaration was blocked, the custom properties never landed, and the SVG fell back to its intrinsic size. The logo rendered at the wrong dimensions in the header of every page. Nothing errored. Nothing looked obviously broken. The scanners still gave the site full marks, because a scanner reads headers — it does not open the page and check whether the design survived.

The fix was to stop using the attribute at all. Size now goes onto the SVG’s own width and height attributes, which are HTML, not CSS, and are unaffected by style-src:

const sized = markSvg.replace(/<svg\b/, `<svg width="${width}" height="${size}"`)

The stats grid had the same bug, for the same reason, and was fixed by moving the value into a class.

What we changed about testing

The lesson is not “CSP is hard”. It is that a passing header scan proves the header is present, not that the site still works. Those are different claims, and only one of them is what you actually care about.

Now the check loads the real pages in a headless browser and fails on any console violation:

chrome --headless --enable-logging=stderr --virtual-time-budget=6000 \
  --dump-dom "https://safeworldstudios.com/" 2>log
grep -iE "violates|refused to" log

That catches the class of bug where the policy is technically correct and the page is quietly wrong. It found both problems above in about thirty seconds, after the header scanners had already told us everything was fine.

You can see the resulting policy on the security page, and check it against what this site actually sends.

SafeWorld

An independent software studio, founded 2025.

Work
  • Sevoria
  • Aria Lab
  • MOSAIC
  • All projects
Company
  • About
  • Services
  • Hosting
  • Notes
  • Contact
Legal
  • Security
  • Privacy
  • Terms
  • Cookies
© 2026 SafeWorld Studioshello@safeworldstudios.com