Highlight.js is usually easy to lock down with Content Security Policy. The core library does not need eval, it does not need inline event handlers, and it works fine under a strict policy if you load it like a normal script.

The place where people get sloppy is theming. They drop in inline <style> blocks, use broad CDN allowlists, or keep style-src 'unsafe-inline' around because syntax highlighting “needs it”. It doesn’t.

If you want the short version: serve the Highlight.js JavaScript and CSS from your own origin, initialize it from a nonce-based script or external JS file, and keep object-src 'none' plus a tight base-uri, form-action, and frame-ancestors.

What Highlight.js needs under CSP

Typical Highlight.js usage needs:

  • script-src for the library and your initialization code
  • style-src for the Highlight.js theme CSS
  • font-src only if your site theme loads fonts
  • img-src only if your page needs images, not for Highlight.js itself

Highlight.js does not normally require:

  • 'unsafe-eval'
  • 'unsafe-inline' for scripts
  • object-src anything other than 'none'

For most sites, this is enough:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

That works if you self-host:

  • highlight.min.js
  • a theme like github.min.css
  • your app JS that calls hljs.highlightAll()

Safe self-hosted setup

This is the setup I recommend most of the time because it avoids CSP drift.

HTML

<link rel="stylesheet" href="/assets/highlight/github.min.css">
<script defer src="/assets/highlight/highlight.min.js"></script>
<script defer src="/assets/js/docs.js"></script>

<pre><code class="language-js">
const answer = 42;
console.log(answer);
</code></pre>

docs.js

document.addEventListener('DOMContentLoaded', () => {
  hljs.highlightAll();
});

CSP header

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

That’s clean, boring, and secure. Boring is good here.

If you use an inline init script

Sometimes you just want one tiny inline script instead of a separate file. Fine. Use a nonce.

HTML with nonce

<link rel="stylesheet" href="/assets/highlight/github.min.css">
<script defer src="/assets/highlight/highlight.min.js"></script>
<script nonce="{{ .CSPNonce }}">
  document.addEventListener('DOMContentLoaded', () => {
    hljs.highlightAll();
  });
</script>

CSP header with nonce

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM_NONCE}}';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Replace {{RANDOM_NONCE}} with a fresh unpredictable value per response.

If you want background on how nonces work, the official CSP docs are the right place to start, and https://csp-guide.com has practical directive breakdowns.

CDN setup for Highlight.js

I still prefer self-hosting, but CDN is common on docs sites.

If you load both JS and CSS from a CDN, allow only that origin instead of throwing in a wildcard.

HTML

<link rel="stylesheet"
      href="https://cdn.example.com/highlight.js/styles/github.min.css">
<script defer
        src="https://cdn.example.com/highlight.js/highlight.min.js"></script>
<script defer src="/assets/js/docs.js"></script>

CSP header

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self' https://cdn.example.com;
  img-src 'self' data:;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

That’s all Highlight.js needs.

Strict nonce-based policy with strict-dynamic

If your site already uses a modern nonce policy, Highlight.js fits into it fine.

The real-world header from headertest.com looks like this:

content-security-policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-MzdlYzNkNzEtMTM0YS00MDQ5LTkwY2ItZWRmMWY4YTU1NTVh' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
  frame-src 'self' https://consentcdn.cookiebot.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none'

For Highlight.js, I would adapt that pattern like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Then load Highlight.js from a trusted nonce-bearing bootstrap script or directly from self.

Example

<link rel="stylesheet" href="/assets/highlight/github.min.css">

<script nonce="{{ .CSPNonce }}">
  const s = document.createElement('script');
  s.src = '/assets/highlight/highlight.min.js';
  s.onload = () => hljs.highlightAll();
  document.head.appendChild(s);
</script>

This works under strict-dynamic because the nonce-authorized script creates the child script.

Do you need style-src 'unsafe-inline'?

No, not for Highlight.js.

A lot of production headers still include it, like the headertest.com example above. Usually that is there because of legacy UI code, tag managers, consent tooling, or CSS-in-JS choices. Not because syntax highlighting needs inline styles.

Use this:

style-src 'self';

Or, if you must load the theme from a CDN:

style-src 'self' https://cdn.example.com;

Only use a nonce or hash for inline styles if you absolutely have to keep an inline <style> block.

Inline style block with nonce

<style nonce="{{ .CSPNonce }}">
  .docs pre code { border-radius: 6px; }
</style>
Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'nonce-{{RANDOM_NONCE}}';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Still, I’d move that CSS into a file.

Copy-paste policies

1. Minimal self-hosted Highlight.js CSP

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Use this when your page is simple and everything is local.

2. Self-hosted with nonce for inline init

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM_NONCE}}';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Best when you need a tiny inline bootstrap.

3. CDN-hosted Highlight.js

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self' https://cdn.example.com;
  img-src 'self' data:;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Swap cdn.example.com for your actual CDN origin.

4. Strict modern CSP

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Good fit for apps already using nonce-based script loading.

Common CSP errors with Highlight.js

“Refused to load the stylesheet…”

Your theme CSS origin is missing from style-src.

Fix:

style-src 'self' https://cdn.example.com;

Or self-host the CSS and keep style-src 'self'.

“Refused to load the script…”

Your Highlight.js script origin is missing from script-src, or your nonce does not match.

Fix one of these:

script-src 'self';

or

script-src 'self' https://cdn.example.com;

or

script-src 'self' 'nonce-{{RANDOM_NONCE}}';

“Refused to execute inline script…”

You wrote this:

<script>
  hljs.highlightAll();
</script>

But your policy does not allow inline scripts.

Fix it by moving the code into an external file, or use a nonce:

<script nonce="{{ .CSPNonce }}">
  hljs.highlightAll();
</script>

If I were shipping a docs site with Highlight.js today, this is what I’d start with:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

And if I needed inline bootstrapping:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM_NONCE}}';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

That keeps Highlight.js happy without punching giant holes in your policy. Which is the whole point of CSP.