If you embed Whimsical and your CSP is even slightly too strict, it will break in ways that are annoying to debug. Usually you get a blank iframe, a console error, and a vague sense that CSP is punishing you for trying to ship something useful.

Here’s the good news: Whimsical embeds are usually straightforward because they’re iframe-based. That means you often only need to allow the Whimsical frame itself, instead of opening up script-src or connect-src all over your app.

I like iframe embeds for this reason. They keep third-party code boxed in, and your CSP can stay mostly boring.

The embed we’re dealing with

A typical Whimsical embed looks something like this:

<iframe
  class="whimsical-embed"
  src="https://whimsical.com/embed/EXAMPLE"
  width="100%"
  height="600"
  loading="lazy"
  referrerpolicy="strict-origin-when-cross-origin"
  allowfullscreen
></iframe>

If your CSP does not allow Whimsical in frame-src, the browser will block it.

The minimum CSP change

If your site already has a CSP, the smallest working change is usually:

Content-Security-Policy: default-src 'self'; frame-src 'self' https://whimsical.com;

That tells the browser:

  • load everything from your own origin by default
  • allow iframes from your own site and https://whimsical.com

If you use the older fallback behavior, child-src may also matter for older setups, but in modern CSP, frame-src is the one you want. If you want a refresher on directive behavior, https://csp-guide.com is useful and concise.

Start from a real CSP, not a toy one

Toy policies are fine for docs, but production sites already have a pile of directives. The real problem is modifying an existing CSP without accidentally weakening it.

Here’s a real 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-YzI4NWUzYTItZTAxMC00MTBiLTg0MzctZGI1ZjcxMGRiNTg0' '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'

To support Whimsical, I would change only frame-src:

Content-Security-Policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-YzI4NWUzYTItZTAxMC00MTBiLTg0MzctZGI1ZjcxMGRiNTg0' '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 https://whimsical.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

That’s it.

I would not add Whimsical to script-src, style-src, img-src, or connect-src unless you’ve verified you actually need it. For iframe embeds, you usually don’t.

Why frame-src is enough most of the time

This trips people up all the time.

Resources loaded inside the iframe are governed by the embedded page’s own CSP, not yours. Your page’s CSP controls whether the browser is allowed to load that iframe in the first place.

So if the embed is:

<iframe src="https://whimsical.com/embed/EXAMPLE"></iframe>

your app generally only needs:

frame-src https://whimsical.com

You do not need to whitelist all the JavaScript and API endpoints Whimsical uses internally, unless you’re loading Whimsical resources directly into your top-level page.

A stricter example

If you want a cleaner, tighter policy for a page that only embeds Whimsical and doesn’t need much else:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m123';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  frame-src 'self' https://whimsical.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';

That’s a pretty decent baseline.

A couple of opinions here:

  • object-src 'none' should be in basically every modern CSP.
  • base-uri 'self' is cheap protection. I always add it.
  • frame-ancestors 'none' is great if your site should never be framed by someone else.

Express example

If you’re serving a Node/Express app, set the header explicitly or use Helmet.

Plain Express

import express from "express";

const app = express();

app.use((req, res, next) => {
  res.setHeader(
    "Content-Security-Policy",
    [
      "default-src 'self'",
      "script-src 'self'",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "font-src 'self'",
      "connect-src 'self'",
      "frame-src 'self' https://whimsical.com",
      "frame-ancestors 'none'",
      "base-uri 'self'",
      "form-action 'self'",
      "object-src 'none'"
    ].join("; ")
  );
  next();
});

app.get("/", (req, res) => {
  res.send(`
    <!doctype html>
    <html>
      <body>
        <h1>Whimsical embed</h1>
        <iframe
          src="https://whimsical.com/embed/EXAMPLE"
          width="100%"
          height="600"
          loading="lazy"
          referrerpolicy="strict-origin-when-cross-origin"
          allowfullscreen
        ></iframe>
      </body>
    </html>
  `);
});

app.listen(3000);

Express with Helmet

import express from "express";
import helmet from "helmet";

const app = express();

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:"],
        fontSrc: ["'self'"],
        connectSrc: ["'self'"],
        frameSrc: ["'self'", "https://whimsical.com"],
        frameAncestors: ["'none'"],
        baseUri: ["'self'"],
        formAction: ["'self'"],
        objectSrc: ["'none'"]
      }
    }
  })
);

app.listen(3000);

If you already have a Helmet config, just append https://whimsical.com to frameSrc.

Nginx example

For Nginx:

add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-src 'self' https://whimsical.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'" always;

If you have different policies per route, I’d seriously consider applying the Whimsical allowance only on pages that actually render the embed.

Common mistakes

1. Adding Whimsical to the wrong directive

I see people do this:

script-src 'self' https://whimsical.com

That does nothing for an iframe embed if frame-src still blocks it.

2. Forgetting frame-src falls back to default-src

If you don’t specify frame-src, the browser may fall back to default-src. So this policy will block Whimsical:

Content-Security-Policy: default-src 'self'

because https://whimsical.com is not allowed by default.

3. Over-allowing all frames

This works:

frame-src *

It also wrecks the point of having a tight CSP around embeds. Don’t do this unless you enjoy future incident reviews.

4. Confusing frame-src with frame-ancestors

These are completely different.

  • frame-src: what your page can embed
  • frame-ancestors: who can embed your page

If Whimsical isn’t loading, changing frame-ancestors won’t help.

How to debug it fast

Open DevTools and look for errors like:

Refused to frame 'https://whimsical.com/' because it violates the following Content Security Policy directive: "frame-src 'self'".

That message usually tells you the exact directive causing the failure.

You can also temporarily switch to report-only mode while testing:

Content-Security-Policy-Report-Only: default-src 'self'; frame-src 'self' https://whimsical.com;

That won’t block content, but it will show violations in the console. I use this when I’m tightening a policy incrementally on a production app.

Do you need sandboxing too?

Sometimes yes.

If you want extra restrictions on the iframe itself, add a sandbox attribute. For example:

<iframe
  src="https://whimsical.com/embed/EXAMPLE"
  sandbox="allow-scripts allow-same-origin allow-popups"
  width="100%"
  height="600"
></iframe>

Be careful here. Sandbox rules can break embed functionality fast. I only add sandboxing after I’ve tested exactly what the embed needs.

A production-ready recommendation

If you already have a CSP, make the smallest possible change:

frame-src 'self' https://whimsical.com

If you’re starting fresh, use something like:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-<dynamic-nonce>';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self';
  frame-src 'self' https://whimsical.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';

Then test the actual embed page in a real browser, with DevTools open, before calling it done.

That’s the practical version: keep your CSP narrow, allow Whimsical only in frame-src, and resist the urge to scatter third-party domains across every directive just because an iframe exists.