TulipLot works best in Chrome or a Chromium-based browser. Some features may be limited here.
← All posts
Embedding · · 9 min read

Camera, microphone, and clipboard inside an iframe

Checked on 2026-08-24.

Put a video-call widget in an iframe and it can't reach the camera. Put a copy button in one and, in Chrome, navigator.clipboard.writeText() is refused. Neither is a bug. A cross-origin frame starts with the powerful features switched off, and the page framing it has to hand each one over.

Permissions Policy is the mechanism, and it has two halves people mix up. There's an allow attribute you write on the <iframe>, and a Permissions-Policy response header your server sends. They aren't alternatives. The header sets a ceiling; the attribute spends what's underneath it. Get that backwards and you'll write an allow that looks correct and does nothing.

It's a separate mechanism from the sandbox attribute, which takes capabilities away from a frame rather than handing them over. The Permissions Policy specification is explicit that the two sit side by side: it "is intended to be used in combination with the sandbox mechanism (i.e. it does not duplicate feature controls already covered by sandbox)". You'll often write both.

Off by default

Every feature Permissions Policy controls has a default allowlist, and for the ones you care about it's 'self'. The specification's definition of 'self' is the whole story: the feature is allowed in a top-level document and in a same-origin child frame. "It is disallowed by default in child navigables whose document is cross-origin with its parent's document."

Camera and microphone come from Media Capture and Streams: "This specification defines two policy-controlled features identified by the strings "camera" and "microphone". Both have a default allowlist of "self"." The Permissions Policy specification puts it plainly in its examples: "Camera and microphone are disabled by default in all cross-origin frames." Fullscreen's default is 'self' too, from the Fullscreen API Standard. Geolocation and payment follow the same shape.

Clipboard is the odd one out. As Permissions Policy features, clipboard-read and clipboard-write appear only in the working group's companion feature list, under a heading for features that "have been proposed, but the definitions have not yet been integrated into their respective specs." The Clipboard API and events specification never mentions Permissions Policy at all. It does define clipboard-write, but as a Permissions API permission, which is a different registry from the one that governs frames.

MDN is blunt about what shipped. In Chromium, "The HTTP Permissions-Policy permissions clipboard-read and clipboard-write must be allowed for <iframe> elements that access the clipboard", while "The clipboard-read and clipboard-write permissions are not supported (and not planned to be supported) by Firefox or Safari." So write the grant for Chromium and expect other engines to ignore it:

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

The unevenness isn't confined to the clipboard. MDN's availability banner on the Permissions-Policy header reads: "This feature is not Baseline because it does not work in some of the most widely-used browsers." Check the table for your feature.

The allow attribute

The attribute is the direct, per-frame grant. Per the HTML Standard, its value "must be a serialized permissions policy."

<iframe src="https://maps.example.com/" allow="geolocation"></iframe>

A directive is a feature name, whitespace, then an allowlist. Semicolons separate directives, so allow="camera; microphone; fullscreen" grants three at once. Spaces separate origins within an allowlist.

Leave the allowlist off and you don't get a wildcard. The Permissions Policy specification: "The allowlist for the features named in the attribute may be empty; in that case, the default value for the allowlist is 'src', which represents the origin of the URL in the iframe's src attribute." So allow="camera" and allow="camera 'src'" mean the same narrow thing: the origin in src, and nothing else.

Plan for that. MDN: "By default, if an <iframe> navigates to another origin, the policy is not applied to the origin that the <iframe> navigates to." A login redirect can move your frame off the src origin, and the grant won't follow. Name the origins the flow crosses: allow="camera 'src' https://auth.example.com".

The other values are * for every origin, 'self' for the embedding page's own, and 'none' for nothing. Keywords take single quotes here; bare origins don't. MDN: "Note that origins in <iframe> allow attributes are not quoted."

Like sandbox, the attribute is read at navigation time. Per the HTML Standard, "the allow and allowfullscreen attributes only take effect when the content navigable of the iframe is navigated. Adding or removing them has no effect on an already-loaded document." Setting allow from script on a live frame achieves nothing.

The header, and the ceiling it sets

Its syntax is a structured-field dictionary, not the attribute syntax. Commas separate directives, not semicolons. An allowlist of several origins goes in parentheses. Origins are quoted, and self is a bare token.

Permissions-Policy: camera=(self "https://widget.example.com"), microphone=(), fullscreen=*

An empty list, (), disables the feature everywhere, nested frames included. The attribute's equivalent is 'none'.

Here's the cap, from the HTML Standard: "Neither allow nor allowfullscreen can grant access to a feature in an iframe element's content navigable if the element's node document is not already allowed to use that feature." MDN's Permissions-Policy page restates it from the other end: "For an <iframe> to have a feature enabled its allowed origin must also be in the allowlist for the parent page." And MDN's <iframe> page, on the attribute itself: it "implements a further restriction on top of the policy specified in the Permissions-Policy header. It doesn't replace it."

