Inline scripts are one of the easiest ways to accidentally punch a hole through your XSS defenses.

If you allow <script> blocks or inline event handlers without strict controls, an attacker only needs one HTML injection point to start running JavaScript in your users’ browsers. CSP hashes are one of the cleanest ways to keep a small amount of inline JavaScript while still blocking everything else.

This guide is the practical version: what hashes do, when to use them, how to generate them, and the exact headers to copy and paste.

What CSP hashes actually do

A CSP hash lets the browser run a specific inline script only if its contents exactly match a hash you list in script-src.

Example:

Content-Security-Policy: script-src 'self' 'sha256-AbCdEf123...';

If your page contains an inline script whose exact text hashes to AbCdEf123..., the browser runs it. If the script changes by even one character, it gets blocked.

That “exact text” part is where people get burned. Whitespace, line breaks, indentation, semicolons, templating output, and minification changes all matter.

When hashes are a good fit

Hashes work well when:

  • You have a small, fixed inline script
  • The script content is static at render time
  • You want a strict CSP without allowing all inline scripts
  • You don’t want to manage per-request nonces

Hashes are a bad fit when:

  • The inline script changes on every request
  • A framework injects dynamic values into the script block
  • You have lots of inline scripts that change during builds
  • You’re trying to permit inline event handlers like onclick

For dynamic pages, nonces are usually easier. For implementation details across different setups, csp-guide.com is a solid reference.

Basic example

Here’s a page with a tiny inline script:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Hash example</title>
</head>
<body>
  <script>
    window.appConfig = { theme: 'dark' };
  </script>
</body>
</html>

To allow that script with CSP, hash the exact contents inside the <script> tag:

window.appConfig = { theme: 'dark' };

Then send a header like this:

Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-vIsp2avtxDy0157AryO+jEJVpLdmka7PI7o7C4q5ABE='

That’s the whole trick.

How to generate a SHA-256 hash

You need the base64-encoded SHA-256 digest of the script contents.

Using OpenSSL

This is the one I reach for most often:

echo -n "window.appConfig = { theme: 'dark' };" | openssl dgst -sha256 -binary | openssl base64

Output:

vIsp2avtxDy0157AryO+jEJVpLdmka7PI7o7C4q5ABE=

Then add it to your header:

Content-Security-Policy: script-src 'self' 'sha256-vIsp2avtxDy0157AryO+jEJVpLdmka7PI7o7C4q5ABE='

Using Node.js

Useful in build scripts:

const crypto = require('crypto');

const script = "window.appConfig = { theme: 'dark' };";
const hash = crypto.createHash('sha256').update(script, 'utf8').digest('base64');

console.log(`'sha256-${hash}'`);

Using Python

import hashlib
import base64

script = "window.appConfig = { theme: 'dark' };"
digest = hashlib.sha256(script.encode("utf-8")).digest()
print("'sha256-" + base64.b64encode(digest).decode("ascii") + "'")

Copy-paste server examples

Nginx

add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-vIsp2avtxDy0157AryO+jEJVpLdmka7PI7o7C4q5ABE='" always;

Apache

Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-vIsp2avtxDy0157AryO+jEJVpLdmka7PI7o7C4q5ABE='"

Express.js with Helmet

const express = require('express');
const helmet = require('helmet');

const app = express();

app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: [
        "'self'",
        "'sha256-vIsp2avtxDy0157AryO+jEJVpLdmka7PI7o7C4q5ABE='"
      ]
    }
  })
);

app.get('/', (req, res) => {
  res.send(`
    <!doctype html>
    <html>
      <body>
        <script>window.appConfig = { theme: 'dark' };</script>
      </body>
    </html>
  `);
});

app.listen(3000);

Netlify _headers

