A CSP of script-src 'self' blocks every inline <script> block on the page by default, including ones you wrote. On a static site with no server available to generate a per-request nonce, the fix is a hash: compute the SHA-256 hash of the exact script content and add it to the policy explicitly.

Key takeaways

  • 'self' does not mean "my own scripts." It means scripts loaded via a src attribute from your own domain — inline script is excluded by design, since inline script is exactly what an XSS payload injects.
  • Nonces don't work on a pure static site. They need a server to mint a fresh random value per response — a static HTML file has no such step.
  • Hashes are the static-site answer — a fingerprint of content that never changes, which is exactly what a static file guarantees.
  • The hash below is real, computed from this site's own anti-flash theme script, the same one running on every page you're reading this on.
  • Any edit to the script invalidates the hash. This is the actual maintenance cost — not implementing CSP once, but keeping the hash in sync with the script forever after.

Why "self" breaks inline scripts

script-src 'self' is a common first CSP policy, and it looks like it should just work — the script is on the same domain, after all. But CSP's 'self' keyword only covers scripts loaded via a src attribute pointing at your own origin. An inline <script>...</script> block has no src to match — it's excluded categorically, not because of where it lives but because of what it is. That's deliberate: unauthorized inline script is precisely the payload a cross-site scripting attack injects, so CSP's whole model treats "inline" as the thing to distrust by default.

A real worked example, not a placeholder

Every page on this site carries one small inline script in the <head>, before the stylesheet loads — it reads a stored theme choice from localStorage and applies it immediately, so nobody who picked dark mode sees a flash of the light theme on the next page load. It's 520 characters, does one thing, and never changes unless the theme system itself changes. That makes it the exact case a hash-based CSP is built for.

This site's actual inline theme script, hashed.
Property Value
Script length520 characters
Hash algorithmSHA-256
Computed hashsha256-6oplrr6UXKzzd9XzJqaE3iLEpvinpRxIv3FfSn7Cy7Y=

That hash was computed by taking the exact text between this site's own <script> and </script> tags, hashing it with SHA-256, and base64-encoding the result — not copied from a tutorial. A policy of script-src 'self' 'sha256-6oplrr6UXKzzd9XzJqaE3iLEpvinpRxIv3FfSn7Cy7Y=' would allow exactly this script to run and nothing else inline, which is the actual goal: not "no CSP" and not "allow all inline script," but an allowlist of one specific, known, unchanging block.

The part that actually costs you later. Edit this script — even whitespace — and its hash changes. The policy above would then reject the new version silently, and the anti-flash script would stop running, meaning the exact flash-of-wrong-theme bug the script exists to prevent would come back. Hash-based CSP isn't a one-time setup cost; it's a permanent coupling between the script's content and the policy that allows it.

Why hashes, not nonces, for a static site

Nonce-based CSP is the more common recommendation in general security writing, and for a server-rendered app it's usually the right call — the server mints a fresh random value on every response and stamps it onto both the CSP header and the script tag, so a cached or replayed old page can't reuse a stolen nonce. A static site has no server generating per-request anything. Every visitor gets the identical HTML file, so a nonce baked into that file would be the same for every single request — which defeats the entire point of a nonce being unpredictable per response. Hashes don't have this problem, because a static file's content is genuinely static; the hash is valid for as long as the content doesn't change, which for a static site is exactly the right lifetime.

How to get the hash for your own inline script

  1. Isolate the exact text between your script's opening and closing tags — whitespace and all, exactly as it appears in the file.
  2. Hash it with SHA-256 and base64-encode the digest.
  3. Prefix it with sha256- and add it to your script-src directive.
  4. Verify in the browser — deploy the policy, then check the console. A blocked script's CSP violation message includes the hash Chrome and Firefox actually expected, which is the fastest way to catch a mismatch without hand-computing it twice.
This site does not currently ship a CSP header. The hash above is real and correctly computed from the live script, but adding the header itself is server configuration this static file can't demonstrate on its own — worth being upfront about rather than implying more than what's actually deployed.
Curious what else on this site is measurably real vs. just claimed? The structured data worked example does the same thing for JSON-LD — this site's own live markup, not a hypothetical.

Badri Dutta

Software engineer · 15 years building for the web

Fifteen years building for the web. This exists because most CSP guides use a made-up example hash and never show where it actually came from — computing one from real, running code takes ten extra minutes and removes all the guesswork.

Full background →