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.
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.
<!-- 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.
ExpandThree Permissions-Policy patterns: self-only, trusted third-party, and fully disabled
Allow only this origin
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
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
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
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.
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.
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
- Cross-origin iframes are blocked from all capabilities by default -- use
<iframe allow="...">to opt in. Permissions-Policy: camera=()in an HTTP header disables the API entirely for the page, including all embedded scripts.(self)restricts to this origin;(self "https://x.com")adds a trusted domain;()disables everything.- The Permissions API has narrow, inconsistent support -- it's useful where available but cannot be relied on across browsers.
- Feature existence checks (
'geolocation' in navigator) are more portable than querying permission state.
Further Reading and Watching
- What is Feature Policy HTTP Security Header? (YouTube) -- overview of Permissions-Policy (formerly Feature-Policy) with practical examples
- Permissions-Policy -- MDN -- full reference for the header syntax and supported feature names
Keep reading