SiteTidy
HomeGuidesPermissions Policy: Lock Down Browser Features Without Breaking Your Iframes
Security

Permissions Policy: Lock Down Browser Features Without Breaking Your Iframes

A practical guide to the Permissions-Policy header and iframe allow attribute, including inheritance, camera and microphone delegation, rollout, reporting and common production mistakes.

Published on September 2, 2026

Permissions-Policy is one of those response headers that is easy to add and surprisingly easy to misunderstand.

A scanner tells you the header is missing. You find a long example on the web, disable camera, microphone, geolocation and another dozen browser features, get a better security score, and move on.

That can be perfectly harmless on a simple brochure site. On an application with video calls, maps, payment flows, embedded dashboards or third-party widgets, the same approach can quietly remove capabilities that the product actually needs.

The useful way to think about Permissions Policy is not “which security headers should I copy?” It is:

Which browser capabilities should this document, and the documents it embeds, be allowed to use?

That makes the policy an architectural decision rather than a scanner-appeasement exercise.

What Permissions Policy actually controls

Permissions Policy lets a page restrict access to policy-controlled browser features. Depending on the feature and browser, that includes capabilities such as geolocation, camera, microphone, fullscreen, display capture, USB, Bluetooth, screen wake lock and several newer platform APIs.

The current W3C specification describes it as a mechanism for selectively enabling and disabling browser features and APIs. MDN maintains the more practical list of directives because the set evolves as browser APIs evolve.

A deliberately restrictive header might begin like this:

Permissions-Policy: camera=(), microphone=(), geolocation=()

The empty parentheses mean no origin gets that capability under this policy.

That includes your own page.

This is the first detail worth getting right. camera=() does not mean “do not let random third-party iframes use the camera”. It means you have disabled the camera feature for the document and its descendants under that policy.

If your application later adds QR scanning or a video-support feature, that old security-hardening decision suddenly becomes a product bug.

Permissions Policy is not the browser permission prompt

There are two different decisions involved when a site wants something like geolocation.

First, is this document allowed by policy to use the feature at all?

Then, where the API requires it, has the user granted the relevant permission?

Permissions Policy deals with the first question. It does not silently grant camera, microphone or location access to a site.

For example, allowing geolocation for your own origin:

Permissions-Policy: geolocation=(self)

does not bypass the user’s location permission. It establishes that the feature is eligible to be used by that origin; the normal API and permission model still applies.

This distinction matters because I occasionally see policies treated as if they were a replacement for browser permissions. They are an additional boundary.

The syntax is small; the allowlist decision is the real work

The header is made from feature directives and allowlists.

A few common shapes are:

Permissions-Policy: camera=()
Permissions-Policy: camera=(self)
Permissions-Policy: camera=*
Permissions-Policy: camera=(self "https://video.example")

Conceptually:

Policy Meaning
camera=() Disable camera everywhere covered by the policy
camera=(self) Allow your own origin
camera=* Allow all origins
camera=(self "https://video.example") Allow your origin and the named origin

I would avoid * for powerful features unless there is a very specific reason for it. If you know which embedded service needs a capability, name that boundary rather than delegating it indiscriminately.

There is another trap here: directives have their own default allowlists. Omitting a directive is not equivalent to explicitly writing (), and the default is not identical for every feature. MDN’s current directive documentation is the right place to check the default for a capability you actually use.

Do not maintain a mental table of fifty defaults. It will age badly.

The HTTP header establishes policy for the document and influences what descendants can receive.

An iframe’s allow attribute lets the embedding page control features for that particular frame.

Suppose your application embeds a video service:

<iframe
  src="https://video.example/room/123"
  allow="camera; microphone; fullscreen">
</iframe>

That is not a magic override for a restrictive parent header.

If the parent response says:

Permissions-Policy: camera=(), microphone=()

then the iframe cannot re-enable those features merely by asking for them in allow.

The W3C model deliberately treats policy inheritance as a restriction. A child cannot widen a capability that its parent has already removed.

This is one of the most useful properties of the system and one of the most common sources of confusion during debugging.

A parent policy and iframe policy combine toward the restrictive result

Imagine the top-level application sends:

Permissions-Policy: camera=(self "https://video.example"), microphone=(self "https://video.example")

and embeds:

<iframe
  src="https://video.example/room/123"
  allow="camera; microphone">
