← Blog

Clipboard Access Inside iframes and Cross-Origin Contexts

Embedding a clipboard-using feature inside an iframe is where the modern Clipboard API gets most confusing. The same code that works on the top-level page fails silently inside an iframe, throws NotAllowedError sometimes and SecurityError other times, and behaves differently across browsers. This article maps the actual rules — the Permissions-Policy header, the allow attribute on the iframe, the sandbox flags — and the patterns that make clipboard features portable across embedded contexts.

Why iframes are different

An iframe runs in a nested browsing context. The browser decides whether to let that context access powerful APIs (camera, clipboard, microphone, payment) based on two separate layers:

  • The outer page's Permissions-Policy. HTTP header on the outer document that declares which APIs are available for what origins. If the outer page disallows clipboard-read for the iframe's origin, the iframe cannot get it back by asking.
  • The iframe element's allow attribute. An explicit per-embed grant that restricts further within the outer policy's budget.

Both must permit the API for a clipboard call inside the iframe to succeed. Combined with sandbox flags and cross-origin opener rules, the interaction is complicated.

The allow attribute

By default, an iframe embed does not carry clipboard permissions. Even if the outer page's policy grants them, the iframe must opt in explicitly:

<iframe src="https://app.example" allow="clipboard-read; clipboard-write"></iframe>

Without the allow attribute, navigator.clipboard.readText() inside the iframe rejects with NotAllowedError on first call, and writeText() succeeds only when triggered inside a user gesture on some browsers while rejecting on others.

The allow attribute is a restriction, not an elevation. You cannot grant an iframe permissions its outer page does not already have.

The Permissions-Policy header

The outer page declares its policy via an HTTP response header:

Permissions-Policy: clipboard-read=(self "https://app.example"), clipboard-write=(self "https://app.example")

This reads: "I allow clipboard-read for my own origin and for app.example." Any other origin embedded on my page cannot use clipboard-read, even if its iframe allow attribute requests it.

Common misconceptions:

  • An empty policy (no header) is not permissive — it applies the browser's default, which on Chrome and Edge disallows cross-origin iframes from using clipboard-read.
  • clipboard-read=() with empty parens means "disallow everywhere, including self". Often accidentally set by copy-pasting example configs.
  • clipboard-read=* allows all origins — rarely what you want for security-sensitive APIs.

The sandbox attribute

An iframe with a sandbox attribute runs in a restricted mode. By default, sandbox disables script execution, same-origin privileges, and clipboard access. Flags add permissions back:

<iframe sandbox="allow-scripts allow-same-origin"></iframe>

Clipboard access inside a sandboxed iframe requires both allow-scripts (so your code runs at all) and an allow attribute as covered above. There is no allow-clipboard sandbox flag — the allow attribute is the single mechanism.

The paste event still works — mostly

The synchronous paste event is not gated by Permissions-Policy, because it runs in response to an explicit user action. A user pasting into a focused input inside an iframe fires a paste event with clipboardData populated, even on iframes without the clipboard allow attribute.

The exception: sandboxed iframes without allow-same-origin run in an opaque origin that the clipboard layer treats conservatively. In those contexts, paste event data arrives stripped of some types — notably custom application/* types and sometimes text/html.

If your embedded feature needs reliable paste event data, set sandbox="allow-scripts allow-same-origin".

Cross-origin iframes and focus

Even with all the policies aligned, a cross-origin iframe needs focus to call the async clipboard API. A click inside the iframe gives the iframe focus automatically. A click on the outer page does not — a subsequent clipboard call from inside the iframe will fail because the iframe lost focus to the outer page.

This is usually fine for "click the button in the iframe" flows. It breaks for orchestrated flows where the outer page opens a modal and triggers a clipboard call in the iframe from a postMessage: by the time the iframe receives the message, the user's click has already been absorbed by the outer page's modal.

Workable pattern: do the clipboard work in whichever frame received the user's click. Use postMessage only to hand the resulting value across the boundary.

Testing cross-origin behaviour

Local-development iframes are usually same-origin, and same-origin iframes skip many restrictions. That makes it easy to ship an integration that works on dev and fails in production. Before shipping, test with an actual cross-origin iframe — either by hosting the inner app on a separate subdomain or by using a sandbox attribute with no allow-same-origin flag to force the opaque-origin behaviour.

A concrete checklist for embedding

If you are shipping a feature that needs clipboard access inside an iframe:

  1. On the outer page, set the Permissions-Policy header to permit the inner origin: clipboard-read=(self "https://inner.example"), clipboard-write=(self "https://inner.example").
  2. On the iframe element, set allow="clipboard-read; clipboard-write".
  3. If you use sandbox, include allow-scripts and allow-same-origin.
  4. Trigger clipboard calls only from user gestures inside the iframe. Avoid postMessage-triggered clipboard calls.
  5. Test the configuration on a cross-origin subdomain before shipping.

Debugging

When a clipboard call fails in an iframe, browser DevTools is your first stop. In Chrome, the Application > Frames panel shows the computed Permissions-Policy for each frame, making it easy to see whether the iframe actually has clipboard permissions at runtime — versus what you think you configured.

If the permissions look right but calls still fail, run your iframe's JS against Clipboard Inspector embedded the same way you embed your own app. If the inspector works inside your iframe configuration, the clipboard layer is working and the bug is in your app. If it does not, the configuration is wrong. For the underlying permissions model, see the Permissions API guide.