HubSpot chat is one of those integrations that looks trivial right up until CSP starts blocking half of it.

I’ve seen this pattern a lot: the base site already has a decent policy, someone drops in the HubSpot tracking/chat script, the widget silently fails, and the first reaction is to throw 'unsafe-inline' or https: into the policy until the errors go away. That works, but it also wrecks the whole point of having CSP.

If you’re adding HubSpot chat to a site with a real CSP, there are a few mistakes that keep showing up. Most of them come from misunderstanding how third-party widgets actually load: initial script, follow-up network calls, embedded frames, sometimes websockets, and occasionally inline styles or dynamically injected scripts.

Here’s how I’d approach the common failures and how to fix them without turning your policy into mush.

Mistake #1: Only allowing the HubSpot script origin

A lot of developers start with script-src because that’s where the install snippet lives. Fair enough. But the widget usually needs more than one directive to work.

A common “almost there” policy looks like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://js.hs-scripts.com;

That may allow the bootstrap script to load, but chat still won’t render because HubSpot also needs network access and often embeds frames.

The fix is to think in terms of the full widget lifecycle:

  • script-src for the loader
  • connect-src for API/XHR/fetch/websocket calls
  • frame-src for embedded chat UI or supporting frames
  • img-src for avatars, beacons, and tracking pixels
  • sometimes style-src if the widget injects styles in a way your policy blocks

A more realistic starting point looks like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://js.hs-scripts.com https://js.usemessages.com;
  connect-src 'self' https://api.hubapi.com https://api.hsforms.com https://*.hubspot.com https://*.hubapi.com https://*.usemessages.com wss://*.hubspot.com wss://*.usemessages.com;
  frame-src 'self' https://*.hubspot.com https://*.usemessages.com;
  img-src 'self' data: https:;
  style-src 'self' 'unsafe-inline';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';

You’ll probably need to tune those hosts for your exact deployment, but this is the right shape.

Mistake #2: Relying on default-src to cover everything

This one bites teams with already-established policies.

Here’s a real-world style of 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-YjExMzI1YTctMTM2ZC00YWVmLTgyZTAtZjhhYmMzOWE1ODcx' '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'

This is a good example of a policy that’s intentionally explicit. default-src is not doing the heavy lifting because once you define script-src, connect-src, frame-src, and friends, those specific directives take over.

So if you add HubSpot domains only to default-src, it won’t fix blocked API calls when connect-src already exists. Same for chat iframes when frame-src is already locked down.

Bad fix:

default-src 'self' https://js.hs-scripts.com https://*.hubspot.com;

Real fix:

script-src 'self' 'nonce-...'
  'strict-dynamic'
  https://www.googletagmanager.com
  https://*.cookiebot.com
  https://*.google-analytics.com
  https://js.hs-scripts.com
  https://js.usemessages.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://*.hubspot.com
  https://*.hubapi.com
  https://*.usemessages.com
  wss://*.hubspot.com
  wss://*.usemessages.com;

frame-src 'self'
  https://consentcdn.cookiebot.com
  https://*.hubspot.com
  https://*.usemessages.com;

That’s the difference between “we added HubSpot to CSP” and “the widget actually works.”

Mistake #3: Breaking your nonce/strict-dynamic setup with a raw third-party script tag

If your site already uses nonces and 'strict-dynamic', don’t casually paste a plain external script tag into a template and assume the host allowlist will save you.

A lot of modern CSP deployments look like this:

script-src 'self' 'nonce-rAnd0m123' 'strict-dynamic';

With 'strict-dynamic', trust flows from nonce-bearing scripts. Host allowlists become much less relevant in supporting browsers. If your HubSpot snippet is added by your application and gets the nonce, great. If not, it may be blocked even though the HubSpot host appears in script-src.

I prefer to load third-party scripts from a nonce-approved bootstrap script I control.

Example:

<script nonce="{{ .CSPNonce }}">
  const s = document.createElement('script');
  s.src = 'https://js.hs-scripts.com/YOUR_PORTAL_ID.js';
  s.async = true;
  s.defer = true;
  document.head.appendChild(s);
</script>

And the header:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{NONCE}}' 'strict-dynamic';
  connect-src 'self' https://*.hubspot.com https://*.hubapi.com https://*.usemessages.com wss://*.hubspot.com wss://*.usemessages.com;
  frame-src 'self' https://*.hubspot.com https://*.usemessages.com;
  img-src 'self' data: https:;
  style-src 'self' 'unsafe-inline';
  object-src 'none';
  base-uri 'self';

