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-srcfor the loaderconnect-srcfor API/XHR/fetch/websocket callsframe-srcfor embedded chat UI or supporting framesimg-srcfor avatars, beacons, and tracking pixels- sometimes
style-srcif 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.