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-srcfor the library and your initialization codestyle-srcfor the Highlight.js theme CSSfont-srconly if your site theme loads fontsimg-srconly if your page needs images, not for Highlight.js itself
Highlight.js does not normally require:
'unsafe-eval''unsafe-inline'for scriptsobject-srcanything 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>
Recommended production baseline
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.