A third-party script inside an iframe should never be able to turn on your users' camera, microphone or GPS just because your page embeds it. Permissions-Policy is the HTTP response header that makes that rule enforceable in modern browsers. It is the successor to Feature-Policy, it is separate from Content-Security-Policy, and a single misconfigured allowlist is enough to re-open a sensor you thought you had closed.

I build and run PixelValut, a set of browser-side developer and security tools, and I lock these headers down on my own sites the same way I recommend here. I wrote this with help from AI tools and checked every command and claim myself. This article is specifically about Permissions-Policy—camera, microphone, geolocation, payment, usb, fullscreen and how to verify the header—not a CSP deep-dive.

What does Permissions-Policy do for camera, mic and geo?

Direct answer: Permissions-Policy lets you declare, in an HTTP response header, which origins may use powerful browser features such as camera, microphone and geolocation. A deny-all default for sensors, with explicit allowlists only for trusted same-origin or partner origins, stops malicious iframes and injected scripts from calling those APIs even if they somehow execute on the page.

Header syntax in one pass

The modern header name is Permissions-Policy. Older docs and some servers still emit Feature-Policy with a different grammar. Prefer the new name and the allowlist syntax:

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

Meaning of the allowlist tokens:

Token Meaning
* Any origin may use the feature (almost never what you want for sensors)
() Empty allowlist — feature disabled for everyone, including your own origin
(self) Only your origin
(self "https://trusted.example") Your origin plus listed origins
"https://trusted.example" Only that origin (no self)

Feature names are lowercase. Common ones you should decide on deliberately:

Feature Risk if left open Typical production default
camera Covert video capture from iframe/script () unless you are a video product
microphone Covert audio capture () unless you need WebRTC on this origin
geolocation Silent location reads () or (self) for a maps feature you own
payment Payment Request API abuse () or (self) on checkout only
usb WebUSB device access ()
fullscreen Phishing overlays via fullscreen (self) often enough
display-capture Screen sharing () unless intentional
clipboard-read Clipboard exfiltration () or (self)

Threat scenarios this header actually blocks

Malicious iframe. You embed a partner widget. The partner gets compromised and serves a script that calls navigator.mediaDevices.getUserMedia(). Without Permissions-Policy, the browser may still prompt the user—or, in nested contexts with prior grants, behave in ways you did not intend. With camera=() and microphone=() on your top-level response (and appropriate allow attributes on the iframe), the feature is unavailable in that document tree.

Third-party script on your origin. A tag manager or analytics bundle is not a substitute for an allowlist. Permissions-Policy is enforced by the browser against the document's policy regardless of which script file initiated the call. Combined with a tight script policy (and habits from cookie security flags and certificate controls like CAA records and CT logs), you reduce both exfiltration and sensor abuse.

Payment and USB surprise surfaces. Checkout pages sometimes load chat or upsell iframes. payment=() on the marketing site and payment=(self) only on the payment origin keeps Payment Request scoped. usb=() should be the default everywhere unless you ship a deliberate WebUSB tool.

Permissions-Policy does not replace CSP, SRI or cookie flags. It closes a different door: powerful Web APIs.

A studio condenser microphone, face-down phone and small GPS-style tracker on a wooden desk, representing camera, mic and geolocation sensors
AI-generated image.

Deny-all vs allowlists: comparison

Strategy Example Pros Cons When to use
Deny-all sensors camera=(), microphone=(), geolocation=() Smallest attack surface Breaks intentional WebRTC/maps Marketing sites, docs, blogs, most SaaS admin UIs
Self-only geolocation=(self) First-party maps/check-in work Still exposed if XSS runs as you Features you own on the same origin
Explicit partner camera=(self "https://meet.example") Supports known embeds Must maintain the list; typos fail open or closed depending on test coverage Video vendors you trust
Wildcard * camera=* Convenient for demos Any iframe can request the sensor Never in production

Fail closed for camera, microphone, geolocation, usb and payment unless a product requirement says otherwise. Fullscreen at (self) is a reasonable compromise for app shells.

Real configuration examples

Static marketing site (recommended default):

Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), interest-cohort=(), browsing-topics=() 

SaaS app with first-party WebRTC on app.example.com:

