I’ve had to fix this exact problem more than once: a site adds KaTeX for math rendering, everything works locally, then production CSP turns it into a pile of console errors.
KaTeX itself is usually not the hard part. The hard part is fitting it into a CSP that already has analytics, consent tooling, nonces, and a bunch of inherited decisions nobody wants to touch.
Here’s a real-world style case study based on a production-shaped policy, using the kind of CSP you actually see on a live site.
The starting point
A real CSP 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-MjIyM2U0MzQtMjVlMi00MjE0LWJkMTYtNzkwYjY0ZDQ0NzYx' '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'
That’s not a bad policy. It has some strong choices:
object-src 'none'frame-ancestors 'none'base-uri 'self'- nonce-based
script-src strict-dynamic
But it also has some common baggage:
style-src 'unsafe-inline'- a bunch of third-party allowances mixed into
default-src font-src 'self', which is often too strict for frontend libraries unless you self-host everything
Now drop KaTeX into this setup and see what breaks.
The feature request
The dev team wanted server-rendered docs pages with inline math and display equations:
<p>Einstein wrote \(E = mc^2\)</p>
<div>
\[
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
\]
</div>
They added KaTeX the usual way:
<link rel="stylesheet" href="/assets/katex/katex.min.css">
<script defer src="/assets/katex/katex.min.js"></script>
<script defer src="/assets/katex/contrib/auto-render.min.js"></script>
<script nonce="{{ .CSPNonce }}">
document.addEventListener("DOMContentLoaded", function () {
renderMathInElement(document.body, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "\\[", right: "\\]", display: true },
{ left: "\\(", right: "\\)", display: false }
]
});
});
</script>
This is already the right instinct: self-host KaTeX, use a nonce for the init script, avoid random CDN exceptions.
But production still broke.
What actually failed
There were two issues.
1. Fonts were blocked
KaTeX uses web fonts. If the CSS is loaded from /assets/katex/katex.min.css, the browser will fetch fonts referenced by that stylesheet, usually under something like:
/assets/katex/fonts/KaTeX_Main-Regular.woff2
/assets/katex/fonts/KaTeX_Math-Italic.woff2
If you self-host those fonts under the same origin, font-src 'self' is fine.
But I’ve seen teams copy only the CSS and JS files, forget the fonts, and then silently fall back to broken rendering. The page “works,” but spacing and symbols are off. That’s worse than a hard failure because people miss it in review.
Typical console error when fonts are missing or blocked:
Refused to load the font 'https://cdn.example.com/katex/fonts/KaTeX_Main-Regular.woff2'
because it violates the following Content Security Policy directive:
"font-src 'self'".
2. Inline styles tempted the team into bad CSP changes
KaTeX itself does not require inline script execution in the browser if you use it sanely. But teams often react to rendering glitches by loosening CSP too much:
- adding
'unsafe-inline'toscript-src - allowing random font CDNs
- broadening
style-srcbecause “math is broken”
That’s the wrong fix.
The existing policy already had 'unsafe-inline' in style-src, which is common but not great. KaTeX did not need that. It just happened to already be there.
The bad “before” fix
I’ve seen this kind of policy change get proposed during rollout:
Content-Security-Policy:
default-src 'self' https:;
script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
style-src 'self' 'unsafe-inline' https:;
img-src 'self' data: https:;
font-src 'self' https: data:;
connect-src 'self' https: wss:;
frame-src 'self' https:;
object-src 'none';
This is how security gets chipped away one support ticket at a time.
Problems with this version:
'unsafe-inline'inscript-srcdestroys a major CSP protection'unsafe-eval'is almost never justified for KaTeXhttps:in multiple directives is way too broadfont-src 'self' https: data:allows a lot more than needed
This kind of policy “fixes” KaTeX by making CSP less meaningful.
The actual fix
We kept the existing strong parts and changed only what KaTeX truly needed.
Safer production setup
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' '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';
The key change was not adding permissions. It was making sure KaTeX assets were properly self-hosted:
/assets/katex/katex.min.css/assets/katex/katex.min.js/assets/katex/contrib/auto-render.min.js/assets/katex/fonts/*.woff2
That let us keep:
font-src 'self';
No font CDN. No blanket https:. No data: for fonts.
Before and after in the template
Before: brittle and CSP-hostile
<link rel="stylesheet" href="https://cdn.example.com/katex.min.css">
<script src="https://cdn.example.com/katex.min.js"></script>
<script src="https://cdn.example.com/auto-render.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
renderMathInElement(document.body);
});
</script>
Why this is bad:
- external CDN means more CSP exceptions
- inline script needs
'unsafe-inline'or a nonce/hash - if the CDN CSS points to CDN fonts,
font-src 'self'breaks
After: self-hosted and nonce-friendly
<link rel="stylesheet" href="/assets/katex/katex.min.css">
<script defer src="/assets/katex/katex.min.js"></script>
<script defer src="/assets/katex/contrib/auto-render.min.js"></script>
<script nonce="{{ .CSPNonce }}">
document.addEventListener("DOMContentLoaded", function () {
renderMathInElement(document.body, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "\\[", right: "\\]", display: true },
{ left: "\\(", right: "\\)", display: false }
],
throwOnError: false
});
});
</script>
This version works with a modern nonce-based script-src and doesn’t force you to weaken font-src.
A better cleanup pass
If I were cleaning this up beyond “make KaTeX work,” I’d also tighten the original policy structure.
The source policy puts third-party hosts into default-src. I don’t love that. I’d rather make default-src 'self' and explicitly list third parties only where needed.
Something like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' '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';
That makes the policy easier to reason about. default-src should be boring.
If you want a good directive-by-directive reference, https://csp-guide.com is useful, and the official CSP behavior is documented in the MDN Content Security Policy docs.
Practical rules for KaTeX under CSP
My opinionated checklist:
-
Self-host KaTeX JS, CSS, and fonts
- Don’t half-copy the package.
- Fonts matter.
-
Use a nonce for the initialization script
- If you already have nonce-based CSP, this is easy.
- Don’t backslide into
'unsafe-inline'for scripts.
-
Do not add
unsafe-eval- KaTeX doesn’t need it in a normal setup.
-
Keep
font-srcnarrowfont-src 'self'is ideal if assets are local.- Avoid
https:unless you truly need it.
-
Don’t blame
style-srcfor every rendering issue- A lot of “KaTeX CSP problems” are actually missing fonts.
-
Test with the network tab open
- You’ll catch blocked
.woff2files immediately.
- You’ll catch blocked
The result
After the fix:
- math rendered correctly
- no new third-party origins were added
script-srcstayed nonce-basedfont-src 'self'remained intact- no need for
'unsafe-eval' - no need to weaken CSP just to support math
That’s the real lesson here. KaTeX is CSP-friendly if you deploy it like you mean it. Most breakage comes from sloppy asset hosting, not from the library itself.
If a team tells me “we had to loosen CSP for KaTeX,” I usually read that as: “we took the quick path and let security pay for it.”