Highcharts is one of those libraries that usually works fine until you turn on a real Content Security Policy. Then the fun starts: tooltips stop rendering, exports break, inline styles get blocked, and you end up staring at DevTools wondering which directive is yelling at you this time.

I’ve had better results treating Highcharts like any other third-party JavaScript dependency: start with a strict CSP, load only what you need, and loosen the policy only when you can prove why.

If you want background on specific directives, the official CSP spec docs and https://csp-guide.com are useful. For Highcharts-specific behavior, the official docs are your source of truth.

The basic problem

Highcharts itself is just JavaScript, but charts often involve:

  • external script loading
  • inline styles or dynamically applied styles
  • image assets
  • export modules
  • data fetching from APIs
  • accessibility helpers
  • optional plugins

CSP tends to break Highcharts in four common areas:

  1. script-src blocks the library or modules
  2. style-src blocks inline style behavior
  3. img-src blocks SVG data URIs or exported chart images
  4. connect-src blocks API calls for chart data

The right CSP depends on how you load Highcharts.

A realistic starting CSP

Here’s a real-world CSP header from headertest.com:

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-Nzc4OGM0ZGItZDYwZC00YTUzLWI3NDYtNzA0OTk5YjNlYjIz' '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 Highcharts policy, but it’s a good example of how CSP looks in a real app: analytics, consent tooling, APIs, WebSockets, and a nonce-based script policy.

Now let’s adapt that style to Highcharts.


Option 1: Self-host Highcharts with a strict CSP

This is my preferred setup. Self-host the library, avoid random CDN allowances, and keep script-src tight.

HTML

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Highcharts CSP Demo</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <link rel="stylesheet" href="/assets/app.css">
</head>
<body>
  <div id="sales-chart"></div>

  <script src="/assets/js/highcharts.js" defer></script>
  <script src="/assets/js/modules/exporting.js" defer></script>
  <script src="/assets/js/chart-init.js" defer></script>
</body>
</html>

Chart initialization

document.addEventListener('DOMContentLoaded', () => {
  Highcharts.chart('sales-chart', {
    title: {
      text: 'Monthly revenue'
    },
    series: [{
      type: 'line',
      name: 'Revenue',
      data: [12, 18, 15, 24, 31, 28]
    }],
    xAxis: {
      categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
    }
  });
});

CSP header

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

This works for a lot of basic Highcharts installs.

Why style-src 'unsafe-inline' shows up so often

This is the part people hate, and I get it. You want a strict CSP, then a charting library casually needs inline styling behavior. Depending on your Highcharts configuration and modules, removing 'unsafe-inline' from style-src may break rendering details.

If you can make your charts work without it, great. Test thoroughly. But in practice, many teams keep:

style-src 'self' 'unsafe-inline';

That’s not ideal, but it’s common.

If you need a deeper explanation of why style directives get messy, https://csp-guide.com/style-src/ is a decent reference.


Option 2: Use a nonce-based CSP for your own inline bootstrap code

A lot of apps render chart config from the server directly into the page. If you do that with inline scripts, use a nonce. Don’t fall back to 'unsafe-inline' for scripts unless you enjoy regret.

Server-generated HTML

<div id="traffic-chart"></div>

<script src="/assets/js/highcharts.js" defer></script>

<script nonce="{{ .CSPNonce }}">
  window.chartConfig = {
    title: { text: 'Traffic' },
    series: [{
      type: 'column',
      name: 'Visits',
      data: [120, 150, 170, 140]
    }],
    xAxis: {
      categories: ['Week 1', 'Week 2', 'Week 3', 'Week 4']
    }
  };
</script>

<script src="/assets/js/chart-bootstrap.js" defer></script>

Bootstrap file

document.addEventListener('DOMContentLoaded', () => {
  Highcharts.chart('traffic-chart', window.chartConfig);
});

CSP header

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM_NONCE}}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data:;
  connect-src 'self';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  object-src 'none';