If your stack supports nonces, use them consistently. Mixing nonce-based trust with random hardcoded third-party tags is where things get weird fast.

Mistake #4: Forgetting websocket endpoints

Chat widgets love realtime connections. If the launcher appears but messages don’t update, or presence indicators are broken, check connect-src for wss: endpoints.

I’ve seen developers allow HTTPS APIs but forget websockets entirely:

connect-src 'self' https://*.hubspot.com https://*.usemessages.com;

That’s incomplete for chat.

Fix:

connect-src 'self'
  https://*.hubspot.com
  https://*.hubapi.com
  https://*.usemessages.com
  wss://*.hubspot.com
  wss://*.usemessages.com;

When debugging, the browser console usually tells you exactly which websocket URL got blocked. CSP debugging is one of the few times where reading the raw console message saves you twenty minutes of guessing.

Mistake #5: Using frame-src 'none' or an overly strict frame policy

Some teams lock down frames hard, which I generally like. But HubSpot chat often depends on iframes.

If you have this:

frame-src 'none';

or even this:

frame-src 'self';

the chat UI may never render.

Fix it by allowing the specific HubSpot frame origins you actually need:

frame-src 'self' https://*.hubspot.com https://*.usemessages.com;

If your site uses the older child-src, make sure you know which browsers you care about, but for modern deployments I’d focus on frame-src.

Mistake #6: Reaching for https: wildcards instead of specific hosts

When chat breaks under deadline pressure, the ugly fix is usually this:

script-src 'self' https:;
connect-src 'self' https: wss:;
frame-src 'self' https:;

Yes, it works. It also means any HTTPS origin can serve active content or receive data under those directives. That’s not a policy; that’s panic.

Be specific. Start in Report-Only, load the widget, collect violations, and add only the hosts that are genuinely required.

If you want a base pattern to work from, keep it tight:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' https://js.hs-scripts.com https://js.usemessages.com;
  connect-src 'self' https://*.hubspot.com https://*.hubapi.com https://*.usemessages.com wss://*.hubspot.com wss://*.usemessages.com;
  frame-src 'self' https://*.hubspot.com https://*.usemessages.com;
  img-src 'self' data: https:;
  style-src 'self' 'unsafe-inline';
  object-src 'none';
  base-uri 'self';
  report-to default-endpoint;

Then promote it to enforcing mode once you’ve validated the host list.

Mistake #7: Ignoring style violations because “it’s just a widget”

I don’t love 'unsafe-inline' in style-src, but third-party widgets often push you there unless they fully support nonces or hashes for every injected style block.

If HubSpot chat renders partially or looks broken, check for blocked inline styles in the console. You may need:

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

Would I allow 'unsafe-inline' in script-src? Absolutely not. In style-src, I’m still cautious, but I’ll accept it for some third-party UI widgets if the alternative is no widget and the rest of the policy remains strong.

If your current policy is already strict and you don’t want to weaken global styles, test whether the widget works without changing style-src first. Don’t add 'unsafe-inline' unless you actually need it.

Mistake #8: Making changes without a reporting loop

The fastest way to waste time is editing CSP blind.

Use Content-Security-Policy-Report-Only first, exercise the full chat flow, and watch what gets blocked:

  • launcher load
  • widget open
  • send message
  • receive reply
  • file upload if enabled
  • mobile viewport behavior
  • cookie consent interaction if you gate chat behind consent

If you need a reference point for policy structure, official CSP docs are still the best source, and for quick policy patterns I’d use https://csp-examples.com as a starting scaffold, not as a final answer.

A practical hardened example

If you already have a policy similar to the headertest.com one, here’s the kind of incremental update I’d make for HubSpot chat instead of rewriting everything:

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-{{NONCE}}' 'strict-dynamic'
    https://www.googletagmanager.com
    https://*.cookiebot.com
    https://*.google-analytics.com
    https://js.hs-scripts.com
    https://js.usemessages.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
    https://*.hubspot.com
    https://*.hubapi.com
    https://*.usemessages.com
    wss://*.hubspot.com
    wss://*.usemessages.com;
  frame-src 'self'
    https://consentcdn.cookiebot.com
    https://*.hubspot.com
    https://*.usemessages.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';

That’s usually enough to get a HubSpot chat deployment unstuck without blowing open the rest of the site.

My rule of thumb: if a widget forces you to relax CSP, do it narrowly, per directive, and only after you’ve watched actual violation data. Third-party chat is messy. Your policy doesn’t have to be.