I’ve seen this exact failure pattern a few times: a team adds Mermaid to docs or an internal dashboard, the diagrams look great locally, then production CSP quietly wrecks everything.
The symptoms are boring but expensive:
- diagrams don’t render
- initialization throws console errors
- someone “fixes” it by adding
'unsafe-inline'or weakeningscript-src - now the page works, but the policy got worse
Mermaid is a perfect example of where frontend convenience collides with a real CSP. If you run a site with a strict policy, you can’t just paste the snippet from a README and hope for the best.
Here’s a real-world case study using a production-style CSP, based on the header returned by headertest.com:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-YjM4MmZmYzMtZjc5MC00NDkwLWIwODAtZTk1NmI2MjVlYjdl' '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 toy policy. It already uses a nonce and 'strict-dynamic', blocks plugins with object-src 'none', and locks down embedding with frame-ancestors 'none'. It also carries one compromise a lot of production apps still have: style-src 'unsafe-inline'.
The setup
The developer wanted Mermaid diagrams inside documentation pages.
Their first pass looked like this:
<div class="mermaid">
flowchart TD
A[User Request] --> B[Render Page]
B --> C[Initialize Mermaid]
C --> D[Draw SVG]
</div>
<script type="module">
import mermaid from '/js/mermaid.esm.min.mjs';
mermaid.initialize({
startOnLoad: true
});
</script>
This worked in local development. It failed in production.
Before: what broke
The production page had a nonce-based CSP. The inline module script above did not include the nonce, so the browser blocked it.
Typical console error:
Refused to execute inline script because it violates the following Content Security Policy directive:
"script-src 'self' 'nonce-YjM4MmZmYzMtZjc5MC00NDkwLWIwODAtZTk1NmI2MjVlYjdl' 'strict-dynamic' ...".
Either the 'unsafe-inline' keyword, a hash, or a nonce is required to enable inline execution.
That was the first break.
Then someone moved the code into an external file:
<script type="module" src="/js/diagram-init.js"></script>
// /js/diagram-init.js
import mermaid from '/js/mermaid.esm.min.mjs';
mermaid.initialize({
startOnLoad: true
});
That solved the inline-script problem, but there was a second issue: Mermaid often injects styles or relies on styling behavior that collides with strict style-src setups, depending on version and rendering mode. In this particular environment, the site already had style-src 'unsafe-inline', so diagrams rendered. But that was luck, not design.
If your goal is a tighter CSP, Mermaid can be the reason you keep unsafe-inline around longer than you want.
The bad fix
I’ve seen developers respond by punching a bigger hole in the policy:
Content-Security-Policy:
script-src 'self' 'unsafe-inline' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
That’s the easy path and the wrong one.
Adding 'unsafe-inline' to script-src throws away one of the biggest CSP wins you had. If you already use nonces and 'strict-dynamic', don’t backslide just because a diagram library needs a little care.
If you need a refresher on how these directives behave, the CSP directive references at https://csp-guide.com are actually useful and not full of fluff.
After: the production-safe version
The fix was straightforward:
- stop using inline script for Mermaid bootstrapping
- self-host Mermaid
- initialize it from a nonce-approved or external same-origin script
- decide deliberately how to handle Mermaid’s styling behavior
HTML
<div class="mermaid">
flowchart TD
A[User Request] --> B[Render Page]
B --> C[Initialize Mermaid]
C --> D[Draw SVG]
</div>
<script nonce="{{ .CSPNonce }}" type="module" src="/js/diagram-init.js"></script>
If your templating system can inject a nonce, great. If not, a same-origin external script is still fine with script-src 'self', though in nonce-first deployments I prefer keeping the nonce on script tags consistently.
JavaScript
// /js/diagram-init.js
import mermaid from '/js/vendor/mermaid.esm.min.mjs';
mermaid.initialize({
startOnLoad: true,
securityLevel: 'strict',
theme: 'default'
});
Two notes here:
securityLevel: 'strict'matters. Don’t relax Mermaid’s own security model unless you have a very specific reason.- Self-hosting avoids widening
script-srcto random CDNs.
CSP
The policy did not need major expansion for scripts. That was the nice part.
A workable version stayed close to the original:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-YjM4MmZmYzMtZjc5MC00NDkwLWIwODAtZTk1NmI2MjVlYjdl' '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 the practical “after” state: Mermaid works without weakening script-src.
The style problem nobody likes talking about
The harder question is style-src.
In the real header above, style-src already includes 'unsafe-inline'. So Mermaid works more easily, but the site is still carrying a CSP compromise.
If you want to tighten that later, Mermaid may force you to test carefully. Depending on your Mermaid version and rendering path, removing 'unsafe-inline' from style-src can break diagram rendering or appearance.
A stricter target policy might look like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{NONCE}}' 'strict-dynamic';
style-src 'self' 'nonce-{{NONCE}}';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
That’s cleaner, but don’t assume Mermaid will just cooperate. Test actual diagrams, not just whether the page loads.
My rule is simple: if a library pushes you toward weaker style-src, document that tradeoff explicitly. Don’t let it become accidental security debt.
Before and after, side by side
Before: blocked inline Mermaid init
<div class="mermaid">
graph LR
A --> B
</div>
<script type="module">
import mermaid from '/js/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true });
</script>
Why it fails:
- inline script has no nonce
- nonce-based CSP blocks it
After: nonce-approved external init
<div class="mermaid">
graph LR
A --> B
</div>
<script nonce="{{ .CSPNonce }}" type="module" src="/js/diagram-init.js"></script>
import mermaid from '/js/vendor/mermaid.esm.min.mjs';
mermaid.initialize({
startOnLoad: true,
securityLevel: 'strict'
});
Why it works:
- no unsafe inline JavaScript
- Mermaid is self-hosted
- existing
script-srcremains strong
What I’d ship on a real site
If I were adding Mermaid to a docs site today, I’d do this:
- self-host Mermaid
- initialize from a nonced external module
- keep
securityLevel: 'strict' - avoid adding new third-party script origins
- test whether Mermaid forces inline styles in your exact version
- if
style-src 'unsafe-inline'is still present, treat that as a separate cleanup task, not a reason to weakenscript-src
That last point matters. Teams often mix these together and end up making the wrong concession. Mermaid should not be the excuse for turning a nonce-based script policy into a permissive one.
A practical debugging checklist
When Mermaid doesn’t render under CSP, I check these in order:
-
Was the init script blocked?
Look for nonce/hash violations in DevTools. -
Is Mermaid loaded from an allowed origin?
With the policy above, same-origin is fine. Random CDN imports are not. -
Is Mermaid trying to inject styles that your
style-srcblocks?
This is where stricter deployments usually hit friction. -
Did someone relax Mermaid’s own security settings?
Don’t casually switch away fromsecurityLevel: 'strict'. -
Are there hidden fetches or workers?
Less common, but worth checking in more complex integrations.
The takeaway
The good news: adding Mermaid does not require blowing up a strong script-src. A nonce-based policy like the real headertest.com header can support Mermaid just fine if you load it properly.
The annoying part is styles. That’s where the real negotiation usually happens.
If you’re maintaining csp-examples, this is the lesson worth showing developers: the secure fix is usually small, and the insecure fix is usually lazy. Mermaid is just one more case where a little discipline beats adding 'unsafe-inline' and calling it done.