This is much better than allowing all inline scripts.

The headertest.com example uses the same basic pattern for scripts:

script-src 'self' 'nonce-Nzc4OGM0ZGItZDYwZC00YTUzLWI3NDYtNzA0OTk5YjNlYjIz' 'strict-dynamic' ...

That’s a solid modern approach if your app already uses nonces.


Option 3: Loading Highcharts from a CDN

I usually recommend self-hosting, but sometimes teams want the official distribution from a CDN.

Then your policy needs to explicitly trust that source.

HTML

<div id="users-chart"></div>

<script src="https://code.highcharts.com/highcharts.js" defer></script>
<script src="https://code.highcharts.com/modules/accessibility.js" defer></script>
<script src="/assets/js/users-chart.js" defer></script>

CSP header

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://code.highcharts.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  object-src 'none';

If you load modules from the same CDN, that single script source is usually enough.

If your chart pulls images, patterns, or external icons over HTTPS, img-src https: may be necessary. Don’t add it unless you actually need it.


Data-driven charts need connect-src

A chart with hardcoded values is easy. A chart that fetches JSON from your API needs the right connect-src.

JavaScript

async function renderChart() {
  const response = await fetch('https://api.example.com/stats/revenue');
  const payload = await response.json();

  Highcharts.chart('sales-chart', {
    title: { text: 'Revenue API data' },
    series: [{
      type: 'line',
      name: 'Revenue',
      data: payload.values
    }],
    xAxis: {
      categories: payload.labels
    }
  });
}

renderChart().catch(console.error);

CSP header

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data:;
  connect-src 'self' https://api.example.com;
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  object-src 'none';

This mirrors the real-world pattern from headertest.com, where connect-src includes APIs, telemetry, and even WebSocket endpoints.

If your chart updates live over WebSockets, add the wss: endpoint explicitly:

connect-src 'self' https://api.example.com wss://stream.example.com;

Exporting module gotchas

Highcharts exporting is where CSP often gets weird.

Depending on your configuration, exporting may involve:

  • generated SVG
  • data URIs
  • client-side image generation
  • remote export services if you use them

At minimum, I’ve found this commonly needed:

img-src 'self' data:;

Without data:, chart exports or previews can fail because generated image content gets blocked.

If you use any remote export endpoint, you’ll also need to allow it under connect-src. If it opens in a frame or popup flow, other directives may come into play too.

My rule: enable exporting only after the base chart works under CSP, then test download PNG, download SVG, print, and any custom export buttons.


A practical production policy

Here’s a realistic policy for a self-hosted Highcharts app that fetches API data and uses nonce-based inline config:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM_NONCE}}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';
  report-to default-endpoint;
  report-uri /csp-report;

I like this because it’s honest. It doesn’t pretend everything can be locked down perfectly while still using a charting library with dynamic styling behavior.


Debugging CSP breakage with Highcharts

When a chart doesn’t render, check the browser console first. CSP errors are usually explicit.

Typical messages look like:

  • refused to load script
  • refused to apply inline style
  • refused to connect to API endpoint
  • refused to load image from data:

Work through them one directive at a time.

My usual order is:

  1. get scripts loading
  2. get chart rendering
  3. fix styles
  4. fix API calls
  5. test exports
  6. test accessibility and optional modules

Don’t dump https: into every directive just to make the warning disappear. That’s how CSP turns into decorative security.


What I’d ship

If I were setting up Highcharts for a production app today, I’d do this:

  • self-host Highcharts
  • avoid inline scripts unless they use nonces
  • allow style-src 'unsafe-inline' if testing proves it’s required
  • allow img-src data: for export-related behavior
  • keep connect-src limited to the actual API origins
  • set object-src 'none', base-uri 'self', and frame-ancestors 'none'

That gets you a CSP that is actually enforceable, not just technically present.

If you want a stricter policy later, great. Start from a working baseline first. With Highcharts, that saves a lot of time.