Permissions-Policy header: syntax, examples and mistakes (tested in Chrome)

Permissions-Policy switches browser features such as camera, microphone and geolocation off for your page and anything it embeds. Small syntax mistakes change what it does, so we tested them.

1. A safe default

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

() = nobody, not even your own page. In Chrome, that header turned off exactly those features on the page and in its iframes, even iframes with allow="geolocation; camera". fullscreen, which wasn't listed, stayed on. There is no "disable all" keyword: list each feature you don't use.

2. What Chrome did with each form

HeaderResult
geolocation=()Off for the page and every iframe
geolocation=(self)Your page only; a cross-origin iframe stayed blocked even with allow="geolocation"
geolocation=(self "https://maps.example")Your page, plus that origin's iframe if the iframe also has allow="geolocation"
geolocation=*Any iframe with allow="geolocation"; without the attribute the iframe was still blocked
geolocation 'none' (old Feature-Policy syntax)Whole header ignored, nothing turned off. Console: Parse of permissions policy failed because of errors reported by structured header parser.
geolocation=noneOff, but with a console error: Invalid allowlist item(none) … must be *, self or quoted url. Write ()
interest-cohort=(), camera=()The unknown interest-cohort was skipped; camera=() still applied

Two rules come out of this. Origins go in double quotes inside the parentheses. And a header from a scanner report or an old blog post written as feature 'none'; … does nothing at all in today's Chrome: rewrite it.

3. Iframes need both

With no header at all, a cross-origin iframe had no camera or geolocation until the <iframe> tag said allow="camera; geolocation". The header sets the limit; the attribute grants the feature to that frame. A feature has to pass both.

4. Adding it

# nginx (server block; 'always' also covers error pages)
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

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

A nested nginx location with its own add_header drops this line (the same trap measured with HSTS). The free security headers generator builds this header together with CSP, HSTS and the rest for nginx, Apache, Netlify/Cloudflare and Express.

Frequently asked questions

How do I disable all features with Permissions-Policy?

There is no wildcard 'deny all'. List each feature with empty parentheses, for example camera=(), microphone=(), geolocation=(). Features you don't list keep their defaults.

Why does my scan still say Permissions-Policy is not set or invalid?

If it is written in the old Feature-Policy form (geolocation 'none'), Chrome rejected the whole header with a parse error in our test. Use geolocation=().

What is the difference between Permissions-Policy and Feature-Policy?

Same idea, new name and new syntax: feature=(allowlist) instead of feature 'allowlist'. Chrome ignored the old syntax when it was sent in the Permissions-Policy header.

Do I still need the iframe allow attribute?

Yes. In our test even geolocation=* in the header did not enable geolocation in a cross-origin iframe without allow="geolocation".

Written with AI assistance. Measured on 2026-10-01 in Chromium 153: a page on localhost:8981 sent each Permissions-Policy below and embedded a page from another origin (127.0.0.1:8982) with and without allow="geolocation; camera". We read document.featurePolicy.allowsFeature() in both and logged console errors. This checks the policy, not the permission prompt the user sees afterwards. The nginx/Apache lines were not run. PlainHeaders is our extension; the other methods are free.