If you’ve ever dropped a Miro embed into a page and thought “it’s just an iframe, this should be easy,” CSP will usually humble you pretty fast.

Miro embeds are one of those cases where a policy that looks clean and locked down can still fail in annoying ways. The iframe might not render, the board shell loads but stays blank, assets get blocked, or collaboration features quietly break because a websocket or worker got denied.

I’ve seen teams waste hours debugging this because they only added frame-src and stopped there. That’s usually not enough.

Here are the most common CSP mistakes I see with Miro embeds, why they happen, and how to fix them without turning your policy into * soup.

Mistake #1: Only allowing Miro in default-src

A very common starting point looks like this:

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

That feels reasonable, but it often won’t do what you expect. default-src is just a fallback. As soon as you define more specific directives like frame-src, script-src, connect-src, or img-src, those specific directives take over.

A real-world header from headertest.com shows this pattern clearly:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-YmJjYjhlNmMtMWVkMC00ZmQ5LTllZGYtZGQ4Mzc2ZTU3ZWEx' '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'

Notice how frame-src is explicitly set. That means default-src does not control frames anymore. If your site has a policy like this and you try to embed Miro, the browser will check frame-src, not default-src.

Fix

Allow Miro in frame-src explicitly:

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

If you want a quick refresher on directive fallback behavior, the official CSP docs are worth checking, and https://csp-guide.com has a clean explanation too.

Mistake #2: Forgetting that embedded apps make network requests

An iframe is not just a static document anymore. Modern embedded apps pull data, open sockets, load images, fetch fonts, and sometimes use workers.

If your page embeds Miro and the top-level page CSP is restrictive, you may need to allow Miro-related origins in directives beyond frame-src, depending on how the embed is implemented and what your app around it is doing.

A broken policy often looks like this:

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

The frame may appear, but features can fail if requests tied to the embedded experience or surrounding integration are blocked.

Fix

Start with a tighter but realistic policy:

Content-Security-Policy:
  default-src 'self';
  frame-src 'self' https://miro.com https://*.miro.com;
  connect-src 'self' https://miro.com https://*.miro.com wss://*.miro.com;
  img-src 'self' data: https:;
  style-src 'self' 'unsafe-inline';
  font-src 'self' https: data:;

I’m deliberately not pretending there’s one magical minimal allowlist for every Miro setup. The exact hostnames can vary over time and by feature. Use browser CSP violation reports and DevTools network errors to tighten this based on what your embed actually needs.

If you control a Miro integration script on your own page, check whether that script calls Miro APIs directly. If it does, your page’s connect-src absolutely matters.

Mistake #3: Allowing frame-src but forgetting child-src in older policies

This one is less common now, but it still shows up in legacy codebases and older middleware templates.

Some teams still use child-src because that used to cover frames and workers. Then they add frame-src later, or they only keep child-src and assume everything is covered consistently everywhere.

Bad example

Content-Security-Policy:
  default-src 'self';
  child-src 'self' https://miro.com;

Better

Content-Security-Policy:
  default-src 'self';
  frame-src 'self' https://miro.com https://*.miro.com;
  worker-src 'self' blob:;

Be explicit. If you need frames, use frame-src. If you need workers, use worker-src. Don’t rely on old fallback behavior unless you enjoy debugging browser quirks.

Mistake #4: Blocking workers and blob: URLs

This is one of the sneakier failures. A lot of rich apps use Web Workers, and many of them spin those workers up from blob: URLs. If your CSP is strict, that can break functionality without making the root cause obvious.

Typical too-strict policy:

Content-Security-Policy:
  default-src 'self';
  frame-src 'self' https://miro.com https://*.miro.com;
  worker-src 'self';

If the embedded app or your integration depends on blob-backed workers, that won’t be enough.

Fix

Allow blob: for workers if your testing shows it’s required:

Content-Security-Policy:
  default-src 'self';
  frame-src 'self' https://miro.com https://*.miro.com;
  worker-src 'self' blob:;

Sometimes you’ll also need script-src blob: for app code you own, but don’t add that casually. worker-src blob: is a much narrower exception.

Mistake #5: Using frame-ancestors 'none' and wondering why your own embedded page can’t be framed

This one bites teams building internal wrapper pages around Miro.

The headertest.com example includes:

frame-ancestors 'none';

That directive controls who is allowed to embed your page. It does not control what your page is allowed to embed. People mix this up all the time.

