Fix guide · high · iframe_dangerous_allow
Iframe delegates dangerous browser features to a cross-origin frame
An <iframe> on your page uses allow= to delegate camera, microphone, geolocation, payment, USB, or similar powerful APIs to a third-party origin. The browser permission prompt names YOUR domain, not the third party — so users approve the wrong thing.
Why it matters
The iframe allow= attribute is the per-frame Permissions Policy. It explicitly grants the embedded origin permission to call powerful browser APIs (getUserMedia for camera/mic, Geolocation, PaymentRequest, navigator.usb, etc.) as if the parent had granted them.
Two things go wrong:
- The permission prompt UX is misleading. When the embedded frame calls
getUserMedia, the browser shows a prompt that says "ALLOW <YOUR-DOMAIN> to access your camera?" — not the third-party origin. Users who trust your site grant the permission, and the third party gets it.
- You've extended your trust boundary to whoever runs that origin. If the third-party CDN is compromised (polyfill.io 2024, Browsealoud 2018), or if the origin changes hands silently (which has happened repeatedly with abandoned widgets), the new operator inherits all the permissions you delegated — to every page that embeds them.
Common offenders we've seen in real audits:
- Live-chat widgets with
allow="microphone; camera"for "voice support" features the operator never enabled. - Marketing video players with
allow="autoplay; payment; geolocation"— autoplay is reasonable; the rest is leftover from a "rich shoppable ad" feature. - Embedded analytics dashboards with
allow="clipboard-read"so the user can copy a report — except the iframe also has full clipboard access on every page load.
The fix is to grant the least powerful allow= list that makes the widget actually work. For most embeds, that's allow="autoplay; encrypted-media" and nothing else.
How to fix it
- Audit every cross-origin iframe. For each, ask the vendor: which of these features does your widget actually use? Cross out the rest.
- Strip dangerous features from
allow=. Specifically removecamera,microphone,geolocation,payment,usb,serial,bluetooth,hid,midi,clipboard-readunless the widget genuinely needs them. - Constrain the allowlist to the specific origin. Instead of
allow="payment", useallow="payment 'src'"(which only grants to the iframe's src origin) orallow="payment https://js.stripe.com"(specific origin).
```html <!-- Bad: payment allowed to the iframe origin AND any descendant frames --> <iframe src="https://payments.example.com/checkout" allow="payment"></iframe>
<!-- Better: only the iframe's own origin --> <iframe src="https://payments.example.com/checkout" allow="payment 'src'"></iframe> ```
- Consider
sandbox=for untrusted embeds. It restricts what the iframe can do (no scripts, no top-level navigation, no popups) regardless ofallow=.
``html <iframe src="https://untrusted.example.com/widget" sandbox="allow-scripts" allow=""></iframe> ``
- Set a top-level Permissions-Policy header so even if a third-party widget tries to call a feature, the browser refuses:
``http Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=(self "https://js.stripe.com") ``
This is the iframe-attribute's whole-page sibling — set both.
Did vibecheck flag this on your app?
If you reached this page from a vibecheck inspection report, the redacted match in your scan output is the exact string we found in your bundle. After applying the fix above, run the inspection again — the finding should clear.
Run another inspection