Skip to main content
The FERAL host renders publisher-supplied UI. That code is not trusted. The host therefore drops every surface into a <iframe sandbox> with a deliberately restrictive Content-Security-Policy and a strict schema for any message the surface sends back. This page documents the flags, the CSP derivation rules, and the postMessage envelope so that publishers know what their code may and may not do.

Iframe attributes

<iframe
  sandbox="allow-scripts"
  referrerPolicy="no-referrer"
  srcDoc={buildSrcDoc({ tree, manifest, signedWithKeyId })}
/>
Notable choices:
  • sandbox="allow-scripts" — and explicitly not allow-same-origin. The iframe runs as an opaque origin, so window.parent.document is unreachable. Cookies, localStorage, IndexedDB, and fetch to the host’s origin are all denied by the browser, even before the CSP fires.
  • referrerpolicy="no-referrer" — outbound requests the surface does make leak no URL identifiers from the host.
  • srcDoc=... — we never load a third-party HTML URL. The host renders the SDUI tree to static HTML inside the worktree and ships it as inline document content. No src= ever points at publisher origins.
The srcDoc carries the rendered tree plus a tiny bootstrap that:
  1. Adds a click listener that walks ancestors looking for a [data-action-id], then postMessages a strict envelope to the host. No inline onclick= handlers ever exist in the document.
  2. Adds a submit listener for [data-form] nodes that posts the serialized form values up.
  3. Has zero eval, zero new Function, and zero dynamic <script src>.

CSP derivation

The CSP is generated by feral_core.genui.permissions_policy.build_csp_header (Python) and mirrored at feral-client-v2/src/pages/AppSurface.csp.js (JS). Both read manifest.permissions.network and produce:
default-src 'none';
script-src 'unsafe-inline';      /* needed for the inline bootstrap */
style-src 'unsafe-inline';        /* SDUI styles are inline */
img-src 'self' data: https:;
media-src 'self' data:;
font-src 'self' data:;
connect-src <derived from manifest.permissions.network or 'none'>;
frame-ancestors 'self';
base-uri 'none';
form-action 'none';
Notes:
  • connect-src defaults to 'none'. Publishers must list every origin they want to talk to. Wildcard * is accepted only under the high-trust flow described in Manifest signing.
  • frame-ancestors 'self' keeps the surface from being embedded in arbitrary attacker pages.
  • default-src 'none' means anything not explicitly granted above is blocked.
If a publisher needs WebSocket connectivity, list wss://... URIs in permissions.network. The CSP helper coerces bare hostnames into https://... to keep the policy strict-by-default.

postMessage envelope

The only structured channel between the surface and the host is window.postMessage. The host validates every event with feral_core.genui.app_message_schema.AppMessage (Python) and the matching validateAppMessage from feral-client-v2/src/pages/AppSurface.types.ts. Anything that doesn’t match is dropped silently — no exception, no audit noise. Schema:
type AppMessage = {
  type: "request_data" | "submit_form" | "navigate" | "close";
  payload: Record<string, unknown>;
  message_id: string;
  signed_with_key_id: string;
};
Constraints:
  • type must be one of the four enum values. New values require a schema bump on both sides.
  • payload is a JSON object whose serialized size must be ≤ 64 KB.
  • signed_with_key_id echoes the key id of the manifest being rendered, which lets the host correlate misbehaviour with a specific publisher key.
  • The Pydantic schema is extra="forbid"; smuggling extra fields into the envelope is rejected.
Host-side dispatch:
typeBehaviour
submit_formRouted to sendUiEvent as { action_id, value } — the standard SDUI action path.
navigateCalls openSurface(payload.surface_id) if it’s a string the manifest declares.
request_dataReserved; currently no-op so that future server-driven prefetch doesn’t break apps.
closeReserved; currently no-op.

What this rules out

  • Publishers cannot read or write the host’s cookies or localStorage.
  • Publishers cannot reach the host’s /api/... endpoints unless they list them in permissions.network (and even then they must talk to them through their own backend; the iframe runs as an opaque origin so it has no session cookie).
  • Publishers cannot navigate the host frame; frame-ancestors 'self'
    • sandbox without allow-top-navigation blocks top.location writes.
  • Publishers cannot invoke eval or load <script src=> content; CSP script-src 'unsafe-inline' allows only the inline bootstrap the host generates.
  • feral-core/genui/manifest_signing.py — Ed25519 signing primitives.
  • feral-core/genui/permissions_policy.pyenforce_install_policy
    • build_csp_header.
  • feral-core/genui/app_message_schema.pyAppMessage Pydantic schema.
  • feral-client-v2/src/pages/AppSurface.jsx — the iframe host.
  • feral-client-v2/src/pages/AppSurface.csp.js — JS mirror of the CSP builder.
  • feral-client-v2/src/pages/AppSurface.types.ts — TS mirror of AppMessage.