---
title: Web Component Security Model
description: The sandbox boundary for third-party web components — what a component can and cannot access, and how bundles stay immutable
date: 2026-07-22
order: 3
tags:
  - web-components
  - security
  - sandbox
---

Third-party code never runs with platform trust. Every web component executes inside
a layered sandbox, and the only way data gets **in** is a validated message bridge
carrying data the user is already permitted to see. Outbound, the sandbox blocks all
network APIs — with one honest limit on navigation, [described below](#the-honest-limit).

## The sandbox boundary

Components run in an `<iframe sandbox="allow-scripts">` **without**
`allow-same-origin`, which gives the document an *opaque origin*. On top of that,
the sandbox page ships a strict Content Security Policy with **no `connect-src`**.

| A component **can** | A component **cannot** |
| --- | --- |
| Render freely inside its shadow DOM | Read platform cookies, storage, or the parent page's DOM |
| Receive granted data over the bridge (`init`/`data` messages) | Make network requests — `fetch`, XHR, and WebSockets are blocked by CSP |
| Receive the platform theme and follow light/dark live | Load scripts or assets from external URLs |
| Report its height (`resize`) and errors over the bridge | Obtain tokens, sessions, or an API client — none exist in the sandbox |

Every bridge message carries a per-instance random nonce and is validated on both
sides; messages from unrelated frames are ignored.

## Immutable, pinned bundles

- Bundles are uploaded to platform storage — **no arbitrary external URLs**, so the
  main application's CSP stays tight.
- Each upload creates a **new immutable version**; nothing is ever overwritten.
- The bundle's **SHA-256 is pinned** at registration and re-verified on every
  serve — if stored bytes ever differed from the pin, the platform refuses to serve
  a single byte.
- Uploads are capped at 10 MB and must be a single ESM file defining the
  registered custom element.

## Why there are no tokens in v1

Earlier integration approaches injected third-party scripts into the main page with
access to a token service and API client — full trust. This system replaces that
with a real boundary: the platform resolves permissions **server-side** from the
registration's declared scopes and pushes only the resulting data. Since the
component never authenticates, there is nothing to steal, replay, or escalate —
compromising a component's code yields at most the data its consented scopes
already forward.

## The honest limit

Data that *is* granted is, by definition, handed to third-party code — and once
handed over, its confidentiality cannot be technically guaranteed. In particular,
CSP blocks every network API but cannot stop a component from navigating its own
frame to an external URL that embeds data it already received over the bridge. The
sandbox strictly limits *what* a component can ever obtain (only the data its
consented scopes forward — never tokens, sessions, or other users' data), not what
a malicious component might do with that data afterwards.

That is exactly why the [consent overlay](./consent.md) discloses every declared
scope before the first run, and why a **disabled** registration acts as an
immediate kill switch — placed widgets render a neutral placeholder instead of
loading any code.

## Related

- [Permissions](./permissions.md) — the declared-scope vocabulary
- [Consent behavior](./consent.md) — end-user disclosure
- [Building Web Components](/web-components) — the authoring guide
