Sandbox host runtime for plugin iframes #73

Closed
opened 2026-07-04 14:52:38 +02:00 by fable-5 · 1 comment
Collaborator

Context

Code plugins execute in opaque-origin iframes with a capability-filtered RPC bridge — the security core of the plugin system (ADR 0008).

Scope

Implement the host runtime in apps/web: iframe factory (sandbox="allow-scripts", no allow-same-origin, per-frame CSP via srcdoc/headers per plugin-architecture.md), RPC router binding SDK protocol to host services with capability filtering against the manifest (undeclared capability → rejected + logged), per-request timeouts and frame health (hung plugin → error placeholder, never a frozen app), resize protocol, and teardown on navigation.

Acceptance criteria

  • a test plugin calling an undeclared capability is rejected (security test)
  • the iframe cannot reach cookies/localStorage/parent DOM (assertion test with a malicious fixture plugin)
  • a plugin that never responds renders the timeout placeholder within 5 s and the page stays responsive
  • CSP blocks plugin network access (fixture plugin fetch attempt fails, verified)

Technical notes

  • ADR 0008, plugin-architecture.md §Sandbox runtime, security.md.
  • The malicious fixture plugin built here becomes a permanent security regression asset.

Dependencies

Depends on #70.

Size: ~2 days


Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add de and en), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.

## Context Code plugins execute in opaque-origin iframes with a capability-filtered RPC bridge — the security core of the plugin system (ADR 0008). ## Scope Implement the host runtime in `apps/web`: iframe factory (`sandbox="allow-scripts"`, no `allow-same-origin`, per-frame CSP via srcdoc/headers per plugin-architecture.md), RPC router binding SDK protocol to host services with capability filtering against the manifest (undeclared capability → rejected + logged), per-request timeouts and frame health (hung plugin → error placeholder, never a frozen app), resize protocol, and teardown on navigation. ## Acceptance criteria - [ ] a test plugin calling an undeclared capability is rejected (security test) - [ ] the iframe cannot reach cookies/localStorage/parent DOM (assertion test with a malicious fixture plugin) - [ ] a plugin that never responds renders the timeout placeholder within 5 s and the page stays responsive - [ ] CSP blocks plugin network access (fixture plugin fetch attempt fails, verified) ## Technical notes - ADR 0008, plugin-architecture.md §Sandbox runtime, security.md. - The malicious fixture plugin built here becomes a permanent security regression asset. ## Dependencies Depends on #70. **Size**: ~2 days --- *Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add `de` **and** `en`), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.*
fable-5 added this to the M7 — Plugins milestone 2026-07-04 14:52:38 +02:00
fable-5 added the
frontend
plugins
labels 2026-07-04 14:52:38 +02:00
Author
Collaborator

Done in 0875e2a. Pipeline fully green (all 8 contexts incl. the new plugin sandbox e2e pack running against the Test stage).

Sandbox host runtime (ADR 0008 security core):

  • Code plugins render in opaque-origin iframes (sandbox="allow-scripts", never allow-same-origin) via a new per-plugin frame document served with a CSP that pins every load to the plugin's own asset path and sets connect-src 'none' (no network). The CSP origin comes from APP_BASE_URL, not the request Host, so a Host-rewriting proxy can't break it.
  • Host bridge over a source-filtered postMessage transport; capability calls are gated against the manifest and violations are logged. Render runs under a 5 s deadline — a hung or failed plugin collapses to a placeholder, never freezing the page.

Acceptance criteria (all met, security pack e2e/plugins.spec.ts):

  • undeclared capability rejected (capability_not_permitted)
  • iframe cannot reach cookies/localStorage/parent DOM (malicious fixture plugin — permanent regression asset)
  • non-responding plugin shows the timeout placeholder within 5 s; page stays responsive
  • CSP blocks plugin network access (same-origin and external fetch both fail)

Note: the first parallel CI run flaked on the fidelity gate (cold action-cache extraction race at runner capacity 4, unrelated to this change); the job passed on re-run with a warm cache.

Done in `0875e2a`. Pipeline fully green (all 8 contexts incl. the new plugin sandbox e2e pack running against the Test stage). **Sandbox host runtime** (ADR 0008 security core): - Code plugins render in opaque-origin iframes (`sandbox="allow-scripts"`, never `allow-same-origin`) via a new per-plugin frame document served with a CSP that pins every load to the plugin's own asset path and sets `connect-src 'none'` (no network). The CSP origin comes from `APP_BASE_URL`, not the request `Host`, so a Host-rewriting proxy can't break it. - Host bridge over a source-filtered postMessage transport; capability calls are gated against the manifest and violations are logged. Render runs under a 5 s deadline — a hung or failed plugin collapses to a placeholder, never freezing the page. **Acceptance criteria** (all met, security pack `e2e/plugins.spec.ts`): - [x] undeclared capability rejected (`capability_not_permitted`) - [x] iframe cannot reach cookies/localStorage/parent DOM (malicious fixture plugin — permanent regression asset) - [x] non-responding plugin shows the timeout placeholder within 5 s; page stays responsive - [x] CSP blocks plugin network access (same-origin and external `fetch` both fail) Note: the first parallel CI run flaked on the fidelity gate (cold action-cache extraction race at runner capacity 4, unrelated to this change); the job passed on re-run with a warm cache.
Sign in to join this conversation.
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stwaidele/dorfteich#73
No description provided.