</iframe>

Now the parent has made the video origin eligible, and the individual frame delegates the capabilities it needs.

That is a much better model than broadly allowing camera and microphone for every frame on the site.

The distinction becomes particularly useful when you have several third-party frames:

<iframe
  src="https://video.example/room/123"
  allow="camera; microphone; fullscreen">
</iframe>

<iframe
  src="https://analytics.example/dashboard"
  allow="fullscreen">
</iframe>

There is no reason for the analytics dashboard to inherit camera access simply because the video component needs it.

Treat the iframe allow attribute as part of the integration contract for that embed.

self means your origin, not “things I trust”

This sounds obvious until an application is split across subdomains.

If the page is served from:

https://app.example.com

then self refers to that origin. It does not automatically mean:

https://video.example.com
https://maps.example.com
https://admin.example.com

Origins include scheme, host and port. A sibling subdomain is not the same origin just because it belongs to the same company.

If another origin genuinely needs a feature, make that explicit:

Permissions-Policy: geolocation=(self "https://maps.example.com")

Explicit origin boundaries make the policy slightly longer and substantially easier to reason about six months later.

Do not paste a giant deny-list without checking the product

A common hardening header looks roughly like this:

Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()

For a static company website, that may be a sensible starting point.

For a web application, I would not deploy it merely because an online scanner recommended it.

Inventory what the product does first.

Does it:

  • scan barcodes or documents with the camera?
  • offer voice/video support?
  • embed a map that requests location?
  • use a third-party payment experience?
  • enter fullscreen for presentations or video?
  • share the screen?
  • interact with hardware through WebUSB, WebHID, Bluetooth or Serial?
  • embed another application that needs one of those capabilities?

The difficult failures are often not on the homepage. They appear three screens into an account workflow or inside a support widget that only opens when somebody needs help.

A security header that breaks an important feature is not a successful security improvement.

Start from capabilities, not from somebody else’s header

For an ordinary content site, I would start by identifying capabilities the site clearly has no business using.

For example:

Permissions-Policy: camera=(), microphone=(), geolocation=(), usb=(), serial=(), bluetooth=()

Then I would review each directive rather than expanding the list simply to make it look comprehensive.

For an application that uses video calling on its own origin, the policy might instead contain:

Permissions-Policy: camera=(self), microphone=(self), geolocation=(), usb=(), serial=()

For a third-party video provider:

Permissions-Policy: camera=(self "https://video.example"), microphone=(self "https://video.example"), geolocation=()

paired with an appropriately restricted iframe:

<iframe
  src="https://video.example/room/123"
  allow="camera; microphone">
</iframe>

Those examples are intentionally small. A policy you can explain is preferable to forty copied directives nobody on the team remembers adding.

Feature names and support change over time

This is where Permissions Policy differs from older, relatively static security headers.

The set of policy-controlled features evolves with the web platform. As of 2026, MDN’s current header reference includes established directives alongside newer or experimental controls for capabilities such as local-network access, on-device language features and other emerging APIs. The W3C Permissions Policy document itself is still a Working Draft.

That has two practical consequences.

First, do not assume every browser supports every directive. The specification explicitly allows user agents to support different sets of policy-controlled features.

Second, do not cargo-cult a header from a five-year-old blog post. Some feature names, browser behaviour and platform APIs will have moved on.

I would keep the production policy focused on capabilities relevant to the application and review it when the application adopts a new browser API.

Unknown directives are not a reason to abandon the header

Because support varies, you may see warnings for directives that a particular browser does not recognise.

That is different from a policy being universally invalid.

The current specification defines processing in which unsupported feature names are ignored by the user agent. In practice, you should still test the browsers you support, because the useful question is not “did this header parse somewhere?” but “is the capability boundary we rely on actually enforced where we expect it to be?”

Permissions Policy should be treated as defence and capability control, not as an excuse to make sensitive application logic depend on one browser enforcing one directive.

Reporting exists, but check support before designing around it

The current Permissions Policy specification defines Permissions-Policy-Report-Only, and the header syntax can associate directives with Reporting API endpoints.

That is attractive for rollout because, in principle, you can observe would-be violations before enforcing a policy.

The awkward part is browser support. Permissions Policy itself and its reporting pieces are not uniformly implemented across browsers. MDN currently marks the main header as limited availability and experimental.

