Security Model§

This is the canonical statement of what hyperfrontend defends against, what it does not, and which party is responsible for each control. Every other security claim in these docs is a detail of what follows.

The trust model§

A host embeds a feature the way it takes on any dependency: deliberately. Installing a shell is an act of trust, and the design does not pretend otherwise — a feature you chose is not treated as an attacker.

What the design does assume is that a trusted feature can have a bad day: a compromised transitive dependency, an injected third-party script, a bug that navigates the top-level page. The controls below limit the blast radius of those events without treating the feature team as hostile.

In the model

  • • Other scripts co-resident in either page — analytics snippets, tag managers, chat widgets, compromised dependencies
  • • Unknown pages that embed a publicly reachable feature URL
  • • Stale traffic from a document that was replaced by a reload or navigation
  • • Contract drift between two independently deployed vintages

Outside the model

  • • A host application that is itself malicious toward its own feature
  • • A fully compromised page, where the attacker is the application
  • • Network attackers below TLS
  • • The browser, the platform, and their vulnerabilities

The load-bearing limitation. Origin checks authenticate rooms, not speakers. Every script running inside a page shares its window, and a message it posts reports that page's real origin — because that is where it runs. No additional message check distinguishes such a script from the application itself. Keeping authority away from a page's shared JavaScript realm is a different job, and it belongs to Content Security Policy, Trusted Types, dependency provenance, and server-side authorisation.

Who enforces what§

Three parties carry the security of an embedded feature. Confusing them is the most common way an integration ends up believing it is protected by something that was never switched on.

The browser enforces isolation§

The same-origin policy keeps the feature's DOM, JavaScript state, and storage away from the host and the host's away from the feature. This is not a convention that holds until someone is in a hurry — the browser refuses, on every page load. It is the reason the boundary is an iframe and not a shared runtime. The browser also enforces the two capability attributes the host writes (sandbox and allow) and the frame-ancestors directive the feature's server sends.

The protocol enforces the relationship§

Isolation says nothing about who is on the other side of the wall or whether what they said is well-formed. That is the protocol's job, and it is shipped behaviour rather than advice:

  • Pinned counterparts. Each side learns the other's origin during the handshake and pins it. Inbound routing resolves by the sending window rather than by claimed identity, and an origin mismatch is dropped.
  • Instance identity. Every boot mints an id that is stamped on each action, so traffic left over from a document that has been replaced cannot enter the session that replaced it.
  • A gated handshake. Nothing opens without REQUEST → ACCEPT → OPEN. Five gates can refuse — invalid contract, missing required actions, security policy, contract incompatibility, and unavailable security under a fail-closed channel — and each fires a machine-readable denial on the side that decided.
  • Validation on both ends. A send is validated against the sender's own emitted schemas and throws in the sender's frame; an arriving message is validated against the receiver's own accepted schemas and is dropped with a diagnosable error. Malformed data never detonates inside the other application.
  • A versioned contract. Each side presents the contract vintage it bundled, and an incompatible pair is refused before product messages move — instead of going silently deaf weeks later.
  • An optional encrypted envelope. Product traffic can travel inside a negotiated envelope; handshake frames stay plaintext. See the status table for what each version actually buys.

The operator decides authorisation§

Pinning answers am I still talking to the same counterpart I started with? It does not answer was that counterpart ever allowed to embed me? Those are different jobs, and the second one lives outside the protocol entirely — at the framing and server boundaries you control:

http
Content-Security-Policy: frame-ancestors https://app.mysite.com
  • Who may embed the feature — the frame-ancestors directive on the response that serves the feature document.
  • What an authenticated caller may do — credentials and authorisation checks on the feature's own backend. A message that crossed the boundary is not an authorised operation.
  • Whether traffic is encrypted at all — choosing a protocol version and, for v2, provisioning and rotating the pre-shared key. The SDK never bakes a key into an artifact.
  • The containment posture — the sandbox tokens the frame runs under, priced against what the product actually needs.
  • The page's own integrity — HTTPS, Content Security Policy, Trusted Types, Subresource Integrity, and dependency provenance on both sides.

Capability: delegation and containment§

Capability flows one way — from the containing page into the frame. A feature cannot grant itself browser powers. The SDK splits that flow along two axes, and they answer different questions.

Delegation — permissions

What powerful features may this frame use? Feature-declared, host-applied. The feature lists the Permissions-Policy features it needs beside its contract; the build bakes them as the shell's defaults, discloses them in the generated README, and stamps them into metadata.json for review. The host applies them as the frame's allow attribute and may replace the list outright. Reviewable before install, and cheap to keep narrow.

Containment — sandbox

How large is the blast radius if this frame misbehaves? Host-only, never baked. The SDK manages the two hazardous tokens itself: allow-scripts is always granted, and allow-same-origin only to cross-origin feature URLs — the pairing that would let a same-origin frame shed its own sandbox is not expressible. Everything else is explicit opt-in.

Omit permissions and the frame gets exactly what the feature declared at build time. Passing the key replaces that grant list rather than extending it.

typescript
shell.open({
  permissions: ['fullscreen', 'clipboard-write'], // delegation — replaces the list baked in at build time
  sandbox: { downloads: true },                   // containment — the host's call alone
})

Both attributes are written before the frame loads, which is the only moment they take effect, and both apply to the iframe modes only — popup and standalone open top-level windows that ask the user for permissions directly. Requesting a sandbox on those modes throws.

Status of each control§

What is on by default, what you have to switch on, and what a control is honestly worth.

ControlStatusWhat it is worth
Document isolationBrowser-enforced, alwaysThe guarantee the whole design is built on. Not optional, not bypassable by either side.
Origin pinning and window bindingOn by defaultBinds a session to one counterpart. Authenticates the room, not the speaker.
Instance identityOn by defaultKeeps stale traffic out of a fresh session. Cooperative in plaintext — forgeable by anything that can post to the window — and authenticated inside a v2 envelope.
Handshake gates and reasoned denialOn by defaultRefuses a session before anything opens, and tells the deciding side why. A policy rejection deliberately discloses nothing to the refused party.
Schema validation, both endsOn for actions that carry a schemaStops malformed payloads at the border. Actions without a schema pass through unchecked.
Contract version gateActive when both sides carry a versionMakes deployment drift fail loudly. A side without a version always passes.
Envelope v1Opt-inTime-window obfuscation with a clock-derived password — no secret is involved, so anything that can read the frames can reverse it. Deterrence against casual inspection, not a confidentiality control.
Envelope v2Opt-in, requires a pre-shared keyThe real control: authenticated encryption of product traffic end to end. Handshake frames stay plaintext.
Fail-closed securityOpt-in, on a nexus channelTurns a failed negotiation into a denial instead of a plaintext fallback. Negotiation fails open by default, and the shell surface does not expose the switch — set it when driving the broker directly.
permissions delegationFeature-declared, host-appliedKeeps powerful browser features scoped to the frames that need them, and reviewable before install.
sandbox containmentHost-only, off unless requestedBounds the blast radius of a misbehaving frame. Correct by construction — the self-defeating pairing cannot be expressed.
frame-ancestorsOperator-suppliedThe only thing that decides who may embed the feature at all. The SDK cannot do this for you.
Backend authorisationOperator-suppliedDecides what an authenticated caller may actually do. A delivered message is not an authorised operation.

Going deeper§

Each control has an implementation, and each implementation has its own guarantees and limitations.

Found a vulnerability? Report it privately — see the security policy for the disclosure process and response timeline.