So two conditions must hold before a frame gets anything: your page must have the feature, and your header's allowlist must include the child's origin. If your header says camera=(self), then allow="camera" on a cross-origin frame is dead markup. It parses, it looks right in the inspector, and the frame is still refused. Set the widest allowlist you'll live with in the header, then narrow it per frame.

Typos fail silently, so proofread the names. The Permissions Policy specification says that if a token "does not name one of the user agent's supported features, then the Dictionary Member will be ignored by the processing steps." A misspelled feature looks exactly like an unsupported one.

Fullscreen, and what happened to allowfullscreen

allowfullscreen is older than Permissions Policy, and it survived by being absorbed. The Permissions Policy specification files it under "Additional attributes to support legacy features" and redefines it in terms of the container policy.

That redefinition isn't a rename, and it runs the opposite way from most guesses. The Fullscreen API Standard spells it out: "Unless overridden by the allow attribute, setting allowfullscreen on an iframe is equivalent to" <iframe allow="fullscreen *">. That's a wildcard. The bare attribute grants fullscreen to any origin the frame lands on, while allow="fullscreen" grants it only to the src origin. The old attribute is the permissive one.

Write both and the newer one wins. Per the Permissions Policy specification, if the allow value contains the token fullscreen, "then the allowfullscreen attribute must have no effect", and "then the more restrictive allowlist of allow="fullscreen" will be used".

allowfullscreen isn't deprecated. MDN calls it a legacy attribute "redefined as allow="fullscreen *"", and the specification keeps it because doing so "is for compatibility with existing uses of allowfullscreen". Keep it if it works. Just know it's wider than the modern spelling.

Autoplay, and why a framed video stays silent

Three gates line up here, and only one is Permissions Policy. That's why fixing the delegation often changes nothing.

The first gate is the policy. MDN gives the default allowlist for autoplay as self, so a cross-origin frame lacks it and allow="autoplay" hands it over.

The second gate is the sandbox, the place the two mechanisms meet most visibly. The HTML Standard's conditions for a media element to be "eligible for autoplay" include both "its node document's active sandboxing flag set does not have the sandboxed automatic features browsing context flag set" and "its node document is allowed to use the "autoplay" feature". Any sandbox attribute without allow-scripts sets that flag, so the frame fails the first condition however much you delegated. The conditions are independent.

The third gate is the browser's own autoplay blocking, which isn't a policy. MDN's guide says you "can assume that media will be allowed to autoplay only if at least one of the following is true", and its list is short: the audio is muted or at volume zero, the user has interacted with the site, the site is allowlisted, or the policy granted it. MDN adds that "Autoplay blocking is not applied to <video> elements when the source media does not have an audio track, or if the audio track is muted."

That last detail explains the heading. You grant allow="autoplay", and an audible video still won't start: the browser refuses the playback, and play() rejects. Muted playback was never blocked, so muted is the only thing that plays until someone clicks. No header gets past a user-engagement heuristic.

What a delegation can't do

It can't override the framed origin's own policy. This is the direction people forget. If the framed document arrives with Permissions-Policy: camera=(), the camera is off, whatever your allow says. The Permissions Policy algorithm checks the document's own declared policy after the inherited one, and a declared policy that doesn't match the origin returns Disabled. You can restrict a frame you embed; you can't loosen a page that locked itself down.

It isn't a framing permission. Whether the page loads in a frame at all is settled before any of this runs, by that page's own X-Frame-Options or frame-ancestors — see X-Frame-Options explained and the frame-ancestors directive, or the measured headers for 60 apps at can I embed this site in an iframe?. Of those 60, 7 allowed framing and 53 refused. No allow value moves that number.

It isn't a granted permission either. Permissions Policy decides whether the frame may ask; the Permissions API and the user decide the answer. A prompt can still follow, and the user can still refuse.

Questions

Why doesn't allow="camera" work on my iframe?

Check your own page first. If your Permissions-Policy header omits the frame's origin, or your page lacks the camera itself, the attribute can't grant what you don't hold. Then check whether the frame navigated off its src origin, since 'src' doesn't follow a redirect. Last, check the framed page's own response header.

Is allowfullscreen deprecated?

No. Permissions Policy redefined it rather than removing it, and it still works. It's equivalent to allow="fullscreen *", which is broader than allow="fullscreen". If both appear on one element, the allow value wins.

Does allow replace the sandbox attribute?

No. sandbox removes capabilities from content you're framing; allow hands specific features over to it. Permissions Policy is designed for use alongside sandbox, not instead of it. Autoplay is where they meet most visibly: a sandbox can block it through the automatic features flag while your allow="autoplay" is perfectly valid.