Response header
< Permissions-Policy: camera=(), geolocation=(self)Permissions-Policy Header
Learn how the Permissions-Policy header controls which browser features and APIs can be used in your site and embedded iframes. Enhance security and privacy.
- Direction
- Response
- Category
- Security
On this page
TL;DR:
Permissions-Policydecides which origins may use browser features like the camera, microphone and geolocation in your page and its iframes.Permissions-Policy: camera=(), microphone=(), geolocation=()switches all three off in supporting browsers. Allowing a feature only makes it possible: the user still has to grant permission, and a cross-origin iframe also needs a matchingallowattribute.
What is Permissions-Policy?
This response header sets which origins may use specific policy-controlled browser features. It applies to documents and the frames nested inside them, not to individual scripts. A third-party script running in your page is bound by your page’s policy, whatever origin it was downloaded from. MDN explains how inheritance works.
How Permissions-Policy Works
An empty allowlist turns a feature off. self allows your own origin, as long as the feature’s other requirements, such as HTTPS and user permission, are met. Once an ancestor denies a feature, no iframe below it can turn it back on: camera=() on the parent beats allow="camera" on a child.
Syntax
A typical header (examples on this page are trimmed):
Permissions-Policy: camera=(), microphone=(), geolocation=(self "https://maps.example.com")
Directives are separated by commas. Inside the parentheses, list self and double-quoted origins separated by spaces. camera=* is the wildcard and camera=() is the empty list. This header uses its own syntax, so Feature-Policy’s semicolon-separated format doesn’t carry over.
Common Features
Camera, microphone and geolocation default to self, not *. Every feature has its own default, so leaving a feature out of your header doesn’t mean it’s allowed everywhere. Check the individual camera, microphone and geolocation references.
For a video embed that also needs fullscreen, these are the defaults that matter:
| Directive | Default allowlist | API restriction |
|---|---|---|
camera | self | Camera access through getUserMedia |
microphone | self | Microphone access through getUserMedia |
geolocation | self | Geolocation API |
fullscreen | self | Fullscreen API |
The fullscreen directive has its own compatibility table. Think of an allowlist as a ceiling. It sets the most a frame can do; the user’s permission decides the rest.
Real-World Scenarios
For a map iframe, serve the parent page with the geolocation policy above and delegate the feature to the frame:
<iframe src="https://maps.example.com/store-locator" allow="geolocation"></iframe>
The frame can now ask the user for their location; the user still decides. MDN’s geolocation example shows why you need both the parent header and the iframe delegation.
For a cross-origin video-call frame, name that origin in the parent’s policy:
Permissions-Policy: camera=(self "https://call.example.com"), microphone=(self "https://call.example.com"), fullscreen=(self "https://call.example.com")
Then delegate the same features on the frame:
<iframe src="https://call.example.com/room"
allow="camera; microphone; fullscreen" title="Video call"></iframe>
The iframe’s allow attribute can only narrow what the parent policy allows. If the frame later navigates to a different origin, revisit the allowlist; the original delegation may not apply to the new origin.
Server Configuration
In the nginx server or location that serves your HTML:
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
In Apache with mod_headers loaded:
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
See the nginx and Apache directive docs. Pick one layer to own the policy, and check the header that actually arrives after every proxy.
In Express, you can turn device APIs off by default and enable them only on the page that needs them:
app.use((_req, res, next) => {
res.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
next()
})
app.get('/call', (_req, res) => {
res.set('Permissions-Policy', 'camera=(self), microphone=(self), geolocation=()')
res.sendFile('call.html', { root: '/srv/app/public' })
})
Put the HTML file at that fixed path. Register the default middleware before the route, so the route’s policy is the one that lands; a global setter that runs later would overwrite it.
In Flask, an after-request hook with setdefault adds the default without clobbering a route’s own policy:
from flask import Flask, make_response, render_template
app = Flask(__name__)
@app.after_request
def policy(response):
response.headers.setdefault(
"Permissions-Policy", "camera=(), microphone=(), geolocation=()"
)
return response
@app.get("/call")
def call():
response = make_response(render_template("call.html"))
response.headers["Permissions-Policy"] = "camera=(self), microphone=(self)"
return response
call.html goes in Flask’s templates directory. Flask’s after_request hook receives the response and has to return it. The call page gets its own policy while every other page keeps the camera off.
HTML Equivalent
The iframe allow attribute uses a different syntax and only affects that one iframe, so it can’t stand in for the document-wide header. To turn off camera and microphone in a single frame:
<iframe src="https://embed.example.com/widget" allow="camera 'none'; microphone 'none'"></iframe>
Migration from Feature-Policy
An old Feature-Policy: geolocation 'none' becomes Permissions-Policy: geolocation=(). Check each feature name as you migrate, because which directives are supported varies by browser and feature.
Here’s a full conversion:
Feature-Policy: camera 'none'; microphone 'self'
Permissions-Policy: camera=(), microphone=(self)
The iframe attribute didn’t change: it still uses semicolons and quoted keywords. Leave its value alone; pasting the comma-separated header syntax into it breaks it.
Getting Permissions-Policy right
Put the policy on the document that contains the iframe. A policy on the JavaScript file or a JSON API response has no effect on the page. Keep the parent’s allowlist and the iframe’s allow attribute in agreement.
Common Patterns
A video-call page might use camera=(self), microphone=(self), while a static content page turns both off. Base the policy on what each page actually uses, instead of pasting a long list of experimental directives.
If the call runs on your own origin, camera=(self) also covers same-origin frames inside it, within whatever their ancestors allow. For a third-party call frame, use the explicit origin and delegation shown above. Setting camera=() site-wide and then trying to allow the camera only in the iframe fails, because the parent has already said no.
Testing Permissions-Policy
With camera=() in place, try this from a secure page:
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true })
stream.getTracks().forEach((track) => track.stop())
} catch (error) {
console.log(error.name)
}
Supporting browsers report a policy denial as NotAllowedError. A user clicking “Block” produces the same name, so look at the response header and the iframe’s policy before you blame either one. MDN lists getUserMedia’s error cases.
Check browser support directive by directive; there’s no single version where the whole header starts working. MDN marks the header as limited availability. Seeing the header in the Network panel only tells you it was sent, so test each directive you depend on.
Common Issues
Allowlists take origins, not URLs with paths. Write "https://maps.example.com", not the store-locator URL. A child frame can’t loosen a parent’s denial, and a parent’s permission doesn’t skip the user’s.
A sandboxed frame without allow-same-origin has an opaque origin and can’t use getUserMedia, and allow="camera" won’t change that. MDN lists the iframe and secure-context requirements. Check the sandbox attributes as well as the policy before editing the parent’s allowlist.
Security Considerations
Permissions Policy restricts a specific set of APIs. It doesn’t stop tracking in general or control everything an embedded script does, and it’s no substitute for CSP or iframe sandboxing. Turning off the camera and geolocation has no bearing on which scripts can load or where they can connect; that’s CSP’s job.
Related Headers
Frequently asked questions
What is Permissions-Policy?
Permissions-Policy allows or denies supported policy-controlled APIs for a document and its frames. It does not grant user permission or control every browser API.
What happened to Feature-Policy?
Permissions-Policy replaces Feature-Policy with different header syntax. For example, the old geolocation allowlist using the quoted none keyword becomes geolocation=().
How do I disable a feature?
Use an empty allowlist, such as geolocation=() or camera=(), to deny a supported feature in the document and nested frames. Child declarations cannot override an ancestor denial.
How do I allow features for specific origins?
Use an allowlist such as geolocation=(self "https://maps.example.com") on the parent response and delegate geolocation through the cross-origin iframe allow attribute. User permission is still required.
Sources
Related
Permissions-Policy vs Feature-Policy
Understand the differences between Permissions-Policy and Feature-Policy headers. Compare syntax changes for geolocation, microphone, camera directives and learn how to migrate.
Access-Control-Allow-Credentials Header
Learn how Access-Control-Allow-Credentials controls whether browsers expose responses when credentials (cookies, auth headers) are included in CORS requests.
Access-Control-Allow-Headers Header
Learn how Access-Control-Allow-Headers specifies which custom HTTP headers can be used during cross-origin requests in CORS preflight responses.
Access-Control-Allow-Methods Header
Learn how Access-Control-Allow-Methods specifies which HTTP methods are permitted for cross-origin requests in CORS preflight responses.
Access-Control-Allow-Origin Header: CORS Errors and Fixes
Access-Control-Allow-Origin explained: exact browser errors, why * fails with credentials, why you need Vary: Origin, and fixes for nginx, Express and Django.