CSP for Tidio Live Chat: A Practical Setup Guide
Table of Contents
Tidio is one of those integrations that looks trivial until your CSP blocks half of it.
You paste the widget snippet, refresh, and the chat bubble never appears. Then DevTools starts yelling about script-src, connect-src, frame-src, maybe img-src, and sometimes even style-src. If your site already has a reasonably locked-down policy, Tidio will force you to be explicit.
I’ve had better results treating chat widgets like mini-applications instead of “just another script”. They load scripts, open WebSocket connections, render frames, fetch images, and inject styles. Your CSP has to allow that behavior without turning into default-src * 'unsafe-inline' 'unsafe-eval'.
Here’s how I’d set up CSP for Tidio on a real site.
Start from a real baseline
A realistic CSP often already exists before you add chat. For example, this is a real policy shape from headertest.com:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-OWU2OWI1OTctNzE5NC00ZjgwLWJkNjctMjFjOTgxYTBjNWUy' '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 decent starting point: nonces, strict-dynamic, no object-src, locked frame-ancestors. Good.
Now add Tidio, and the missing pieces are usually:
script-srcfor Tidio’s loaderconnect-srcfor API and WebSocket trafficframe-srcfor embedded chat UIimg-srcfor avatars and assets- sometimes
style-srcif the widget injects inline styles or loads CSS - sometimes
font-srcdepending on how assets are served
The Tidio embed code
A typical Tidio install looks like this:
<script src="//code.tidio.co/YOUR_PUBLIC_KEY.js" async></script>
If your CSP is strict, that script won’t load unless you explicitly allow it or load it via a nonce/hashes strategy that permits dynamic descendants.
If you already use nonces and strict-dynamic, I prefer adding the nonce to your inline bootstrap script and then injecting Tidio from there.
Example:
<script nonce="{{ .CSPNonce }}">
const s = document.createElement('script');
s.src = 'https://code.tidio.co/YOUR_PUBLIC_KEY.js';
s.async = true;
document.head.appendChild(s);
</script>
With strict-dynamic, the trusted nonce-bearing script can load Tidio without you having to keep whitelisting every descendant script host. That’s cleaner than spraying domains into script-src.
Minimal CSP changes for Tidio
If you want the shortest path to a working setup, start with something like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://code.tidio.co;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://code.tidio.co https://api-v2.tidio.co wss://ws-widget.tidiochat.com https://widget-v4.tidiochat.com;
frame-src 'self' https://www.tidiochat.com https://widget-v4.tidiochat.com;
font-src 'self' data: https:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
This is not guaranteed to be the exact final list for every Tidio deployment, because vendors change hostnames over time. Chat vendors do this a lot. Expect to confirm actual requests in DevTools and refine from there.
Merging Tidio into an existing policy
Using the headertest.com policy shape, here’s what a merged version might look like:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-OWU2OWI1OTctNzE5NC00ZjgwLWJkNjctMjFjOTgxYTBjNWUy' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://code.tidio.co;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
img-src 'self' data: https:;
font-src 'self' data: https:;
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://code.tidio.co
https://api-v2.tidio.co
https://widget-v4.tidiochat.com
wss://ws-widget.tidiochat.com;
frame-src 'self' https://consentcdn.cookiebot.com https://www.tidiochat.com https://widget-v4.tidiochat.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
That’s the kind of policy I’d actually ship first in report-only mode, then tighten after watching violations.
Express example
If you’re setting headers in Node/Express, here’s a practical pattern with a nonce:
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.use((req, res, next) => {
const nonce = crypto.randomUUID();
res.locals.cspNonce = nonce;
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://code.tidio.co 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' data: https:",
"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://code.tidio.co https://api-v2.tidio.co https://widget-v4.tidiochat.com wss://ws-widget.tidiochat.com",
"frame-src 'self' https://consentcdn.cookiebot.com https://www.tidiochat.com https://widget-v4.tidiochat.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'"
].join('; ');
res.setHeader('Content-Security-Policy', csp);
next();
});
app.get('/', (req, res) => {
res.send(`
<!doctype html>
<html>
<head>
<script nonce="${res.locals.cspNonce}">
const s = document.createElement('script');
s.src = 'https://code.tidio.co/YOUR_PUBLIC_KEY.js';
s.async = true;
document.head.appendChild(s);
</script>
</head>
<body>
<h1>Hello</h1>
</body>
</html>
`);
});
app.listen(3000);
I like this pattern because it keeps inline bootstrapping trusted without falling back to 'unsafe-inline' in script-src.
Nginx example
If you terminate at Nginx and your app doesn’t generate a nonce, you can still allow Tidio directly in script-src:
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' https://code.tidio.co;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data: https:;
connect-src 'self' https://code.tidio.co https://api-v2.tidio.co https://widget-v4.tidiochat.com wss://ws-widget.tidiochat.com;
frame-src 'self' https://www.tidiochat.com https://widget-v4.tidiochat.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
" always;
That works, but I’d still choose nonces if you already have a modern CSP. Direct host allowlists age badly.
Debugging the usual CSP failures
When Tidio breaks, the browser console usually tells you exactly which directive is missing.
Blocked script
Error shape:
Refused to load the script 'https://code.tidio.co/...' because it violates the following Content Security Policy directive: "script-src ..."
Fix:
- add
https://code.tidio.cotoscript-src, or - load it from a nonce-bearing script if you use
strict-dynamic
Blocked WebSocket
Error shape:
Refused to connect to 'wss://ws-widget.tidiochat.com/...' because it violates the following Content Security Policy directive: "connect-src ..."
Fix:
- add
wss://ws-widget.tidiochat.comtoconnect-src
This one gets missed constantly. People remember HTTPS endpoints and forget WebSockets.
Blocked iframe
Error shape:
Refused to frame 'https://widget-v4.tidiochat.com/...' because it violates the following Content Security Policy directive: "frame-src ..."
Fix:
- add the reported Tidio host to
frame-src
Blocked image or avatar
Error shape:
Refused to load the image 'https://...' because it violates the following Content Security Policy directive: "img-src ..."
Fix:
- if you already allow
https:inimg-src, you may be fine - otherwise add the specific image host
I usually keep img-src 'self' data: https: unless I have a strong reason to constrain it further.
Use Report-Only first
For third-party widgets, I strongly recommend starting with:
Content-Security-Policy-Report-Only: ...
That lets you collect violations without breaking chat for users. Once requests stabilize, move the policy to enforcing mode.
If you want ready-made policy shapes for comparison, https://csp-examples.com is handy.
A stricter version
If you want to tighten the policy after confirming Tidio behavior, here’s the direction I’d take:
- keep
object-src 'none' - keep
base-uri 'self' - keep
frame-ancestors 'none' - avoid
'unsafe-inline'inscript-src - only keep
'unsafe-inline'instyle-srcif the widget actually needs it - replace broad
https:host allowances with exact domains where feasible
Example:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://code.tidio.co;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://widget-v4.tidiochat.com https://code.tidio.co;
font-src 'self' data:;
connect-src 'self' https://api-v2.tidio.co https://widget-v4.tidiochat.com wss://ws-widget.tidiochat.com;
frame-src 'self' https://www.tidiochat.com https://widget-v4.tidiochat.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
That’s closer to what I’d call maintainable.
Final checklist
Before you call it done, verify all of these in DevTools:
- Tidio loader script fetches successfully
- chat bubble appears
- widget opens
- WebSocket connection succeeds
- avatars and icons load
- no CSP violations on page load or interaction
- policy still blocks unexpected inline script execution
Official CSP documentation is here: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
And the practical rule with Tidio is simple: don’t guess the final host list. Start with a tight baseline, run in report-only mode, click through the chat flow, and allow only what the widget actually uses. That gets you a working live chat without gutting your CSP.