MathJax is one of those libraries that looks harmless until you lock down your CSP and everything explodes.

You ship a clean policy, reload the page, and suddenly your equations stay as raw TeX, inline styles get blocked, fonts don’t load, and the console turns into a crime scene. I’ve seen teams blame MathJax, blame CSP, then quietly add 'unsafe-inline' everywhere just to make the pain stop.

That works, but it’s a bad trade.

Here are the most common CSP mistakes with MathJax, what causes them, and how I’d fix them without gutting the policy.

Mistake #1: Allowing the script but forgetting everything MathJax actually needs

A lot of developers start here:

Content-Security-Policy: default-src 'self'; script-src 'self' cdn.jsdelivr.net

Then they load MathJax from a CDN:

<script
  defer
  src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
</script>

Looks reasonable. Still breaks.

Why? Because MathJax usually needs more than just script-src. Depending on your output mode and configuration, it may inject styles, fetch fonts, and load additional components.

Typical missing directives:

  • style-src
  • font-src
  • sometimes connect-src
  • sometimes img-src if extensions or surrounding content depend on it

A safer starting point looks more like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.jsdelivr.net;
  style-src 'self' 'unsafe-inline';
  font-src 'self' https://cdn.jsdelivr.net;
  img-src 'self' data:;
  object-src 'none';
  base-uri 'self';

I don’t love 'unsafe-inline' in style-src, but with MathJax it’s a common compromise because it injects styles dynamically. If you want the directive details, csp-guide.com has solid breakdowns.

Mistake #2: Blocking inline styles and expecting MathJax to be fine

This is the big one.

MathJax v3 commonly inserts <style> blocks and inline styling as part of rendering. If your policy is strict like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.jsdelivr.net;
  style-src 'self';

you’ll usually get browser errors like:

Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'self'"

And then your math renders half-broken or not at all.

Fix option 1: Allow inline styles for MathJax pages

If your threat model allows it, this is the practical fix:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.jsdelivr.net;
  style-src 'self' 'unsafe-inline';
  font-src 'self' https://cdn.jsdelivr.net;

Not perfect. Very common.

Fix option 2: Self-host and reduce moving parts

If you self-host MathJax and keep the page simpler, you can at least avoid broad CDN allowances:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  font-src 'self';

This doesn’t remove the inline style issue, but it does shrink the trust boundary. I generally prefer self-hosting for security-sensitive apps anyway.

Mistake #3: Using a nonce for your app scripts but not understanding MathJax loading

Teams often move to nonce-based CSP and assume they’re done:

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

Then they add:

<script nonce="r4nd0m">
  window.MathJax = {
    tex: { inlineMath: [['$', '$'], ['\\(', '\\)']] }
  };
</script>

<script
  defer
  src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
</script>

The config script runs because it has the nonce. The external MathJax script is blocked because the source isn’t allowed.

You need both:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0m' https://cdn.jsdelivr.net;
  style-src 'self' 'unsafe-inline';
  font-src 'self' https://cdn.jsdelivr.net;

If you’re using strict-dynamic, be careful. It changes how source expressions are interpreted and can surprise people who copied a CSP from another project without understanding it.

For example, the real CSP on Headertest currently includes:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-ZTk0ZDIwN2ItODA0NC00ZjBhLWFiMWMtY2MzM2Y1OWQ1ZTI4' '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 a good reminder that real policies are rarely “just allow one script.” They evolve around actual dependencies. If you want to inspect your own headers and spot missing directives, Headertest is handy.

Mistake #4: Forgetting fonts when using CHTML output

MathJax commonly uses CommonHTML output, and that means font files matter.

A classic failure looks like this policy:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.jsdelivr.net;
  style-src 'self' 'unsafe-inline';
  font-src 'self';

If MathJax fonts are served from the CDN, the browser blocks them. The page may still render math, but spacing or glyphs look wrong.

Fix it by allowing the actual font origin:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.jsdelivr.net;
  style-src 'self' 'unsafe-inline';
  font-src 'self' https://cdn.jsdelivr.net;

Better fix: self-host the fonts too.

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  font-src 'self';

That’s cleaner, easier to reason about, and less likely to break when a CDN path changes.

Mistake #5: Setting default-src 'none' without explicitly rebuilding what MathJax needs

I like strict policies, so I also see this mistake a lot:

Content-Security-Policy: default-src 'none'; object-src 'none'; base-uri 'self';

Good instinct. Incomplete execution.

With default-src 'none', every resource type must be explicitly allowed. If you forget one directive, MathJax fails in weird ways.

A better strict policy for a self-hosted setup:

Content-Security-Policy:
  default-src 'none';
  script-src 'self' 'nonce-r4nd0m';
  style-src 'self' 'unsafe-inline';
  font-src 'self';
  img-src 'self' data:;
  connect-src 'self';
  base-uri 'self';
  object-src 'none';
  frame-ancestors 'none';

And for CDN-hosted MathJax:

Content-Security-Policy:
  default-src 'none';
  script-src 'self' 'nonce-r4nd0m' https://cdn.jsdelivr.net;
  style-src 'self' 'unsafe-inline';
  font-src 'self' https://cdn.jsdelivr.net;
  img-src 'self' data:;
  connect-src 'self';
  base-uri 'self';
  object-src 'none';
  frame-ancestors 'none';

The point is simple: strict CSP is great, but only if you enumerate real dependencies.

Mistake #6: Ignoring report-only and debugging blind

I still see people tweak CSP directly in production, reload, and guess.

Use Report-Only first:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' https://cdn.jsdelivr.net;
  style-src 'self';
  report-to csp-endpoint;

Or the older form:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' https://cdn.jsdelivr.net;
  style-src 'self';
  report-uri /csp-report

Then load pages with real math content and watch what actually gets blocked.

MathJax failures are often multi-step:

  1. config script blocked
  2. main loader blocked
  3. inline style blocked
  4. font blocked

If you only fix the first console error, you’ll think you solved it when you haven’t.

Mistake #7: Copy-pasting a generic CSP that was never tested with MathJax

This happens constantly in docs sites, LMS platforms, and markdown renderers.

Someone copies a “secure CSP” from a blog post:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  object-src 'none';

Then they add MathJax and act surprised when equations don’t render.

MathJax is not a plain static library. It changes the DOM, injects styles, and may load fonts or components. Your CSP has to reflect reality, not ideology.

Here’s the baseline I’d start with for most teams using MathJax v3 from jsDelivr:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0m' https://cdn.jsdelivr.net;
  style-src 'self' 'unsafe-inline';
  font-src 'self' https://cdn.jsdelivr.net;
  img-src 'self' data:;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';

And if I control deployment, I’d rather self-host and trim it to this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0m';
  style-src 'self' 'unsafe-inline';
  font-src 'self';
  img-src 'self' data:;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';

What I’d actually do

My practical advice:

  • self-host MathJax if you can
  • use nonces for your own inline config script
  • expect to allow 'unsafe-inline' in style-src unless you’ve verified a more restrictive setup works
  • explicitly allow fonts
  • test with real equations, not a blank page
  • deploy in Report-Only before enforcing

If your CSP goal is “maximum purity,” MathJax will annoy you. If your goal is “strong policy that still ships,” the answer is usually controlled allowances, fewer external origins, and testing the exact rendering path your users hit.

That’s the version that survives contact with production.