Permissions-Policy

Browsers ship a long list of powerful APIs: camera, microphone, geolocation, payment sheets, USB, Bluetooth, clipboard access, WebAuthn. Most sites need a handful. A marketing page or a docs site needs none. Every feature you leave reachable is one more thing an injected script can try to trigger.

Permissions-Policy is the response header that lets you turn those features off for the page (and for any iframes inside it). Set once, enforced by the browser on every API call.

Why restrict features

Two reasons.

  1. Defence in depth. If an attacker manages to inject a script despite CSP, a policy that disables camera and geolocation means the attacker cannot prompt the user even if the rest of the app could. Fewer attack surfaces per compromise.
  2. Vendor control. Third-party iframes (ads, chat widgets, embedded media) inherit permissions from your page by default in older browsers. Explicit policy makes the allowlist exact instead of "whatever the browser happened to do."

Syntax

The header uses HTTP Structured Fields. Each directive is name=allowlist, directives separated by commas.

Permissions-Policy: camera=(), geolocation=(self), payment=(self "https://checkout.vendor.com")

Allowlist tokens:

  • () empty: the feature is disabled everywhere on the page and in every iframe.
  • * wildcard: allowed in all contexts, including cross-origin iframes.
  • self: allowed in same-origin contexts only.
  • "https://origin": allowed in the named origin's iframes.

A useful defensive default for a site that uses none of these APIs:

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), bluetooth=(), hid=(), serial=(), clipboard-read=(), display-capture=()

The registry

The feature list is a living document maintained by the W3C working group, not baked into the spec itself. New features are added as browser APIs ship.

Selected features60+ total in the registry
cameramedia
MediaDevices.getUserMedia
microphonemedia
MediaDevices.getUserMedia
display-capturemedia
getDisplayMedia()
geolocationlocation
navigator.geolocation
paymentpayments
PaymentRequest
fullscreenui
Element.requestFullscreen
clipboard-readui
navigator.clipboard.read
usbdevice
navigator.usb
hiddevice
navigator.hid
serialdevice
navigator.serial
bluetoothdevice
navigator.bluetooth
publickey-credentials-getauth
WebAuthn sign-in
Every feature listed is off by default when you disable it in the header. The policy is additive: anything not named stays at the browser's default.

The spec itself is the W3C Permissions Policy Working Draft (currently October 2025). It specifies the framework and algorithm; the features are the companion document above.

Iframe delegation

The header on the parent page sets a ceiling for what any cross-origin iframe can use. The iframe tag's allow attribute is what actually delegates a feature down into that iframe. Both are required.

Delegating to an iframeboth layers must allow
parent page · example.com
Permissions-Policy: camera=(self "https://widget.vendor.com")
iframe · widget.vendor.com
<iframe src="https://widget.vendor.com/"
        allow="camera">
inside the iframe
navigator.mediaDevices.getUserMedia(...)
permission prompt shown

Cross-origin iframes need both: the parent's Permissions-Policy must include the iframe's origin, AND the iframe tag must ship a matching allow attribute. Miss either and the feature is blocked inside the frame even if it works on the parent page.

Toggle either side and watch the API call inside the iframe change state. The pattern is the same as CSP frame-ancestors in spirit: the parent decides the boundary, the child requests what it needs, the browser intersects.

Migration from Feature-Policy

Feature-Policy was the 2018 original. It is deprecated and being removed from specs, though browsers still accept it for compatibility. The syntaxes look similar but are not interchangeable:

Feature-Policy:    camera 'self' https://vendor.com
Permissions-Policy: camera=(self "https://vendor.com")

Values in Permissions-Policy use the Structured Fields list syntax (parentheses, quoted strings, explicit tokens). Ship both during migration if you still serve old browsers, then drop Feature-Policy.

Common mistakes

no header at all on a site that uses none of the features

Every feature is potentially reachable by script. Cheapest defence in depth on the web: disable them explicitly.

Feature-Policy syntax in Permissions-Policy

camera 'self' (space-separated, quoted 'self') is Feature-Policy syntax. Permissions-Policy needs camera=(self). Mixed-up values are silently ignored.

forgetting iframe allow= attribute

Parent's Permissions-Policy allows the vendor's origin, but the iframe tag has no allow="camera". Feature is blocked inside the frame. Ship both.

wildcard * instead of origin list

camera=* allows every iframe, including any vendor iframe that loads another iframe from its own vendor. Name origins explicitly.

shipping the policy only on top-level pages

API responses, /static/ assets, and framed pages all also honour the policy. Set it at the edge so every response gets it.


Check which features your site has left reachable, and whether the policy cleanly matches what you actually use: scan your domain.