Skip to content

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.

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 RegExp must be anchored with ^…$; a dev warning says when it isn’t. The g and y flags are rejected, because they make test() stateful.
  • A function (origin) => boolean covers lists that change at runtime, see Multi-tenant embeds.
  • '*' is rejected unless you also set unsafeAllowAnyOrigin: 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).

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: an async predicate returns a Promise, which allows nothing. Load the list first, then connect.
  • Two connectToParent calls on one page must pass the same function (or equal strings and RegExps), otherwise the second one gets RIK_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.com

Only 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.

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.

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.

On the host page: limit which origins may be framed.

Content-Security-Policy: frame-src https://widget.example.com

On 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.com

X-Frame-Options is the legacy version of frame-ancestors; when both are set, browsers use frame-ancestors.

A <Frame> document is a srcdoc document, and it inherits the host page’s CSP.

  • With style-src 'nonce-…', give every <style> you render through head the nonce: <style nonce={nonce}>.
  • copyStyles clones the parent’s style elements with their nonce, so mirrored styles keep working under a nonce-based policy.

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-kit

A 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.

For a page meant to run inside other sites’ iframes, such as a widget or checkout:

  • frame-ancestors lists the sites that may embed it, and matches allowedOrigins.
  • Cookies it needs while framed are SameSite=None; Secure. With third-party cookies blocked, add Partitioned (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.title if the page uses syncTitle: the embedding site receives it.

On the host page:

  • sandbox with only the flags the embed needs (see above).
  • allow only 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.
  • maxHeight on useIframeResize for anything you don’t control.
  • 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, toString and __proto__ answer RIK_METHOD_NOT_FOUND.
  • Error stacks cross the boundary only with debug: true, since they leak file paths. Don’t enable debug in production.
  • A cross-origin child controls its own reported size: set maxHeight for untrusted content.