So I would use reporting where your target browser estate supports it, but I would not make it the only test strategy.

You still need real workflow testing.

For a mature application, that means exercising camera, location, payment, fullscreen, embedded widgets and other capability-dependent flows in the browsers you claim to support.

If you already use CSP reporting, the operational mindset is similar: reports are evidence to investigate, not a substitute for understanding the policy. The SiteTidy CSP series goes into that rollout discipline in more depth.

Permissions Policy and CSP are different boundaries

It is easy to lump these headers together because both are security policies.

They answer different questions.

A Content Security Policy can restrict where scripts, styles, frames and other resources are allowed to come from. Permissions Policy controls whether documents are eligible to use particular browser features.

For an embedded video service you might therefore need both concepts:

CSP frame-src
    ↓
May this origin be embedded at all?

Permissions-Policy + iframe allow
    ↓
Which browser capabilities may the embedded document use?

Allowing https://video.example in frame-src does not automatically grant it microphone access. Granting microphone through Permissions Policy does not automatically make CSP permit the frame.

Keeping those questions separate makes both policies easier to debug.

Do not confuse it with the old Feature-Policy header

Permissions Policy grew out of work previously called Feature Policy. You will still find old examples using:

Feature-Policy: camera 'none'; microphone 'none'

Do not copy that syntax into a new implementation.

Use current Permissions-Policy documentation and current browser testing. Apart from the header name changing, the syntax and specification model evolved as the work moved forward.

Old Stack Overflow answers are particularly good at keeping obsolete header examples alive.

Deployment examples

The exact configuration point depends on where your responses are produced.

Nginx

add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

Apache

Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"

ASP.NET Core

A small middleware can add the header centrally:

app.Use(async (context, next) =>
{
    context.Response.Headers["Permissions-Policy"] =
        "camera=(), microphone=(), geolocation=()";

    await next();
});

Node / Express

app.use((req, res, next) => {
  res.setHeader(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=()'
  );
  next();
});

I prefer setting this at a layer where the policy is visible and version-controlled with the application or infrastructure. A mysterious CDN rule added years ago is much harder to diagnose when somebody eventually needs camera access.

After deployment, inspect the actual public response rather than assuming your framework configuration reached production. SiteTidy’s website audit is useful for checking the response headers the public site is really returning.

Test the negative case as well as the happy path

A useful Permissions Policy test proves two things:

  1. the feature works where you intended to allow it;
  2. the same feature is blocked where you intended to deny it.

For example, if only your video iframe should receive camera access, test that frame and a second frame that should not receive it.

That catches policies that are accidentally too broad.

Browser developer tools and console messages can help diagnose policy violations, but I would also test the API’s real failure behaviour. A camera integration should handle getUserMedia() being unavailable or denied rather than assuming the happy path forever.

That matters beyond Permissions Policy. Users can deny permissions, enterprise browser policy can remove capabilities, hardware can be absent, and embedded contexts can change.

Feature failure should be a handled state.

A rollout I would actually use

For an existing production application, I would not begin by copying the longest Permissions Policy I could find.

I would do this:

  1. list the powerful browser capabilities the application and its embeds genuinely use;
  2. identify features that are clearly unnecessary;
  3. add a small policy denying those unnecessary capabilities;
  4. explicitly delegate required capabilities to known origins and frames;
  5. test important user journeys in supported browsers;
  6. inspect production response headers;
  7. expand the policy only when each additional restriction has a reason.

For a simple static site, steps one and two may take five minutes. For a SaaS application full of embeds, they deserve more care.

The point is not to produce the most impressive-looking header. It is to create a capability boundary that remains understandable when the product changes.

The maintenance rule is simple

Revisit Permissions Policy when you add a feature that touches a browser capability or introduce a new embedded origin.

If a developer adds a video provider and discovers that microphone access is blocked, the wrong fix is:

Permissions-Policy: microphone=*

The useful question is which document needs the microphone and why.

Usually the answer can be expressed narrowly.

That is where Permissions Policy earns its place alongside CSP and the other response-header controls: not as another line that makes a scanner turn green, but as an explicit record of which parts of your application are allowed to ask the browser to do powerful things.

Primary references

Permissions Policy is still evolving, so check current platform documentation when adopting a new directive:

Ready to audit your site?

Put this guide into practice immediately using our free tools.

Browse 180+ Free Tools