I’ve seen this exact problem a bunch of times: a team has a pretty decent Content Security Policy, marketing wants Intercom, someone pastes the vendor snippet into the site, and suddenly the browser console looks like a crime scene.

The hard part isn’t getting Intercom to load. The hard part is getting it to load without wrecking a policy that was actually doing useful work.

Here’s a realistic case study using a real baseline CSP from headertest.com, then tightening it up for Intercom live chat with minimal blast radius.

The starting point

This is the real CSP header you gave me, slightly reformatted for sanity:

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-YTgzMWE0YWEtMDg1NC00YjIyLTg3ZDEtZTgwYWRmOGEzM2Y4' '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'

Honestly, this is not bad. A few things stand out:

  • script-src uses a nonce and 'strict-dynamic', which is good.
  • object-src 'none', base-uri 'self', frame-ancestors 'none' are exactly what I like to see.
  • img-src 'self' data: https: is broad, but common.
  • style-src 'unsafe-inline' is weaker than I’d want, but lots of third-party widgets force that tradeoff.
  • font-src 'self' is going to be a problem the second a third-party widget wants hosted fonts.
  • frame-src and connect-src are restrictive enough that Intercom will definitely hit them.

The “before” situation

The team drops in the standard Intercom boot snippet. Usually it looks roughly like this:

<script nonce="{{ .CSPNonce }}">
  window.intercomSettings = {
    app_id: "abc123xy"
  };
</script>

<script nonce="{{ .CSPNonce }}">
  (function(){
    var w=window;var ic=w.Intercom;
    if(typeof ic==="function"){
      ic('reattach_activator');ic('update',w.intercomSettings);
    } else {
      var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};
      w.Intercom=i;
      var l=function(){
        var s=d.createElement('script');
        s.type='text/javascript';s.async=true;
        s.src='https://widget.intercom.io/widget/abc123xy';
        var x=d.getElementsByTagName('script')[0];
        x.parentNode.insertBefore(s,x);
      };
      if(document.readyState==='complete'){l();}
      else if(w.attachEvent){w.attachEvent('onload',l);}
      else{w.addEventListener('load',l,false);}
    }
  })();
</script>

Because the inline scripts are nonce’d, the bootstrap code itself is usually fine.

Then the browser starts complaining:

Refused to connect to 'https://api-iam.intercom.io/...'
because it violates the following Content Security Policy directive:
"connect-src 'self' https://api.headertest.com ..."

Refused to frame 'https://js.intercomcdn.com/...'
because it violates the following Content Security Policy directive:
"frame-src 'self' https://consentcdn.cookiebot.com"

Refused to load the script 'https://widget.intercom.io/widget/abc123xy'
because it violates the following Content Security Policy directive:
"script-src 'self' 'nonce-...' 'strict-dynamic' ..."

That last error deserves a quick note.

With 'strict-dynamic', host allowlists in script-src are ignored by supporting browsers once a nonce or hash is present. That’s not a bug; that’s the feature. If your nonce’d bootstrap script dynamically injects the Intercom script, modern browsers should allow it because trust flows from the nonce’d script.

But older browsers don’t fully behave that way, and mixed browser support is where people get surprised. If you care about graceful fallback, you may still choose to list Intercom script origins explicitly.

What Intercom usually needs

For the Messenger widget, you should expect to allow some mix of these origin families:

  • https://widget.intercom.io for the loader script
  • https://js.intercomcdn.com for widget assets
  • https://*.intercom.io for API and messaging endpoints
  • https://*.intercomcdn.com for static assets
  • wss://*.intercom.io for websocket connections
  • https://intercom-sheets.com in some setups
  • https://uploads.intercomcdn.com or similar for attachments/images

Check the current official docs before locking anything in: https://www.intercom.com/help/en/articles/3894-install-intercom-on-your-web-product-or-site and your CSP behavior against the CSP spec/docs: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP

Vendor infrastructure changes. That’s reality. I don’t love broad wildcards, but for chat widgets, trying to pin every single subdomain can become a maintenance tax.

The naive fix

The first fix people ship is usually this kind of mess:

Content-Security-Policy:
  default-src * data: blob: 'unsafe-inline' 'unsafe-eval';
  script-src * 'unsafe-inline' 'unsafe-eval';
  style-src * 'unsafe-inline';
  img-src * data: blob:;
  connect-src * wss:;
  frame-src *;

