CSP Mistakes That Break Apache Superset Embeds
Table of Contents
Embedding Apache Superset sounds simple: generate an iframe, allow the host app, ship it. Then CSP shows up and quietly wrecks the whole thing.
I’ve seen teams lose hours on this because the browser error messages are half-useful and Superset embeds have a few moving parts: iframe loading, API calls, auth endpoints, static assets, and whatever sits in front of Superset like Nginx, Apache, or a cloud proxy. One wrong directive and the embed either goes blank or fails in weird ways after login.
Here are the CSP mistakes I see most often with Superset embeds, and how I’d fix them.
Mistake 1: Keeping frame-ancestors 'none'
This is the fastest way to block your own embed.
A real-world CSP header from headertest.com includes this:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-ZDczMjIzMTctYjUxMC00YWRjLTk1NTYtNmY5Mzc3OTkyZDYy' '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 frame-ancestors 'none' is correct for a site that should never be embedded. It is completely wrong for Superset embedding.
If your Superset instance sends this header, the browser will refuse to render it inside your application iframe.
Fix
Set frame-ancestors on the Superset response to allow the parent app origins that are allowed to embed it.
Example:
Content-Security-Policy: default-src 'self'; frame-ancestors 'self' https://app.example.com https://admin.example.com; object-src 'none'; base-uri 'self';
If only one app should host the dashboard, keep it tight:
Content-Security-Policy: frame-ancestors https://app.example.com;
Don’t use * here. Don’t use broad wildcards unless you absolutely have to. frame-ancestors is your anti-clickjacking control, and for embeds it doubles as your allowlist.
Mistake 2: Editing frame-src and forgetting that it does nothing for the embedded page
This one confuses people constantly.
frame-srccontrols what your app page is allowed to embedframe-ancestorscontrols who is allowed to embed Superset
If your host app loads Superset in an iframe, then:
- the host app needs
frame-src(orchild-src) allowing the Superset origin - the Superset app needs
frame-ancestorsallowing the host app origin
You usually need both sides configured.
Fix
On the host app:
Content-Security-Policy: default-src 'self'; frame-src 'self' https://superset.example.com;
On Superset:
Content-Security-Policy: default-src 'self'; frame-ancestors https://app.example.com;
If you only change one side, you’ll still get blocked. I’ve seen people tweak Superset for an hour when the actual issue was the parent app’s CSP.
Mistake 3: Using a tiny default-src and forgetting Superset’s real asset and API needs
A lot of CSPs start life as this:
Content-Security-Policy: default-src 'self';
That’s a fine starting point. It’s not enough for most Superset deployments.
Superset usually needs:
- JavaScript bundles from its own origin or CDN
- CSS and fonts
- XHR/fetch requests back to Superset APIs
- sometimes websocket endpoints depending on your setup
- image and SVG assets
- auth/session endpoints
- maybe a reverse-proxied API origin that differs from the visible page origin
If your browser console shows violations for scripts, styles, or fetch calls, this is where to look first.
Fix
Build the policy around actual browser requests, not guesses. A practical baseline for a same-origin Superset deployment often looks like this:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self';
frame-ancestors https://app.example.com;
base-uri 'self';
form-action 'self';
object-src 'none';
If you serve Superset static assets from a separate domain, add that domain explicitly:
script-src 'self' https://static.examplecdn.com;
style-src 'self' 'unsafe-inline' https://static.examplecdn.com;
font-src 'self' https://static.examplecdn.com data:;
img-src 'self' data: https://static.examplecdn.com;
If you want ready-made patterns for common CSP setups, csp-examples.com is useful for policy snippets.
Mistake 4: Breaking embedded auth by forgetting connect-src
Superset embeds don’t just render HTML. They usually make API calls after load. If connect-src is too strict, the iframe loads but the dashboard data never appears.
This is one of the most annoying failure modes because it looks like “Superset is slow” or “the chart API is broken” when it’s really CSP.
Fix
Allow the actual API origin in connect-src.
Same-origin setup:
connect-src 'self';
Split frontend and API origins:
connect-src 'self' https://api-superset.example.com;
If your deployment uses websockets anywhere in the stack, include wss: or the explicit websocket endpoint:
connect-src 'self' https://api-superset.example.com wss://api-superset.example.com;
The headertest.com example includes a broader connect-src with analytics and websocket endpoints:
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;
That pattern is a good reminder: connect-src is often much busier than people expect.
Mistake 5: Copy-pasting strict-dynamic or a nonce policy from another app
I like nonce-based CSP. I use it when the app architecture supports it. But I also see people cargo-cult a policy like this:
script-src 'self' 'nonce-random123' 'strict-dynamic';
That’s fine for an app generating fresh nonces correctly on every response. It’s not fine if your reverse proxy injects a static header or your templates don’t attach matching nonces to inline scripts.
Superset may work badly or fail outright if the policy and rendered markup don’t line up.
Fix
Be honest about what your app can support.
If you are not dynamically injecting matching nonces into every allowed inline script, don’t fake it. Start with a plain allowlist:
script-src 'self';
If you do need inline bootstrap code and can’t remove it yet, this may be the temporary compromise:
script-src 'self' 'unsafe-inline';
I don’t love 'unsafe-inline', but I’d rather see a temporary, understood compromise than a broken nonce setup that gives people a false sense of security.
If your stack supports proper nonces, implement them consistently at the application layer rather than hardcoding them in Apache or Nginx.
Mistake 6: Forgetting the host page’s own CSP and sandbox attributes
Sometimes Superset’s CSP is fine. The parent page is the problem.
I’ve seen embeds fail because the iframe tag had an over-restrictive sandbox attribute:
<iframe
src="https://superset.example.com/embedded/abc123"
sandbox="allow-scripts">
</iframe>
That often blocks forms, same-origin behavior, or other capabilities the embed needs.
Fix
Be careful with sandboxing. If you use it, allow only what Superset actually needs, but don’t starve it.
Example:
<iframe
src="https://superset.example.com/embedded/abc123"
sandbox="allow-scripts allow-same-origin allow-forms"
referrerpolicy="strict-origin-when-cross-origin">
</iframe>
Also check the parent page CSP for frame-src:
Content-Security-Policy: default-src 'self'; frame-src https://superset.example.com;
If your app uses child-src, remember older and newer browser behavior can differ. I usually set frame-src explicitly for clarity.
Mistake 7: Letting proxies overwrite each other’s CSP headers
This one is classic infrastructure pain.
Superset might send one CSP. Nginx adds another. Apache injects a third. Your CDN normalizes headers. Then the browser gets either multiple policies or the wrong one entirely.
The result is messy and hard to debug.
Fix
Choose where CSP is owned.
If you manage it at the reverse proxy, disable app-level CSP injection unless you need per-response logic. If you manage it in Superset or Flask middleware, don’t also stamp a generic CSP in front of it.
Check the final response in browser devtools or with curl.
Example Apache config:
Header always set Content-Security-Policy "default-src 'self'; frame-ancestors https://app.example.com; connect-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self';"
If you need different policies for embedded routes versus the rest of Superset, split by path instead of forcing one policy everywhere.
Mistake 8: Testing only the login page, not the dashboard lifecycle
A Superset embed can pass the “page loaded” test and still be broken.
You need to validate:
- initial iframe navigation
- auth/session establishment
- dashboard API requests
- chart rendering
- export or form actions if enabled
- any cross-origin assets
Fix
Use browser devtools and watch for CSP violations during the whole render flow. I usually keep the Network and Console tabs open side by side and reload from a clean session.
Start in report-only mode if you’re tightening an existing deployment:
Content-Security-Policy-Report-Only: default-src 'self'; frame-ancestors https://app.example.com;
That gives you a safe way to see what would break before enforcing it.
A CSP baseline I’d actually start with
For many Superset embed deployments, I’d begin with something like this on the Superset origin:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self';
frame-ancestors https://app.example.com;
base-uri 'self';
form-action 'self';
object-src 'none';
And on the host app:
Content-Security-Policy:
default-src 'self';
frame-src https://superset.example.com;
Then I’d expand only when the browser shows a legitimate need.
That’s really the whole game with CSP for Superset embeds: know which side owns which directive, keep frame-ancestors accurate, and don’t guess at connect-src. Most breakage comes from those three.