Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions docs/widget-csp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Widget CSP Guidance

## Purpose

OpenClaw Studio's inline-widget feature renders agent-generated HTML
inside a sandboxed `<iframe srcdoc>`. The iframe loads two CDN-hosted
resources at runtime — Tailwind CSS v4 and Chart.js — and runs
inline `<script>` tags inside the srcDoc.

If Studio ever adds a `Content-Security-Policy` header (today it does
not), the policy MUST permit these resources. This doc spells out the
minimum directives so future CSP work does not silently break widgets.

> **Scope.** This is *widget-specific* CSP guidance — directives required
> if Studio adds CSP and wants the inline-widget feature to keep working.
> The permissive tokens below (`'unsafe-inline'`, `img-src *`) apply
> inside the iframe `srcdoc` context, where the iframe
> `sandbox="allow-scripts allow-popups"` attribute is the trust boundary.
> They are NOT recommended for any non-widget surface of Studio. For
> nonce/hash-based alternatives, see "Stricter alternatives" at the end.

## When this applies

- Studio currently ships **no CSP header**. The widget feature works
by default. This doc is a forward-looking guide for whoever adds CSP
later.
- The widget iframe runs with `sandbox="allow-scripts allow-popups"`
and no `allow-same-origin`. The sandbox is the widget's trust
boundary; CSP is an *additional* layer that must be configured
cooperatively with the sandbox, not as a replacement for it.

## Required directives

### `script-src 'unsafe-inline' https://cdn.jsdelivr.net`

Widgets execute inline `<script>` tags inside their srcDoc. Browsers
treat `about:srcdoc` document CSP inheritance unevenly — Chrome and
Firefox have historically differed on which directives flow into the
srcDoc context. To stay compatible, the parent page's `script-src`
must allow inline scripts AND the jsdelivr CDN that hosts the widget
runtime libraries.

- `'unsafe-inline'` — required for `<script>` blocks in widget HTML.
The sandbox prevents these scripts from reaching parent-origin
resources, so `'unsafe-inline'` here does **not** weaken Studio's
own attack surface.
- `https://cdn.jsdelivr.net` — origin for the pinned Tailwind v4
browser CDN (`@tailwindcss/browser@4`) and Chart.js
(`chart.js@4.5.1/dist/chart.umd.min.js`). Both are loaded inside
the iframe at widget-mount time.

### `frame-src 'self'`

Studio embeds widgets via `<iframe srcdoc>`, which navigates to the
synthetic URL `about:srcdoc`. Browsers consistently allow `srcdoc`
iframes when `frame-src` permits the embedding origin (`'self'`).
Adding `'self'` is sufficient — do **not** add `data:` or `about:`
schemes; they don't apply to `srcdoc` and add unrelated attack
surface.

### `img-src *`

Widget HTML may reference user-supplied or agent-generated image URLs
from arbitrary origins (chart screenshots, status icons, external
assets). Restricting `img-src` would block legitimate widget content.
Images load from the iframe context only, where the sandbox prevents
them from reading parent storage; allowing `*` here is consistent with
the sandbox boundary.
Comment on lines +34 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. widget-csp.md recommends unsafe-inline 📘 Rule violation ⛨ Security

The new CSP guidance instructs adding permissive directives like script-src 'unsafe-inline' and
img-src *, which is unsafe and not appropriate as generic open-source repository guidance because
it encourages weakening CSP protections broadly.
Agent Prompt
## Issue description
`docs/widget-csp.md` currently presents permissive CSP directives (e.g., `script-src 'unsafe-inline'` and `img-src *`) as required guidance. This is risky and not suitable as generic open-source documentation because it encourages broadly weakening CSP on the parent page.

## Issue Context
The compliance checklist requires repository instructions/documentation to remain generic and safe for open source audiences. CSP guidance should avoid prescribing insecure defaults; if exceptions are necessary, they should be framed as last-resort with safer alternatives (nonces/hashes, tighter allowlists, widget-only isolation) and clear security warnings.

## Fix Focus Areas
- docs/widget-csp.md[26-60]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


## Optional / situational

### `connect-src`

If a widget uses `fetch()` to a third-party API (e.g., a real-time
quotes endpoint), the parent CSP must permit that origin. The default
Studio widgets ship today do not perform fetch — they visualize
agent-supplied data inline. Adding allowlisted `connect-src` origins
is opt-in per deployment.

### `style-src`

Tailwind v4's browser runtime injects styles into the iframe document
via inline `<style>` tags. If a future CSP restricts `style-src`,
include `'unsafe-inline'` in the same way as `script-src`. Most
deployments do not need to tighten `style-src`.

## What NOT to do

