Security
An iframe is a trust boundary. react-iframe-kit checks origins on both sides, but the browser settings around the iframe matter just as much.
Origins
Section titled “Origins”Parent. useIframeRPC, useIframeEvent and useIframeResize accept the child only
from its expected origin: the origin option, or else the origin of the iframe’s current
src. If the iframe navigates or redirects elsewhere, the handshake fails closed. Pass
origin explicitly when the src may redirect.
Child. allowedOrigins is always required: the child can’t reliably learn who
embedded it (Firefox has no location.ancestorOrigins).
connectToParent({ allowedOrigins: ['https://app.example.com', /^https:\/\/[a-z0-9-]+\.preview\.example\.com$/],});- Prefer exact origin strings. A
RegExpmust be anchored with^…$; a dev warning says when it isn’t. Thegandyflags are rejected, because they maketest()stateful. - A function
(origin) => booleancovers lists that change at runtime, see Multi-tenant embeds. '*'is rejected unless you also setunsafeAllowAnyOrigin: true. On the child, that lets any page embed yours and call your methods.
Treat everything that crosses the boundary as untrusted input: validate method arguments the way you would validate an HTTP request body (see Validating what arrives).
Multi-tenant embeds
Section titled “Multi-tenant embeds”A white-label widget is embedded by many customers, and the list changes without a release. Pass a predicate: it’s asked on every handshake, so the list can change while the page is open.
const tenants = new Set(await fetchTenantOrigins()); // e.g. from your config API
connectToParent({ allowedOrigins: [(origin) => tenants.has(origin)], methods,});- A predicate allows an origin only when it returns exactly
true. It must be synchronous: anasyncpredicate returns aPromise, which allows nothing. Load the list first, then connect. - Two
connectToParentcalls on one page must pass the same function (or equal strings andRegExps), otherwise the second one getsRIK_ORIGIN_CONFLICT. - Match whole origins against a list. Avoid “any subdomain of” rules for domains where
other people can host content: in 2024, Microsoft fixed a critical bug
(CVE-2024-49038)
in which a trusted-origin check accepted
*.sharepoint.com, where anyone can publish a page.
allowedOrigins decides who may talk to your page. Who may frame it is decided by the
frame-ancestors header, which the server has to compute per tenant, for example from
the tenant ID in the embed URL:
Content-Security-Policy: frame-ancestors https://tickets.customer-a.com https://customer-a.comOnly the header stops other sites from framing the page at all, which is what protects a
checkout against clickjacking. Don’t derive either list from document.referrer or
location.ancestorOrigins: the host controls the first with referrerpolicy, and
Firefox doesn’t have the second.
Choosing sandbox flags
Section titled “Choosing sandbox flags”| What’s in the iframe | Where it’s served from | Recommended |
|---|---|---|
Your own UI, rendered with <Frame> |
same origin (srcdoc) |
Leave sandbox off; it can’t protect you here (see below). Use CSP instead. |
| Your own app or widget | a different origin you control | sandbox="allow-scripts allow-same-origin", plus only the flags it needs (allow-forms, allow-popups, …) |
| Untrusted or user-generated content | a separate origin used only for that content | sandbox="allow-scripts", and origin: 'null' on the parent |
For pages you don’t control (videos, maps, partner apps, payment forms), see the table in Third-party iframes.
Opaque origins
Section titled “Opaque origins”Without allow-same-origin, the child runs in an opaque origin and every message it sends
comes from 'null'. react-iframe-kit never assumes that: the parent must opt in with
origin: 'null', and a child whose parent is opaque must list 'null' in
allowedOrigins.
With an opaque origin, only the iframe’s window object identifies the child. If its document were replaced in the middle of the handshake, the new document could receive the connection. So don’t expose privileged methods to an opaque-origin child: give it only what any page on the internet could safely call.
Portal rendering (<Frame>, useIframe) needs allow-same-origin and reports
RIK_INVALID_OPTIONS without it.
Content Security Policy
Section titled “Content Security Policy”On the host page: limit which origins may be framed.
Content-Security-Policy: frame-src https://widget.example.comOn the embedded page: limit who may frame it. This is the header-level counterpart of
allowedOrigins, and it also stops clickjacking.
Content-Security-Policy: frame-ancestors https://app.example.comX-Frame-Options is the legacy version of frame-ancestors; when both are set, browsers
use frame-ancestors.
<Frame> under a strict CSP
Section titled “<Frame> under a strict CSP”A <Frame> document is a srcdoc document, and it inherits the host page’s CSP.
- With
style-src 'nonce-…', give every<style>you render throughheadthe nonce:<style nonce={nonce}>. copyStylesclones the parent’s style elements with their nonce, so mirrored styles keep working under a nonce-based policy.
Trusted Types
Section titled “Trusted Types”With require-trusted-types-for 'script', the browser only accepts a TrustedHTML for an
iframe’s srcdoc. <Frame> and useIframe set it through their own policy, named
react-iframe-kit, which can create exactly one document: the library’s empty default one.
It never passes your HTML through, so allowing the name is safe.
If your policy lists allowed names, add it:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types react-iframe-kitA custom srcDoc isn’t passed through that policy. Create it with one of your own:
const preview = trustedTypes.createPolicy('preview', { createHTML: sanitize });
<Frame srcDoc={preview.createHTML(html)} />If the page blocks the document anyway, useIframe reports RIK_INVALID_OPTIONS in
error with a message that names the fix, and <Frame> renders nothing into the iframe.
The rest of your app keeps running.
Checklist for an embeddable page
Section titled “Checklist for an embeddable page”For a page meant to run inside other sites’ iframes, such as a widget or checkout:
frame-ancestorslists the sites that may embed it, and matchesallowedOrigins.- Cookies it needs while framed are
SameSite=None; Secure. With third-party cookies blocked, addPartitioned(CHIPS) so the browser keeps a separate cookie jar per embedding site; plain third-party cookies may not be sent at all. - Nothing sensitive in
document.titleif the page usessyncTitle: the embedding site receives it.
On the host page:
sandboxwith only the flags the embed needs (see above).allowonly for the features it needs (allow="payment",allow="clipboard-write", …). Powerful features such as the camera are off in cross-origin iframes unless listed.referrerpolicy="strict-origin-when-cross-origin"or stricter, so the full URL of your page doesn’t leak to the embed.maxHeightonuseIframeResizefor anything you don’t control.
Other defaults
Section titled “Other defaults”- Messages are validated field by field, and malformed ones are dropped.
- Only a method that is an own property holding a function can be called, so
constructor,toStringand__proto__answerRIK_METHOD_NOT_FOUND. - Error stacks cross the boundary only with
debug: true, since they leak file paths. Don’t enabledebugin production. - A cross-origin child controls its own reported size: set
maxHeightfor untrusted content.