Permissions Policy

The Permissions-Policy HTTP header lets you disable or restrict capabilities on your page and in any iframes you embed. Here is the syntax, the iframe allow attribute, and the Permissions API's real limitations.

June 10, 20264 min read2 / 2

Your ad script and your app code run on the same origin.

A permission granted to your site is a permission granted to every script on it. An analytics tag that gets compromised could read the user's location. An embedded widget could silently access the microphone.

The Permissions-Policy header is how you define what is actually available -- not just to your code, but to every script and iframe on the page.

This sits one layer above the browser permission model -- that model governs the user-facing dialog. This header governs what scripts are even allowed to trigger that dialog in the first place.

Cross-Origin Iframes: Blocked by Default

Before this header existed, cross-origin iframes could access whatever the parent page had permission for. Advertising networks abused this repeatedly. Ads were accessing geolocation, reading sensors, making noise.

The browser fixed this by changing the default.

Cross-origin iframes are now blocked from all capability APIs unless the parent page explicitly opts them in.

If you embed a third-party map widget, it cannot call navigator.geolocation unless you say it can. This is the secure default. The allow attribute on the <iframe> element is the opt-in mechanism.

HTML
<!-- only these two capabilities allowed inside the iframe --> <iframe src="https://maps.example.com/embed" allow="geolocation camera"></iframe>

Without the allow attribute, the iframe gets nothing.

The Permissions-Policy Header

The Permissions-Policy HTTP header goes on your HTML document response. It controls what is available on the main page and to all its scripts.

Three Permissions-Policy patterns: self-only, trusted third-party, and fully disabled ExpandThree Permissions-Policy patterns: self-only, trusted third-party, and fully disabled

Allow only this origin

HTTP
Permissions-Policy: geolocation=(self)

Only your own scripts can call the geolocation API. Third-party scripts injected on your page cannot. This is actually the default behavior, but making it explicit communicates intent.

Allow a specific cross-origin iframe

HTTP
Permissions-Policy: geolocation=(self "https://trusted-map.com")

Your scripts plus any iframe from trusted-map.com can use geolocation. All other cross-origin iframes are still blocked.

Disable entirely

HTTP
Permissions-Policy: camera=()

Empty list. Nobody on this page can access the camera -- not your code, not third-party scripts, not any iframe. Useful when your page has no camera feature and you want to reduce attack surface.

Combine multiple policies

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

Each policy is comma-separated. They stack. If a script tries to access camera after this header, it gets a SecurityError before the browser even considers showing a prompt.

The Permissions API

The Permissions API lets you query permission state before attempting to use an API.

JavaScript
const result = await navigator.permissions.query({ name: 'geolocation' }); if (result.state === 'granted') { navigator.geolocation.getCurrentPosition(updateMap); } else if (result.state === 'prompt') { showAskForLocationButton(); } else { showSettingsInstructions(); }

The three possible states are granted, prompt, and denied. Knowing the state in advance lets you decide whether to show a button, show instructions, or proceed silently.

The limitation

Here is the real problem: the Permissions API has narrow, inconsistent browser support.

Different browsers support different subsets. Safari supports a handful of permissions. Firefox supports a broader list but not the same one as Chrome. Some browsers return denied for permissions they do not support, rather than throwing an error.

In practice, the Permissions API is rarely used. Most codebases check whether the API exists ('permissions' in navigator), then check whether the specific capability exists, and treat a missing capability as equivalent to denied.

The feature detection pattern covered earlier in this series is more reliable than the Permissions API for most use cases.

JavaScript
if (!('geolocation' in navigator)) { showNotSupportedMessage(); return; } // At this point the API exists -- attempt the call // and handle denial in the error callback navigator.geolocation.getCurrentPosition( position => updateMap(position), error => handlePermissionError(error) );

The Permissions API is useful when you have it and the capability is listed. But it is not the foundation. Feature detection is.

What This Means in Practice

For the Permissions-Policy header: if your app has no reason to use a capability, disable it. The header is a cheap way to reduce surface area. If your page embeds third-party content, add it and default to restricting capabilities unless you have a specific reason to grant them.

For cross-origin iframes: the allow attribute is required for any capability you need inside an embedded widget. Don't add it speculatively -- only add what the widget actually needs.

For the Permissions API: use it where it's available and useful. Treat missing support as "not queryable," not as "denied." The real signal is whether the underlying API exists and whether the call succeeds.

The Essentials

  1. Cross-origin iframes are blocked from all capabilities by default -- use <iframe allow="..."> to opt in.
  2. Permissions-Policy: camera=() in an HTTP header disables the API entirely for the page, including all embedded scripts.
  3. (self) restricts to this origin; (self "https://x.com") adds a trusted domain; () disables everything.
  4. The Permissions API has narrow, inconsistent support -- it's useful where available but cannot be relied on across browsers.
  5. Feature existence checks ('geolocation' in navigator) are more portable than querying permission state.

Further Reading and Watching