- **Do not add `'unsafe-eval'`** — Tailwind v4 browser runtime no
longer requires it. Adding it broadens the parent-page attack
surface for marginal widget benefit. If you observe CSP errors
blocking `eval`, audit which library version is loaded; pin to the
documented Tailwind v4 browser build at
`cdn.jsdelivr.net/npm/@tailwindcss/browser@4`.
- **Do not relax the iframe sandbox** to compensate for a strict CSP.
The sandbox is the widget trust boundary; loosening it (e.g.,
adding `allow-same-origin`) opens documented sandbox-escape paths
via `iframe.removeAttribute('sandbox')` followed by self-reload.
- **Do not allowlist `cdn.tailwindcss.com`** (the v3 Play CDN). The
widget code path uses `cdn.jsdelivr.net/npm/@tailwindcss/browser@4`
for Tailwind v4 semantics. Mixing the two CDNs would render widgets
with the wrong Tailwind version.

## Background

Widgets render inside a sandboxed iframe to keep agent-generated HTML
isolated from Studio's origin. The sandbox blocks DOM access, cookie
reads, and storage access; it does not block network requests to
allowed origins. CSP at the parent layer is the second line of
defence — it determines which origins the iframe can fetch from.

The sandbox token set is locked at the source level to the literal
string `allow-scripts allow-popups`. A custom ESLint rule (SEC-01,
defined in `eslint.config.mjs`) prevents introducing dangerous tokens
like `allow-same-origin`, `allow-forms`, or `allow-top-navigation`
into the sandbox attribute via `no-restricted-syntax`.

## Verification

To check that a CSP change does not break widgets, run the Playwright
e2e suite:

```bash
npm run e2e -- tests/e2e/widget-replay-parity.spec.ts
```

The spec asserts that the iframe `sandbox` attribute is the literal
`"allow-scripts allow-popups"`, that the 9 mapped Studio theme CSS
variables are injected into the srcDoc, that iframe attributes are
byte-equal between live render and replay, that forged
`window.parent` postMessage calls cannot resize widgets, and that no
new accessibility violations appear vs a baseline transcript with no
widgets.

## Stricter alternatives

Deployments that want tighter `script-src` than `'unsafe-inline'` can
use CSP nonces or hashes — but those mechanisms require widget-side
coordination:

- **Nonces** — the parent CSP issues `script-src 'nonce-<value>'`, and
every `<script>` inside widget HTML must carry
`<script nonce="<value>">`. Agents emitting widget HTML would need to
be informed of the per-request nonce, which Studio's current
architecture does not propagate.
- **Hashes** — the parent CSP issues `script-src 'sha256-<digest>'`
for each known script body. Works only for stable, build-time
scripts — agent-generated dynamic scripts cannot be hashed in
advance.

Until widget HTML can participate in nonce/hash coordination,
`'unsafe-inline'` inside the sandboxed `srcdoc` is the simplest path.
The sandbox prevents inline scripts from reaching parent-origin
resources, so the security property comes from the sandbox, not from
CSP.
30 changes: 30 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,29 @@ import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
import prettier from "eslint-config-prettier/flat";

// SEC-01: Ban the `allow-same-origin` sandbox token from any TypeScript/TSX file
// in the repo. The token combined with `allow-scripts` is the canonical iframe
// sandbox escape (WHATWG-documented). Lands here per CONTEXT.md D-24..D-27.
const SANDBOX_ESCAPE_MESSAGE =
"allow-same-origin sandbox token enables sandbox escape via document reload (WHATWG-documented). See .planning/phases/02-widget-component-security/02-CONTEXT.md D-24.";

const sec01SandboxRule = {
files: ["**/*.{ts,tsx}"],
rules: {
"no-restricted-syntax": [
"error",
{
selector: "Literal[value=/allow-same-origin/]",
message: SANDBOX_ESCAPE_MESSAGE,
},
{
selector: "TemplateElement[value.raw=/allow-same-origin/]",
message: SANDBOX_ESCAPE_MESSAGE,
},
],
},
};

const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
Expand All @@ -12,6 +35,7 @@ const eslintConfig = defineConfig([
"@typescript-eslint/no-require-imports": "off",
},
},
sec01SandboxRule,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
Expand All @@ -22,6 +46,12 @@ const eslintConfig = defineConfig([

// Vendored third-party code (kept as-is; linting it adds noise).
"src/lib/avatars/vendor/**",

// SEC-01 ESLint fixture path is intentionally ignored at default lint
// time — its file violates the rule so we have a regression test. Run
// `npm run lint -- <fixture path> --no-ignore` to confirm the rule
// fires; default lint excludes it so CI stays green.
"tests/eslint-rules/**",
]),
prettier,
]);
Expand Down
Loading