Yes, Intercom works. So would half the malware on the internet.

This is the classic third-party-widget panic response: “CSP is blocking the business, just make it permissive.” I’ve had to unwind these after audits, and it’s never fun.

The better fix

Start from the existing header and add only what Intercom actually needs.

Here’s a practical “after” policy based on your real baseline:

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://widget.intercom.io https://js.intercomcdn.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' https://js.intercomcdn.com;
  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 https://*.intercom.io wss://*.intercom.io https://*.intercomcdn.com;
  frame-src 'self' https://consentcdn.cookiebot.com https://js.intercomcdn.com https://widget.intercom.io https://intercom-sheets.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';
  report-to default-endpoint;
  report-uri /csp-report

That’s a reasonable production starting point.

Why each change exists

script-src

https://widget.intercom.io https://js.intercomcdn.com

If you’re already using a nonce plus 'strict-dynamic', modern browsers may not need these hosts for the injected loader path. I still include them when rolling out vendor widgets because:

  • they help with compatibility in older browsers
  • they make intent obvious during reviews
  • they reduce “why is this blocked in browser X?” debugging

connect-src

https://*.intercom.io wss://*.intercom.io https://*.intercomcdn.com

This is the big one. Live chat widgets talk constantly:

  • boot/auth calls
  • message sync
  • presence/state polling or sockets
  • attachment/image fetches

If you forget wss://, the launcher might render but conversation updates will break in weird ways.

frame-src

https://js.intercomcdn.com https://widget.intercom.io https://intercom-sheets.com

Intercom uses iframes for parts of the Messenger UI. If your policy only allows Cookiebot frames, Intercom will fail even though the bootstrap script loaded fine.

font-src

https://js.intercomcdn.com

This one gets missed a lot. The widget can render, but typography/icons break. Then someone files a “UI looks weird in production” ticket and nobody suspects CSP for two days.

Before and after: the practical diff

Here’s the shortest useful diff from your original policy:

 script-src:
+  https://widget.intercom.io
+  https://js.intercomcdn.com

 font-src:
+  https://js.intercomcdn.com

 connect-src:
+  https://*.intercom.io
+  wss://*.intercom.io
+  https://*.intercomcdn.com

 frame-src:
+  https://js.intercomcdn.com
+  https://widget.intercom.io
+  https://intercom-sheets.com

That’s the shape I’d start with.

Safer rollout with Report-Only

Don’t push this straight to enforcement if you can avoid it. Ship a Content-Security-Policy-Report-Only header first and watch what the widget actually tries to do in your environment.

Example:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://widget.intercom.io https://js.intercomcdn.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' https://js.intercomcdn.com;
  connect-src 'self' https://*.intercom.io wss://*.intercom.io https://*.intercomcdn.com;
  frame-src 'self' https://js.intercomcdn.com https://widget.intercom.io https://intercom-sheets.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  report-uri /csp-report

If you want ready-made policy patterns, https://csp-examples.com is handy for comparing structures quickly.

A couple of hard-earned lessons

1. Don’t dump Intercom domains into default-src

That’s lazy policy design. Keep permissions in the directives where they belong. If Intercom needs connect-src, say that. Don’t quietly widen everything.

2. Keep the nonce-based bootstrap

Your current policy already uses a nonce and 'strict-dynamic'. That’s better than switching back to 'unsafe-inline' because a vendor gave you a copy-paste snippet.

3. Expect asset drift

Third-party chat vendors change infrastructure. Add reporting, review violations, and treat CSP maintenance as part of owning the integration.

4. Test the full chat flow

Don’t stop at “launcher bubble appears.” Test:

  • opening the Messenger
  • loading past messages
  • sending a message
  • uploading an attachment
  • mobile viewport behavior
  • logged-in vs logged-out states

CSP failures often hide in the second or third interaction.

Final production example

If I were shipping this on a site with your baseline, I’d start here:

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://widget.intercom.io https://js.intercomcdn.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' https://js.intercomcdn.com;
  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 https://*.intercom.io wss://*.intercom.io https://*.intercomcdn.com;
  frame-src 'self' https://consentcdn.cookiebot.com https://js.intercomcdn.com https://widget.intercom.io https://intercom-sheets.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';
  report-uri /csp-report

Not perfect. No real production CSP ever is. But it keeps the original structure intact, preserves the strong parts of the policy, and makes Intercom work without turning CSP into decorative security.