I’ve seen this exact failure more than once: a docs page ships with a pretty strict Content Security Policy, someone drops in a StackBlitz embed for an interactive demo, and suddenly the page shows a blank box or a browser console full of CSP errors.
The frustrating part is that nothing feels obviously broken. The iframe markup looks fine. The StackBlitz project URL loads directly in a new tab. But embedded inside your site? Dead.
This is usually a CSP problem, and StackBlitz is a good example of why CSP work is rarely “set it once and forget it.”
The setup
Say you run a developer-focused site like csp-examples, and you want to embed a live StackBlitz demo in a tutorial.
Your page might start simple enough:
<iframe
src="https://stackblitz.com/edit/csp-demo?embed=1&file=index.js"
width="100%"
height="600"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
title="StackBlitz demo">
</iframe>
And your CSP might look pretty reasonable:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
That policy is strict, clean, and very likely to break the embed.
What broke
The first failure is usually the easiest one: frame-src 'self' blocks StackBlitz entirely.
Typical browser error:
Refused to frame 'https://stackblitz.com/' because it violates the following
Content Security Policy directive: "frame-src 'self'".
That one is straightforward. If you want third-party iframes, you need to explicitly allow them.
So the obvious first fix is:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'self' https://stackblitz.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Sometimes that’s enough for a basic embed. Sometimes it absolutely is not.
The real-world surprise
StackBlitz doesn’t behave like a dead-simple YouTube-style iframe. It’s an application. It may pull assets, establish connections, open previews, and rely on additional origins depending on the embed mode and feature set.
This is where people get tripped up. They allow https://stackblitz.com in frame-src, reload, and still get weird failures inside the embedded experience.
Now, to be clear: your page CSP governs what your page can load. It does not rewrite StackBlitz’s own CSP. But if your page embeds related third-party resources around the widget, or if you’re using wrappers/scripts/helpers in addition to the iframe, the policy often needs more than one tweak.
I usually debug this in two passes:
- Make the iframe itself load.
- Watch the console and network tab for everything else the page now tries to do.
If you want a quick sanity check on your current headers, HeaderTest is handy for seeing the actual response headers your page is sending, especially when CDNs or reverse proxies quietly mutate them.
Before: the “secure but broken” policy
Here’s a realistic “before” header I’d expect on a hardened docs site:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
And here’s what the team sees:
- StackBlitz iframe doesn’t render
- Console shows
frame-srcviolations - Someone suggests loosening everything to
default-src * - Everyone briefly forgets why CSP existed in the first place
I’m pretty opinionated here: don’t nuke your policy because one embed is failing. Fix the exact directive that’s blocking the exact thing you need.
After: a targeted fix
A much better version is this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-src 'self' https://stackblitz.com https://*.stackblitz.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
That does two useful things:
- Keeps the policy narrow
- Allows for StackBlitz subdomains if the embed flow uses them
If your implementation also loads StackBlitz helper scripts on the parent page, then you may need to allow them in script-src too:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m' https://stackblitz.com https://*.stackblitz.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-src 'self' https://stackblitz.com https://*.stackblitz.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
That said, if you’re only embedding via plain iframe markup, don’t preemptively widen script-src. Start with frame-src. CSP should be earned by observed need, not guessed upfront.
A better embed pattern
If I were shipping this on a production docs site, I’d also avoid burying the iframe directly in markdown-generated HTML without any fallback. Give users a direct link in case browser privacy settings, corporate filtering, or a future CSP regression blocks the frame.
<div class="demo-embed">
<iframe
src="https://stackblitz.com/edit/csp-demo?embed=1&file=index.js"
width="100%"
height="600"
loading="lazy"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
title="Live StackBlitz demo">
</iframe>
<p>
If the embed does not load, open it directly:
<a href="https://stackblitz.com/edit/csp-demo?file=index.js" target="_blank" rel="noopener">
View on StackBlitz
</a>
</p>
</div>
That fallback saves support time. People will tell you “your site is broken” when the real issue is their environment blocking third-party frames.
Learning from a real CSP
A good production CSP usually balances strictness with actual business needs. Here’s the real CSP 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-YTQxNzIyODUtNDMzMi00NjY2LThmYzAtNjhhMmZlYjQ5NTM3' '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'
What I like about this header is that it’s specific. It doesn’t pretend the site only talks to itself. It explicitly allows the third-party origins the app really uses.
That’s the right mindset for StackBlitz too. If your site embeds a third-party tool, your CSP should say so clearly. Don’t hide behind default-src *, and don’t pretend the integration doesn’t exist.
A few details worth calling out:
frame-srcis narrowly scoped to the known framed originconnect-srcincludes explicit APIs and even awss:endpointobject-src 'none'andbase-uri 'self'are locked downframe-ancestors 'none'prevents clickjacking of the site itself
If you want a deeper breakdown of directives like frame-src, connect-src, or strict-dynamic, CSP Guide is a solid reference.
The mistake I see most often
Teams treat CSP errors as a reason to loosen the whole policy. That’s backwards.
If StackBlitz is blocked, the fix is usually not:
Content-Security-Policy: default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;
That’s not a fix. That’s giving up.
The right fix is to answer a few boring but useful questions:
- Is the embed just an iframe, or are we loading StackBlitz scripts too?
- Which origin is actually being blocked?
- Is it blocked by
frame-src,script-src, or something else? - Do we need the root domain, subdomains, or both?
- Are we solving a current failure, or making speculative allowances?
That process keeps the policy understandable six months later.
Final before-and-after
Here’s the compact version I’d hand to a team.
Before
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
After
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://stackblitz.com https://*.stackblitz.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
And if your parent page also loads StackBlitz-hosted scripts:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://stackblitz.com https://*.stackblitz.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-src 'self' https://stackblitz.com https://*.stackblitz.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
That’s the real lesson: CSP for embeds is rarely about making the policy “less strict.” It’s about making it accurate.