/*
  Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-vIsp2avtxDy0157AryO+jEJVpLdmka7PI7o7C4q5ABE='

Multiple inline scripts

You can allow more than one inline script by listing multiple hashes.

Content-Security-Policy:
  default-src 'self';
  script-src 'self'
    'sha256-hashForScriptOne='
    'sha256-hashForScriptTwo='
    'sha256-hashForScriptThree='

Each inline script must match one of the listed hashes exactly.

What browsers hash

Browsers hash the contents of the script element, not the <script> tag itself.

This:

<script>
  console.log('hello');
</script>

Means you hash:

console.log('hello');

Not this:

<script>console.log('hello');</script>

That distinction matters a lot when you automate this.

The whitespace problem

Hashes are brittle by design. That’s good for security, annoying for deployment.

These are different scripts and produce different hashes:

<script>console.log('hello');</script>
<script>
console.log('hello');
</script>
<script>
  console.log('hello');
</script>

If your template engine reformats output, your hash may suddenly stop matching. I’ve seen this happen after harmless-looking prettifier changes.

My rule: if you use hashes, make the inline script a single exact string generated by the build, and test it in CI.

Hashes do not fix event handlers

This does not get covered by normal script hashes:

<button onclick="doSomething()">Click me</button>

Inline event handlers are a separate mess. Don’t try to preserve them. Move the code into a script file or a hashed/nonced script block:

<button id="run-action">Click me</button>
<script>
  document.getElementById('run-action').addEventListener('click', doSomething);
</script>

Then hash that script block if you need it inline.

Prefer external scripts when possible

If a script can live in its own .js file, do that instead.

<script src="/static/app.js"></script>

Then your CSP can stay simple:

Content-Security-Policy: default-src 'self'; script-src 'self'

That’s easier to maintain than a pile of hashes, and less fragile during refactors.

Use hashes when you genuinely need a tiny inline bootstrap script, not as an excuse to keep lots of inline JavaScript around.

Common failure cases

1. Templating injects dynamic data

Bad fit for hashes:

<script>
  window.userId = "{{ user.id }}";
</script>

If user.id changes per request, the hash changes per request too. Use a nonce, or move the data into the DOM:

<div id="boot" data-user-id="{{ user.id }}"></div>
<script src="/static/app.js"></script>

2. Build step rewrites the script

Minifiers, HTML compressors, SSR frameworks, and CDNs can all rewrite output. If the final HTML differs from what you hashed, the browser blocks it.

3. You accidentally allow unsafe inline anyway

This defeats the whole point:

Content-Security-Policy: script-src 'self' 'unsafe-inline' 'sha256-vIsp2avtxDy0157AryO+jEJVpLdmka7PI7o7C4q5ABE='

If you include 'unsafe-inline', you’ve basically thrown the door back open for inline script execution.

Debugging hash mismatches

When a hash doesn’t match, check:

  • Did you hash the exact script body?
  • Did whitespace or indentation change?
  • Did your template add a newline?
  • Did the production build minify or rewrite the HTML?
  • Are you hashing UTF-8 text consistently?
  • Are you accidentally hashing the <script> tags too?

Modern browsers usually log the expected hash in the console when they block an inline script. That’s incredibly useful.

You can also inspect your final response headers with a tool like headertest.com to confirm the CSP being served is actually the one you think you deployed.

A solid starting policy

If you need one small inline bootstrap script and everything else is local:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'sha256-vIsp2avtxDy0157AryO+jEJVpLdmka7PI7o7C4q5ABE=';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';

That’s a sane baseline for a lot of apps.

The least painful way to use hashes:

  1. Keep inline scripts tiny and static
  2. Generate hashes during the build
  3. Inject hashes into the CSP header automatically
  4. Fail CI if the HTML changed but hashes didn’t
  5. Move anything nontrivial into external JS files

If you’re hand-maintaining hashes in config files, you’ll eventually forget to update one and break production.

Quick reference

Allow one inline script by hash

Content-Security-Policy: script-src 'self' 'sha256-BASE64_HASH_HERE'

Generate hash with OpenSSL

echo -n "console.log('hello');" | openssl dgst -sha256 -binary | openssl base64

Safe pattern

<script>
  window.APP_ENV = 'production';
</script>
Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-...'

Unsafe pattern

Content-Security-Policy: script-src 'self' 'unsafe-inline'

Hashes are one of the few CSP features that are both strict and practical. Used carefully, they let you keep the tiny inline code you actually need without turning your CSP into theater.