What the Permissions-Policy header can actually lock down
A practical guide to the browser features you can restrict, what you cannot control, and how to deploy the header without breaking useful functionality.
Table of contents
- The short version
- What Permissions-Policy controls
- What it can lock down on your own pages
- What it can lock down in iframes
- What it cannot lock down
- A sensible default policy
- How to deploy without breaking things
- 1. Inventory feature use
- 2. Start in a low-risk environment
- 3. Use page-specific policies where necessary
- 4. Verify the real response
- 5. Document exceptions
- Common syntax mistakes
- The practical privacy value
The short version
Permissions-Policy is an HTTP response header that lets a site limit access to certain browser features: camera, microphone, geolocation, fullscreen, payment, sensors, and a long list of smaller APIs.
It is not a general privacy shield. It will not stop all tracking, block cookies, prevent network requests, or make third-party JavaScript safe. What it can do well is narrower and still valuable: reduce the browser capabilities available to your own pages and to embedded frames.
That matters because modern websites are stitched together from analytics snippets, media embeds, chat widgets, consent managers, advertising scripts, maps, payment flows, and internal experiments. Most of those components do not need access to powerful device APIs. A good policy makes that explicit.
If you are already reviewing headers in production, pair this work with a direct check of the actual response. Our guide to debugging redirects and HTTP headers in production covers the habit that matters here: inspect what the browser really receives, not what your configuration file says should happen.
What Permissions-Policy controls
The header controls access to named browser features. The exact list changes over time because browser APIs change, but common directives include:
cameramicrophonegeolocationfullscreenpaymentusbbluetoothaccelerometergyroscopemagnetometerscreen-wake-lockclipboard-readclipboard-writepublickey-credentials-getinterest-cohortin older discussions, now mostly historical
A directive can allow a feature for no one, for the current origin, or for selected origins. For example:
Permissions-Policy: camera=(), microphone=(), geolocation=(self)
This means camera and microphone are disabled for the document and its nested browsing contexts, while geolocation is allowed for the same origin only.
A more permissive example:
Permissions-Policy: geolocation=(self "https://maps.example"), fullscreen=(self "https://video.example")
This allows your own origin and one named map provider to use geolocation, and your own origin plus a video provider to request fullscreen.
The policy is evaluated by the browser. If a feature is disallowed, JavaScript using that API should fail or behave as unavailable. The exact failure mode depends on the API. Sometimes a promise rejects. Sometimes a capability simply does not appear usable.
What it can lock down on your own pages
On first-party pages, Permissions-Policy is most useful as a guardrail. It reduces the blast radius of accidental or unexpected code.
A marketing page, for example, probably does not need the microphone, camera, Bluetooth, USB, motion sensors, or payment APIs. You can deny those features globally:
Permissions-Policy: camera=(), microphone=(), bluetooth=(), usb=(), payment=(), accelerometer=(), gyroscope=(), magnetometer=()
That does not make every script on the page trustworthy. It does mean that if a tag manager experiment, compromised dependency, or pasted widget tries to call a restricted API, the browser should not grant it access.
For teams with many contributors, this is a useful default. It moves the conversation from vague trust to explicit capability. If a future feature genuinely needs the camera, someone has to change the policy and explain why.
That is the right kind of friction.
What it can lock down in iframes
The header becomes especially useful around embedded content.
Browsers already treat iframes as separate browsing contexts, but embedded third-party content can still request powerful features if allowed by policy and iframe attributes. Permissions-Policy lets the parent page set a ceiling.
For example, if your page embeds a video player, a support widget, and a map, you can avoid giving every frame access to every feature. You might allow fullscreen only for the video frame and geolocation only for the map frame.
There are two layers to understand:
- The HTTP
Permissions-Policyheader sets policy for the document. - The iframe
allowattribute can delegate specific features to a frame, but only within what the parent policy permits.
A simple iframe might look like this:
<iframe src="https://video.example/embed/123" allow="fullscreen"></iframe>
If your header denies fullscreen entirely, the iframe attribute cannot override that denial. If your header allows fullscreen for that origin, the iframe attribute can delegate it.
This hierarchy is one reason the header is worth using. It gives platform or security teams a way to set site-wide boundaries, while still allowing product teams to enable specific embeds where needed.
What it cannot lock down
This is where teams sometimes overestimate the header.
Permissions-Policy does not replace a content security policy. It does not decide which scripts may load. It does not stop a script from sending data over the network. It does not sanitize HTML. It does not prevent XSS. It does not block form spam. If form abuse is the problem, start with the mechanics described in why your contact form is your biggest spam liability, not with this header.
It also does not replace cookie governance. Cookies, local storage, consent, third-party embeds, and browser tracking protections are separate issues. If you are reviewing privacy controls broadly, the cookie landscape deserves its own pass; the practical changes are covered in what changed for cookies in 2026 and what to do about it.
Most importantly, Permissions-Policy does not make third-party JavaScript private. If you load a third-party script into your first-party page, it generally runs with your page privileges, subject to other browser constraints and your security headers. Denying camera access is good. It does not stop that script from reading DOM content, observing user actions, or making allowed network requests.
For that, you need different controls: careful vendor selection, CSP, sandboxed iframes, Subresource Integrity where applicable, data minimization, and boring but necessary reviews.
A sensible default policy
There is no universal header that fits every site, but most content and marketing sites can start restrictive.
A reasonable first pass:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), bluetooth=(), accelerometer=(), gyroscope=(), magnetometer=(), screen-wake-lock=()
Then add back only what the site actually uses.
For example:
- A store using Payment Request API may need
payment=(self). - A location finder may need
geolocation=(self)or a trusted map origin. - A conferencing app may need
camera=(self)andmicrophone=(self). - A video-heavy site may need
fullscreen=(self "https://trusted-video.example").
The important part is not to copy an enormous policy from a checklist and call it done. Start with your feature inventory. Which pages need which browser capabilities? Which embeds need delegation? Which features would be surprising if they were requested?
How to deploy without breaking things
Roll this out like any other production header: deliberately.
1. Inventory feature use
Search your codebase for browser API calls such as getUserMedia, geolocation, requestFullscreen, PaymentRequest, Web Bluetooth, WebUSB, and wake lock APIs.
Then check third-party embeds. Documentation for video, maps, payment, and identity providers often mentions required iframe allow values.
2. Start in a low-risk environment
Add a restrictive header in staging and test core journeys. Pay attention to browser console messages. Browsers often report when a feature is blocked by permissions policy.
3. Use page-specific policies where necessary
Do not force one global policy if your product has very different page types. A blog, checkout, map page, and video room probably need different capabilities.
Most web servers, frameworks, and edge platforms can set headers conditionally by path. That is often cleaner than weakening the entire site for one feature.
4. Verify the real response
Headers can be added, overwritten, duplicated, or stripped by CDNs, reverse proxies, app servers, and middleware. Check the final response in browser DevTools or with command-line tools.
Also test embedded contexts. A top-level page appearing correct does not guarantee that an iframe received the delegation you intended.
5. Document exceptions
Every allowed feature should have an owner and a reason. This sounds bureaucratic until six months later, when nobody remembers why geolocation was opened to a vendor domain that no longer appears on the page.
Common syntax mistakes
The modern header syntax is compact but easy to get slightly wrong.
Use empty parentheses to deny a feature:
Permissions-Policy: microphone=()
Use self for the current origin:
Permissions-Policy: geolocation=(self)
Use quoted origins for specific external origins:
Permissions-Policy: fullscreen=(self "https://video.example")
Avoid relying on old Feature-Policy examples unless you are intentionally supporting legacy behavior. The older header used a different syntax and is not what you should design around today.
Also remember that browser support varies by directive. A browser may support the header but not a particular feature directive. That is normal. Treat the header as a defense-in-depth measure, not as your only privacy or security control.
<!-- tool-cta:start -->
💡 Try this: Verify your Permissions-Policy is being delivered as intended with Get Headers, which shows the raw response headers your server is sending.
<!-- tool-cta:end -->
The practical privacy value
The privacy value of Permissions-Policy is not that it makes a site anonymous or tracker-free. It does not.
Its value is that it narrows access to sensitive browser capabilities. Location, camera, microphone, device sensors, local hardware APIs, and payment flows are powerful. Most pages do not need them. Many embedded components should never be able to ask for them.
That is a real improvement. It reduces accidental prompts, limits unnecessary capability exposure, and gives your team a concrete artifact to review when new functionality ships.
The best version of this header is boring: restrictive by default, loosened only where a user-facing feature requires it, and tested as part of the normal release process.