C3.js is handy because it gets you from raw data to charts fast. It’s also one of those libraries that tends to expose every weak spot in a sloppy Content Security Policy.
I’ve seen teams lock down CSP, deploy C3.js, and then spend an afternoon wondering why the chart container stays blank while the console screams about blocked scripts or styles. Usually the breakage isn’t C3 itself. It’s the mix of D3, inline config, CDN habits, and CSS assumptions.
Here are the mistakes I see most often when people try to use CSP with C3.js, plus the fixes that actually hold up in production.
Mistake #1: Loading C3.js from a CDN without updating script-src
A lot of examples for C3.js still show script tags like this:
<script src="https://cdn.example.com/d3.min.js"></script>
<script src="https://cdn.example.com/c3.min.js"></script>
Then the site has a CSP like this:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';
That blocks both files immediately.
Fix
If you load C3.js and D3 from your own origin, keep script-src 'self'. That’s the cleanest option.
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';
frame-ancestors 'none';
If you really must use a CDN, explicitly allow it:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' https://cdn.example.com;
Still, I’d rather self-host C3.js unless there’s a strong reason not to. Fewer third-party hosts means fewer CSP exceptions and fewer surprises later.
For a good breakdown of directives, the official CSP docs are worth keeping open: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
Mistake #2: Forgetting that C3.js needs D3 too
This one sounds basic, but it shows up all the time in CSP debugging because the console output gets noisy. People allow c3.min.js, but they forget D3, or allow one host and not the other.
Example:
<script src="/js/c3.min.js"></script>
<script>
c3.generate({ bindto: '#chart', data: { columns: [['data1', 30, 200, 100]] } });
</script>
If D3 isn’t loaded first, you’ll get runtime failures that look like app bugs, not CSP bugs.
Fix
Load D3 first, then C3, then your app code.
<script src="/js/d3.min.js"></script>
<script src="/js/c3.min.js"></script>
<script src="/js/chart.js"></script>
And make sure your policy covers wherever those files come from.
Mistake #3: Using inline chart initialization with no nonce or hash
This is probably the most common CSP breakage.
People add C3.js correctly, then initialize the chart inline:
<div id="chart"></div>
<script>
c3.generate({
bindto: '#chart',
data: {
columns: [
['data1', 30, 200, 100, 400, 150, 250]
]
}
});
</script>
Under a strict CSP, that inline block is blocked unless you permit it with a nonce or hash. If your policy is just:
Content-Security-Policy: script-src 'self';
the chart won’t render.
Fix
Best fix: move chart initialization into an external file.
<div id="chart"></div>
<script src="/js/chart-init.js"></script>
// /js/chart-init.js
c3.generate({
bindto: '#chart',
data: {
columns: [
['data1', 30, 200, 100, 400, 150, 250]
]
}
});
If server-side templating makes inline code unavoidable, use a nonce:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m123';
style-src 'self';
<script nonce="rAnd0m123">
c3.generate({
bindto: '#chart',
data: { columns: [['data1', 30, 200, 100]] }
});
</script>
A real-world CSP from headertest.com uses that pattern for scripts:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-MzljY2JhMWQtMDUyNS00ODQ4LWE4OWEtYmE3NTg1MzQ2MDJm' '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 reasonable example of a nonce-based setup, though I’d avoid copying third-party allowances you don’t need.
For nonce and hash details, the official docs are here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
Mistake #4: Giving up and adding 'unsafe-inline' to script-src
This is the panic move. The chart doesn’t render, somebody pastes 'unsafe-inline' into script-src, and now the release goes green.
It also guts one of the main benefits of CSP.
Fix
Don’t use:
script-src 'self' 'unsafe-inline';
Use one of these instead:
- move code into external JS files
- use a nonce for server-generated inline blocks
- use a hash for truly static inline snippets
For C3.js config, external files are usually the least painful.
Mistake #5: Forgetting style-src because C3.js needs CSS too
C3.js isn’t just JavaScript. Without its stylesheet, the chart can render badly or look broken enough that people think CSP blocked the library.
Typical setup:
<link rel="stylesheet" href="/css/c3.min.css">
If your CSP doesn’t allow your stylesheet source, you’ll get ugly charts or invisible elements.
Fix
Allow the stylesheet source under style-src:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
If you’re pulling c3.min.css from a CDN, that host also needs to be whitelisted under style-src.
Be careful not to overcorrect by adding 'unsafe-inline' unless you actually need inline styles. The headertest.com policy includes:
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
That may be necessary for their stack, but for a C3.js integration on your own app, I’d try hard to avoid it.
If you need a refresher on style-src, this CSP reference is useful: https://csp-guide.com/style-src/
Mistake #6: Blocking data fetches used to populate charts
A static chart demo works fine under CSP. Then you swap in AJAX or fetch() to load real chart data, and it breaks because connect-src is too strict.
Example app code:
fetch('https://api.example.com/chart-data')
.then(r => r.json())
.then(data => {
c3.generate({
bindto: '#chart',
data: {
json: data
}
});
});
If your policy doesn’t allow https://api.example.com, the request is blocked even though C3.js itself is fine.
Fix
Add the API origin to connect-src:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
connect-src 'self' https://api.example.com;
If you use WebSockets for live chart updates, remember to allow wss: or the specific socket endpoint too.
The headertest.com example shows a pretty realistic connect-src with both HTTPS and WebSocket endpoints:
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;
That’s the kind of thing you need for dashboards with live updates.
Mistake #7: Ignoring images and embedded assets inside charts or tooltips
C3.js charts themselves are SVG-based, but teams often customize legends, tooltips, or surrounding UI with icons or images. Then CSP blocks those assets and the chart UI looks half-finished.
Fix
Set img-src intentionally. A decent baseline is:
img-src 'self' data:;
If your app uses remote image hosts, add them explicitly.
The headertest.com policy uses:
img-src 'self' data: https:;
That’s broad and convenient, but broader than I usually like. If you know your image origins, list them directly.
Mistake #8: Using default-src and assuming it covers everything cleanly
Yes, default-src is a fallback. No, it’s not enough for a real C3.js deployment.
People write this:
Content-Security-Policy: default-src 'self';
Then later they add API calls, a CDN stylesheet, maybe analytics, and now they’re debugging inherited defaults across half a dozen resource types.
Fix
Be explicit once your app does anything beyond the basics.
A practical CSP for a self-hosted C3.js setup might look like this:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';
If you need a server nonce for one inline bootstrapping block:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{ .CSPNonce }}';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';
That’s much easier to reason about than hoping default-src does the right thing everywhere.
Mistake #9: Debugging blindly instead of reading the exact CSP violation
CSP errors are usually very specific. The browser tells you which directive blocked which resource. But a lot of people jump straight into editing the policy without checking the console.
That leads to random changes like adding 'unsafe-inline', broad host wildcards, or even https: everywhere.
Fix
Read the violation and map it to the directive:
- blocked external JS file →
script-src - blocked inline init code →
script-srcnonce/hash issue - blocked stylesheet →
style-src - blocked API call →
connect-src - blocked image/icon →
img-src
If you want a directive-by-directive reference while debugging, this is solid: https://csp-guide.com
A sane baseline for C3.js
If I were starting fresh with C3.js today, I’d keep it boring:
- self-host D3, C3.js, and C3.css
- put chart config in external JS
- avoid inline scripts
- avoid inline styles
- whitelist only the API origins I actually use
Example:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';
And the page:
<link rel="stylesheet" href="/css/c3.min.css">
<div id="chart"></div>
<script src="/js/d3.min.js"></script>
<script src="/js/c3.min.js"></script>
<script src="/js/chart-init.js"></script>
That setup avoids most CSP pain before it starts. Which, honestly, is the whole game with CSP. The less clever your frontend loading story is, the easier it is to secure.