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:

  1. You’re using an inline script.
  2. 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.

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: into script-src just to make the widget work. That’s the classic lazy fix and it destroys the point of CSP.
  • Don’t keep 'unsafe-inline' in script-src if 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-Only first 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.