Crisp is easy to drop into a site. Getting it past a strict Content Security Policy is where people usually lose an afternoon.
The widget loads scripts, opens network connections, pulls images and fonts, and may embed frames depending on what features you enable. If your CSP is tight — and it should be — you need to explicitly allow what Crisp uses without blowing a hole in the rest of the policy.
This guide is the practical version: what to allow, what to avoid, and copy-paste policies you can start from.
The usual Crisp embed
Most teams add Crisp with the standard snippet:
<script type="text/javascript">
window.$crisp = [];
window.CRISP_WEBSITE_ID = "YOUR_WEBSITE_ID";
(function () {
var d = document;
var s = d.createElement("script");
s.src = "https://client.crisp.chat/l.js";
s.async = 1;
d.getElementsByTagName("head")[0].appendChild(s);
})();
</script>
From a CSP perspective, this raises two issues:
- You’re using an inline script.
- That inline script injects a remote script from
https://client.crisp.chat.
If you already run a nonce-based CSP, use a nonce on the embed script and allow the Crisp host. That’s the cleanest option.
Minimal CSP for Crisp
If you just want Crisp working and don’t care about a very strict policy yet, this is the smallest useful starting point:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' https://client.crisp.chat;
connect-src 'self' https://client.crisp.chat wss://client.relay.crisp.chat https://storage.crisp.chat;
img-src 'self' data: https://image.crisp.chat https://storage.crisp.chat;
style-src 'self' 'unsafe-inline' https://client.crisp.chat;
font-src 'self' https://client.crisp.chat;
frame-src https://client.crisp.chat;
This is permissive enough for most Crisp installs. It is not my favorite because of 'unsafe-inline', but it gets people unstuck fast.
Better CSP: nonce-based Crisp setup
If you control the page template, use a nonce instead of 'unsafe-inline'.
HTML
<script nonce="{{ .CSPNonce }}">
window.$crisp = [];
window.CRISP_WEBSITE_ID = "YOUR_WEBSITE_ID";
(function () {
var d = document;
var s = d.createElement("script");
s.src = "https://client.crisp.chat/l.js";
s.async = 1;
d.head.appendChild(s);
})();
</script>
CSP header
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{RANDOM_NONCE}}' https://client.crisp.chat;
connect-src 'self' https://client.crisp.chat wss://client.relay.crisp.chat https://storage.crisp.chat;
img-src 'self' data: https://image.crisp.chat https://storage.crisp.chat;
style-src 'self' 'unsafe-inline' https://client.crisp.chat;
font-src 'self' https://client.crisp.chat;
frame-src https://client.crisp.chat;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Why still keep 'unsafe-inline' in style-src? Because many third-party widgets, including chat widgets, inject inline styles. You can try removing it, but don’t be surprised if the launcher or chatbox breaks.
Strict-dynamic setup for modern CSPs
If your site already uses nonces and 'strict-dynamic', Crisp fits nicely. I prefer this model on modern apps because I don’t have to keep stuffing script-src with every downstream script host.
Here’s the shape:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic';
connect-src 'self' https://client.crisp.chat wss://client.relay.crisp.chat https://storage.crisp.chat;
img-src 'self' data: https://image.crisp.chat https://storage.crisp.chat;
style-src 'self' 'unsafe-inline' https://client.crisp.chat;
font-src 'self' https://client.crisp.chat;
frame-src https://client.crisp.chat;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
This works because the trusted nonce-bearing script creates the Crisp loader script. If you’re not already comfortable with nonce plumbing, don’t start here on a Friday.
For a real-world example of a nonce + strict-dynamic policy, the CSP served by HeaderTest is a good reference:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-ZmQyZDc5ZjEtMzFjYy00ZWY4LWE0NWEtNjFkYmU1N2VlYmIz' '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 Crisp policy, but it shows the pattern clearly.
Copy-paste examples by platform
Nginx
Basic version:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://client.crisp.chat; connect-src 'self' https://client.crisp.chat wss://client.relay.crisp.chat https://storage.crisp.chat; img-src 'self' data: https://image.crisp.chat https://storage.crisp.chat; style-src 'self' 'unsafe-inline' https://client.crisp.chat; font-src 'self' https://client.crisp.chat; frame-src https://client.crisp.chat; object-src 'none'; base-uri 'self'; frame-ancestors 'none';" always;
Apache
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://client.crisp.chat; connect-src 'self' https://client.crisp.chat wss://client.relay.crisp.chat https://storage.crisp.chat; img-src 'self' data: https://image.crisp.chat https://storage.crisp.chat; style-src 'self' 'unsafe-inline' https://client.crisp.chat; font-src 'self' https://client.crisp.chat; frame-src https://client.crisp.chat; object-src 'none'; base-uri 'self'; frame-ancestors 'none';"
Express / Node.js with Helmet
import helmet from "helmet";
import express from "express";
const app = express();
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
"default-src": ["'self'"],
"script-src": ["'self'", "'unsafe-inline'", "https://client.crisp.chat"],
"connect-src": [
"'self'",
"https://client.crisp.chat",
"wss://client.relay.crisp.chat",
"https://storage.crisp.chat"
],
"img-src": [
"'self'",
"data:",
"https://image.crisp.chat",
"https://storage.crisp.chat"
],
"style-src": ["'self'", "'unsafe-inline'", "https://client.crisp.chat"],
"font-src": ["'self'", "https://client.crisp.chat"],
"frame-src": ["https://client.crisp.chat"],
"object-src": ["'none'"],
"base-uri": ["'self'"],
"frame-ancestors": ["'none'"]
}
}
})
);
Next.js with nonce
If you generate a per-request nonce in middleware or your edge layer:
export default function Page({ nonce }: { nonce: string }) {
return (
<>
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `
window.$crisp = [];
window.CRISP_WEBSITE_ID = "YOUR_WEBSITE_ID";
(function () {
var d = document;
var s = d.createElement("script");
s.src = "https://client.crisp.chat/l.js";
s.async = 1;
d.head.appendChild(s);
})();
`
}}
/>
</>
);
}
And the header:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic';
connect-src 'self' https://client.crisp.chat wss://client.relay.crisp.chat https://storage.crisp.chat;
img-src 'self' data: https://image.crisp.chat https://storage.crisp.chat;
style-src 'self' 'unsafe-inline' https://client.crisp.chat;
font-src 'self' https://client.crisp.chat;
frame-src https://client.crisp.chat;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
What each directive is doing
You don’t need to memorize this, but you do need to know where to look when the console starts yelling.
script-src: allows the Crisp loader script.connect-src: allows XHR, fetch, EventSource, and WebSocket traffic used by the chat client.img-src: covers avatars, uploaded images, and widget assets.style-src: usually needs'unsafe-inline'because widgets inject styles.font-src: covers widget fonts if Crisp serves them from its own host.frame-src: needed if the widget uses embedded frames.
Common CSP errors with Crisp
Refused to load the script
Example:
Refused to load the script 'https://client.crisp.chat/l.js' because it violates the following Content Security Policy directive: "script-src 'self'".
Fix:
script-src 'self' https://client.crisp.chat;
Or, if using a nonce-based embed, make sure the inline script has the right nonce.
Refused to connect
Example:
Refused to connect to 'wss://client.relay.crisp.chat/...' because it violates the following Content Security Policy directive: "connect-src 'self'".
Fix:
connect-src 'self' https://client.crisp.chat wss://client.relay.crisp.chat https://storage.crisp.chat;
Refused to apply inline style
Example:
Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'self'".
Fix:
style-src 'self' 'unsafe-inline' https://client.crisp.chat;
I don’t love allowing inline styles, but with third-party widgets this is often the practical tradeoff.
Recommended production policy
If someone asked me for the default production starting point for Crisp, I’d use this and tighten only if testing proves I can:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic' https://client.crisp.chat;
connect-src 'self' https://client.crisp.chat wss://client.relay.crisp.chat https://storage.crisp.chat;
img-src 'self' data: https://image.crisp.chat https://storage.crisp.chat;
style-src 'self' 'unsafe-inline' https://client.crisp.chat;
font-src 'self' https://client.crisp.chat;
frame-src 'self' https://client.crisp.chat;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
If you want more ready-made patterns for strict and non-strict policies, csp-examples.com is a useful shortcut.
A couple of blunt recommendations
- Don’t throw
https:intoscript-srcjust to make the widget work. That’s the classic lazy fix and it destroys the point of CSP. - Don’t keep
'unsafe-inline'inscript-srcif you can move to nonces. For Crisp, there’s no reason to accept that risk if you control the embed code. - Roll changes out with
Content-Security-Policy-Report-Onlyfirst if the site is busy or revenue-critical.
A report-only version looks like this:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic' https://client.crisp.chat;
connect-src 'self' https://client.crisp.chat wss://client.relay.crisp.chat https://storage.crisp.chat;
img-src 'self' data: https://image.crisp.chat https://storage.crisp.chat;
style-src 'self' 'unsafe-inline' https://client.crisp.chat;
font-src 'self' https://client.crisp.chat;
frame-src 'self' https://client.crisp.chat;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Then watch the browser console and your CSP reports before enforcing it.
That’s the whole game with Crisp and CSP: allow only the hosts the widget actually needs, use a nonce for the embed if you can, and resist the temptation to “just allow everything for now.” That temporary policy has a way of surviving for years.