If you embed Balsamiq mockups on a site with a real Content Security Policy, you hit the usual tradeoff fast: keep the policy tight, or make the embed work with the least drama.

I’ve dealt with this enough times that I now split the problem into a few clear patterns. Balsamiq embeds are usually easiest when treated as a framed third-party app. The hard part is not Balsamiq itself. The hard part is everything around it: analytics, consent tools, inline styles, legacy scripts, and whatever your framework injects.

So here’s the practical comparison guide for CSP on pages that need Balsamiq embeds.

The baseline: what a real production CSP looks like

A lot of teams start with a policy that already has baggage. A real header from headertest.com looks like this:

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-NjBlN2YyOWQtODU4YS00ZmFmLWFhYWYtMTVmYjM0ZWVjZGQ3' '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 policy is already doing a few good things:

  • object-src 'none'
  • base-uri 'self'
  • form-action 'self'
  • frame-ancestors 'none'
  • nonce-based script-src with 'strict-dynamic'

But it also shows the usual compromises:

  • style-src 'unsafe-inline'
  • broad img-src https:
  • several third-party domains in multiple directives

Adding Balsamiq should not turn this into a wildcard party.

Option 1: Dedicated iframe allowlist for Balsamiq

This is the cleanest option if Balsamiq is embedded with an <iframe> and the parent page does not need to run Balsamiq scripts directly.

Example

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  frame-src 'self' https://*.balsamiq.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none'

If your exact embed host is more specific, use that instead of https://*.balsamiq.com.

Pros

  • Keeps Balsamiq isolated to frame-src
  • No need to trust Balsamiq in script-src, style-src, or connect-src
  • Easy to reason about during reviews
  • Least likely to weaken the rest of the app

Cons

  • Only works if the integration is truly iframe-based
  • You need to know the actual Balsamiq embed origin
  • If Balsamiq changes subdomains or uses supporting endpoints, you may need to update the policy

My take

If you can make Balsamiq work with just frame-src, do that and stop there. This is the boring option, which usually means it’s the right one.

Option 2: Add Balsamiq to an existing third-party-heavy policy

This is what most teams actually do. They already have GTM, Google Analytics, Cookiebot, maybe a form vendor, and now they want Balsamiq too.

Using the headertest.com policy as a base, the smallest likely change is:

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

Pros

  • Minimal change
  • Fastest path in an existing production setup
  • Low blast radius if Balsamiq only needs framing

Cons

  • Builds on top of an already complex policy
  • Makes it harder to tell which third party needs what
  • Teams tend to cargo-cult additions forever

My take

This is fine as a short-term move. I just wouldn’t stop here. Once you have five vendors in a policy, nobody remembers why half the entries exist.

Option 3: Split policy by route or page type

If only one page needs Balsamiq, don’t loosen CSP site-wide.

For example:

  • /app/* gets your stricter application policy
  • /docs/mockups/* gets a policy that allows Balsamiq frames

Example for the Balsamiq page

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data:;
  frame-src https://*.balsamiq.com;
  frame-ancestors 'none';
  base-uri 'self';
  object-src 'none';
  form-action 'self'

Pros

  • Best security boundary for mixed-content sites
  • Limits third-party exposure to the page that needs it
  • Easier maintenance over time

Cons

  • Requires route-level header support
  • More moving parts in app config, CDN config, or reverse proxy rules
  • Developers sometimes forget to apply the right policy to new pages

My take

I’m a big fan of route-specific CSP. If your platform supports it cleanly, this is the most mature option. Security policies should match page behavior, not wishful thinking.

Option 4: Over-broad fallback policy

This is the “just make it work” version:

Content-Security-Policy:
  default-src 'self' https:;
  script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
  style-src 'self' 'unsafe-inline' https:;
  img-src 'self' data: https:;
  connect-src 'self' https: wss:;
  frame-src 'self' https:;
  object-src 'none'

Pros

  • Things usually work quickly
  • Lowest integration friction

Cons

  • Bad policy
  • Very bad policy
  • You’ve effectively stopped using CSP as a meaningful control

My take

Don’t do this unless you’re debugging in a temporary environment and you’ve got the discipline to throw it away immediately after. Most teams don’t.

What directives usually matter for Balsamiq embeds

For a normal iframe embed, frame-src is the main one.

Depending on your page and integration style, you may also care about:

  • frame-src for the embedded origin
  • img-src if your own page loads remote preview assets
  • connect-src if your page fetches Balsamiq-related data directly
  • script-src only if you load Balsamiq JavaScript into the parent page
  • frame-ancestors to control who may embed your page

If you want directive-by-directive background, the official CSP reference on MDN Content Security Policy is the first place I’d check. For concise directive explainers, CSP Guide is also useful.

Best default for a simple Balsamiq iframe page

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  frame-src https://*.balsamiq.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none'

Best default when you already have third parties

Keep your existing allowlists, but only add Balsamiq where needed:

frame-src 'self' https://consentcdn.cookiebot.com https://*.balsamiq.com;

Not in default-src. Not in script-src. Not everywhere “just in case”.

Common mistakes

Putting Balsamiq in default-src only

That may work as a fallback in some cases, but it’s sloppy. Use the specific directive that matches the resource type. If it’s a frame, use frame-src.

Confusing frame-src and frame-ancestors

I still see this a lot.

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

Very different jobs.

Expanding script-src without evidence

If the embed is an iframe, the parent page usually does not need Balsamiq in script-src. Don’t grant script execution to a third party unless you know why.

Leaving report-only forever

Run a report-only policy while testing if you need to, then enforce it. A report-only CSP that never graduates is just security-themed logging.

My recommendation

If you’re embedding Balsamiq, I’d choose one of these in order:

  1. Route-specific CSP with Balsamiq only in frame-src
  2. Site-wide CSP with Balsamiq only in frame-src
  3. Anything broader only after verifying a real requirement

That’s the whole game: make the embed work with the narrowest permission set possible.

If your current policy looks like the headertest.com example, you’re already dealing with real-world complexity. Don’t make it worse by tossing Balsamiq into every directive. Add the exact origin to frame-src, test in report-only mode, inspect violations, and only widen further if the browser proves you need to.

That approach is slower by maybe ten minutes. It saves a lot more than ten minutes later.