Permissions-Policy: camera=(self), microphone=(self), display-capture=(self), geolocation=(), payment=(), usb=(), fullscreen=(self) 

Page that embeds a trusted meeting vendor:

<iframe src="https://meet.vendor.example/room/123" allow="camera https://meet.vendor.example; microphone https://meet.vendor.example; fullscreen"> </iframe> 
Permissions-Policy: camera=(self "https://meet.vendor.example"), microphone=(self "https://meet.vendor.example"), geolocation=(), payment=(), usb=() 

The iframe allow attribute and the Permissions-Policy header must agree. The stricter side wins. Setting only one of them is a common production bug.

Nginx snippet:

add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), fullscreen=(self)" always; 

Express:

app.use((req, res, next) => { res.setHeader( "Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=(), fullscreen=(self)" ); next(); }); 

Verify with curl and DevTools

Check the live response:

curl -sI https://pixelvalut.com/tools/ | rg -i 'permissions-policy|feature-policy' 

You want a Permissions-Policy line in the response headers. If you only see Feature-Policy, plan a migration. If you see neither, the browser falls back to defaults that are more permissive than a deny-all posture.

In Chrome or Edge DevTools:

  1. Open Network, reload, select the document request.
  2. Read Response Headers for permissions-policy.
  3. In the console, try navigator.mediaDevices.getUserMedia({audio:true}) on a page that sets microphone=() — you should see a policy error rather than a clean prompt flow.
  4. For iframes, inspect the frame's allow attribute and retest from inside that frame context.

I also hit pages through PixelValut's tools index and the HTTP status checker when I want a quick header read without opening the full browser stack. Confirm the header on the HTML document response, not only on APIs or static assets—policy for features is document-scoped.

Rollout checklist

  1. Inventory which pages genuinely need camera, mic, geo, payment, usb, fullscreen or display-capture.
  2. Set a deny-all baseline on the edge (CDN or reverse proxy) for marketing and docs hosts.
  3. Override per app host with (self) or explicit partner origins where product requires it.
  4. Align iframe allow attributes with the header; remove leftover allow="camera; microphone" on untrusted embeds.
  5. Add a CI or synthetic check that fails if production HTML responses drop the header.
  6. Test WebRTC and maps flows in staging with the real policy before launch.
  7. Document the allowlist next to CSP notes so the next engineer does not "temporarily" set *.
  8. Re-verify after CDN changes—some panels strip custom headers on cache rule edits.

Pre-deploy checklist

  • [ ] Permissions-Policy present on HTML document responses
  • [ ] camera, microphone, geolocation, usb, payment default to () unless required
  • [ ] Trusted embeds listed explicitly; no *
  • [ ] Iframe allow attributes match the header
  • [ ] curl / DevTools verification recorded for staging and production
  • [ ] Feature-Policy legacy header removed or kept intentionally in sync during migration
  • [ ] Owners know which product flows depend on each exception

Frequently asked questions

Is Permissions-Policy the same as Feature-Policy?

No. Feature-Policy was the older header with a different syntax (camera 'none'). Browsers migrated to Permissions-Policy with allowlist syntax (camera=()). Ship the new header; treat Feature-Policy as legacy.

Does a strict Permissions-Policy replace CSP?

No. CSP controls what resources load and what scripts execute. Permissions-Policy controls which powerful APIs those scripts may call. You want both, plus cookie flags and certificate controls where relevant.

What happens if the header is missing?

The browser uses its default feature availability. Many sensors still require a user prompt, but embedded contexts and previously granted permissions can surprise you. Missing policy is not the same as deny-all.

Can users override my policy?

Users control prompts and site settings, but they cannot make your page grant an iframe a feature your Permissions-Policy forbids. The header is an upper bound.

Should every subdomain share one policy?

Not always. app. may need camera for support calls while www. should deny it. Set policy at the host that serves the HTML, and avoid copying a loose app policy onto the marketing site.

How do I test without breaking production WebRTC?

Ship deny-all on hosts that never need sensors. On the WebRTC host, use (self) in staging, run call tests, then promote. Keep partner origins in config, not hard-coded in a one-off nginx edit.

Follow & Connect