If your app has an internal route like /whiteboard that includes a Miro iframe, and you also expect /whiteboard to be embedded into another internal portal, frame-ancestors 'none' will kill that.

Fix

Set frame-ancestors based on who should be allowed to embed your page:

Content-Security-Policy:
  default-src 'self';
  frame-src 'self' https://miro.com https://*.miro.com;
  frame-ancestors 'self' https://portal.example.com;

If the page should never be framed by anything, keep 'none'. Just don’t confuse that with iframe outbound permissions.

Mistake #6: Overusing sandbox on the iframe

This is not strictly a CSP directive, but it shows up in the same debugging session, so it belongs here.

You lock down the iframe with something like this:

<iframe
  src="https://miro.com/app/live-embed/..."
  sandbox=""
  referrerpolicy="strict-origin-when-cross-origin">
</iframe>

That’s maximum restriction. It also breaks most useful embedded apps.

Then somebody weakens CSP because they think CSP is the blocker, when the real issue is the iframe sandbox.

Fix

Use only the sandbox permissions you actually need. For many interactive embeds, you’ll at least need:

<iframe
  src="https://miro.com/app/live-embed/..."
  sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
  referrerpolicy="strict-origin-when-cross-origin"
  allowfullscreen>
</iframe>

Test this carefully. allow-same-origin is a big switch, but many app embeds won’t function without it.

Mistake #7: Shipping script-src 'strict-dynamic' and assuming host allowlists still behave the way you think

This is a more advanced gotcha, but worth calling out because the headertest.com header includes it:

script-src 'self' 'nonce-...' 'strict-dynamic' https://www.googletagmanager.com ...

'strict-dynamic' changes how trust propagates for scripts. If your Miro embed flow depends on scripts you load on your page, and you’re mixing nonces, host allowlists, and third-party bootstrapping scripts, your mental model can get out of sync with what the browser is actually enforcing.

For a plain iframe-only Miro embed, this may not matter much. For pages that also load SDKs or wrapper scripts, it can.

Fix

If you’re using nonce-based CSP with 'strict-dynamic', keep the script loading model simple:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m123' 'strict-dynamic';
  frame-src 'self' https://miro.com https://*.miro.com;
  connect-src 'self' https://*.miro.com wss://*.miro.com;

Then make sure any bootstrap script on the page has the nonce:

<script nonce="rAnd0m123">
  // your embed wrapper logic
</script>

If you’re not intentionally using dynamic script trust, don’t cargo-cult 'strict-dynamic' into the policy.

Mistake #8: Debugging from the server config only, instead of reading the browser violation

I still see people editing Nginx or app middleware blindly, redeploying, refreshing, and guessing.

Don’t do that. CSP tells you what it blocked.

A violation usually gives you the blocked URL and the directive. That’s the shortest path to fixing a Miro embed.

Useful reporting setup

Content-Security-Policy-Report-Only:
  default-src 'self';
  frame-src 'self' https://miro.com https://*.miro.com;
  connect-src 'self';
  report-to csp-endpoint;

And the matching reporting group:

Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://example.com/csp-reports"}]}

Roll out changes in Report-Only first if you’re not sure what the embed needs.

A practical baseline policy for Miro embeds

If I were starting from scratch for a page that embeds Miro and nothing else unusual, I’d begin with something like this:

Content-Security-Policy:
  default-src 'self';
  base-uri 'self';
  object-src 'none';
  form-action 'self';
  frame-ancestors 'self';
  frame-src 'self' https://miro.com https://*.miro.com;
  connect-src 'self' https://miro.com https://*.miro.com wss://*.miro.com;
  img-src 'self' data: https:;
  style-src 'self' 'unsafe-inline';
  font-src 'self' https: data:;
  worker-src 'self' blob:;

And the embed:

<iframe
  src="https://miro.com/app/live-embed/uXjVExample/?moveToViewport=-123,-456,789,321"
  width="100%"
  height="600"
  style="border:0;"
  loading="lazy"
  sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
  referrerpolicy="strict-origin-when-cross-origin"
  allowfullscreen>
</iframe>

Then I’d test, read violations, and trim or extend as needed.

That’s the real pattern with CSP and third-party embeds: start strict, allow only what breaks for a documented reason, and don’t confuse one directive with another. Most Miro CSP problems come from assuming iframes are simple. They aren’t.