Installs into .claude/skills of the current project.
Are you the author of Embed Csp And Iframe Sandboxing?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/mcorbett51090-embed-csp-and-iframe-sandboxing)
---
name: embed-csp-and-iframe-sandboxing
description: Configure CSP `frame-ancestors`, iframe `sandbox` attributes, postMessage origin checks, and web-component shadow-DOM boundaries for embedded dashboards. Invoked by `ravenclaude-core/security-reviewer` when a diff touches embed-auth flow; generated alongside dashboard code by `dashboard-builder`.
invoked_by: ravenclaude-core/security-reviewer
---
# Skill: embed-csp-and-iframe-sandboxing
> **Invoked by:** `ravenclaude-core/security-reviewer` (mandatory for any embed-iframe / web-component change). Generated by `dashboard-builder`.
>
> **When to invoke:** designing the iframe / web-component boundary that contains an embedded dashboard. Reviewing CSP headers. Verifying postMessage origin checks. Web-component shadow-DOM boundary concerns.
>
> **Output:** correct CSP `frame-ancestors` directive; iframe `sandbox` attributes; postMessage origin allowlist; web-component boundary documented.
## The three concerns
### 1. CSP `frame-ancestors` (the host side)
The host application (e.g., `ravenpower.net` or the client's web app) needs a CSP header that **either allows** the dashboard to be embedded into specific origins, or **forbids** embedding entirely if the dashboard is not third-party-facing.
```http
Content-Security-Policy: frame-ancestors 'self' https://client-app.example.com https://*.client-tenant.example.com;
```
- `'self'` — the host's own origin
- Explicit origins — the third-party apps allowed to embed
- `'none'` — forbid embedding entirely (use for admin-only internal dashboards)
- Wildcards — allow sub-domains; use sparingly
`X-Frame-Options: SAMEORIGIN` is the legacy fallback; modern browsers honor `frame-ancestors` when both are present.
### 2. iframe `sandbox` attribute (the embed side)
When the host page renders the dashboard via iframe, the `sandbox` attribute scopes what the iframe can do:
```html
<iframe
src="https://dashboards.example.com/embed/abc123"
sandbox="allow-scripts allow-same-origin allow-forms"
...
></iframe>
```
| Sandbox flag | Effect |
|---|---|
| `allow-scripts` | iframe can execute JS (usually required for interactive dashboards) |
| `allow-same-origin` | iframe content can be treated as same-origin if the URL matches (required for cookies / localStorage scoped to the embed origin) |
| `allow-forms` | iframe can submit forms (filtering, export) |
| `allow-popups` | iframe can open new windows (PDF export, share link) |
| `allow-popups-to-escape-sandbox` | popups don't inherit the sandbox |
**Default:** start with `sandbox="allow-scripts allow-same-origin"` and add flags only when needed. **No `sandbox` attribute at all = full permissions, which is wrong.**
### 3. postMessage origin checks (the message-passing boundary)
When the iframe and host page need to communicate (resize signals, filter changes, theme updates), they use `window.postMessage()`. Both sides must verify the origin:
```javascript
// Host page receiving messages from the embed
window.addEventListener('message', (event) => {
// ALWAYS check origin — never trust event.data without this
if (event.origin !== 'https://dashboards.example.com') return;
// ALWAYS check structure of event.data before using it
if (typeof event.data !== 'object' || !event.data.type) return;
// Now safe to act
});
// Embed iframe sending messages to host
window.parent.postMessage({ type: 'resize', height: 600 }, 'https://host-app.example.com');
// ^ explicit target origin, not '*'
```
**Anti-patterns:**
- `event.origin` not checked
- `postMessage(..., '*')` — broadcasts to any listener; leaks data
- Trusting `event.data` shape without validation
## Web components (alternative to iframes)
For tools like Embeddable.com that use web components rather than iframes, the boundary becomes the **shadow-DOM boundary**:
```html
<dashboard-embed
api-key="<short-lived-token>"
tenant-id="from-server"
></dashboard-embed>
```
- Shadow DOM scopes CSS and DOM access — the host page can't reach into the dashboard's shadow root, the dashboard can't reach out
- Custom-element attributes (like `api-key`) are visible in the host's DOM — pass short-lived tokens, never long-lived secrets
- Event communication via `dispatchEvent` + `addEventListener` on the custom element
## Tool-specific patterns
### Apache Superset
- Embed SDK manages an iframe internally; the host page only needs `frame-ancestors` allowing Superset's origin and the `<superset-embedded-dashboard>` custom element
- Guest token is short-lived (configurable; default ~1 hour — recommend 5-15 min for production)
### Metabase
- Static embed: signed iframe URL; `frame-ancestors` covers it
- Interactive Embedding (Pro+): SDK + JWT flow with parameterized iframes
### Power BI Embedded
- Embed via `powerbi-client` JS library; the library handles the iframe internally
- CSP must allow `frame-ancestors` for `*.powerbi.com` (or the F-SKU embed URL); Microsoft also requires CSP nonce values for the embed page's inline scripts
### Cube (with custom React UI)
- No iframe — direct REST/SQL API calls from the React app to Cube
- CSP `connect-src` directive must allow the Cube API origin
- No `frame-ancestors` concern unless Cube itself is being embedded somewhere
### Embeddable.com
- Web-component-based; shadow-DOM boundary is the primary container
- API key passed as element attribute (must be short-lived)
## Required CSP companions
Beyond `frame-ancestors`, the host page should set:
```http
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{server-generated}';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'self' https://api.cube.example.com;
frame-src 'self' https://dashboards.example.com;
frame-ancestors 'self' https://client-app.example.com;
```
- `frame-src` (or `child-src`) — what the host is allowed to load inside iframes
- `connect-src` — what the host's JS is allowed to fetch from (Cube API, JWT-issuer endpoint)
- `script-src 'nonce-...'` — nonce-based script allowlist (more secure than `unsafe-inline`)
## Anti-patterns this skill flags
- `frame-ancestors` set to `*` (allows embedding anywhere — embed-jacking risk)
- iframe with no `sandbox` attribute (full permissions)
- `sandbox="allow-scripts allow-same-origin allow-top-navigation"` (allow-top-navigation lets the iframe redirect the parent page — phishing risk)
- `postMessage(..., '*')` instead of explicit target origin
- `event.origin` not checked in message handlers
- Host page that doesn't set CSP headers at all (the security review skips entirely)
- Long-lived API keys passed as web-component attributes (visible in host DOM; short-lived tokens only)
- CSP with `unsafe-eval` enabled because "Cube needs it" — Cube doesn't; verify the requirement
- Power BI Embedded engagement that forgets the CSP nonce values Microsoft requires
- Web-component embed with no shadow-DOM (defeats the boundary)
## References
- Knowledge: [`../../knowledge/embedded-analytics-landscape-2026.md`](../../knowledge/embedded-analytics-landscape-2026.md) — tool-specific embed patterns
- Skill: [`../jwt-embed-issuance/SKILL.md`](../jwt-embed-issuance/SKILL.md) — the JWT that travels through the boundary
- Skill: [`../rls-policy-authoring/SKILL.md`](../rls-policy-authoring/SKILL.md) — what the JWT claim ultimately enforces
- MDN: [CSP `frame-ancestors`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors)
- MDN: [iframe `sandbox`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox)
- MDN: [`window.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
- Upstream review rubric: [`../../../ravenclaude-core/agents/security-reviewer.md`](../../../ravenclaude-core/agents/security-reviewer.md)