Antoine Duno
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
- The Permissions-Policy header gives you fine-grained control over which browser APIs your pages and embedded content can access. It replaced the deprecated Feature-Policy header and is now a critical privacy and security control for any modern web application.
- Includes copy-paste code examples and step-by-step instructions.
- Free automated scan available to verify your implementation.
Permissions-Policy Header: Lock Down Browser APIs on Your Website
Modern browsers expose a remarkable range of APIs — camera access, microphone input, geolocation, Bluetooth, USB, payment handlers, biometric sensors, and more. These capabilities are powerful and legitimate when used intentionally. They are dangerous when accessed by compromised third-party scripts, malicious iframes, or injected XSS payloads.
The Permissions-Policy HTTP response header (which replaced the deprecated Feature-Policy header in 2020) gives you a declarative way to control which of these APIs your page — and any content embedded within it — is allowed to access. It is part of the "defense in depth" approach: even if your CSP has a gap and a malicious script runs, a restrictive Permissions-Policy can prevent that script from silently activating the user''s microphone or extracting location data.
From Feature-Policy to Permissions-Policy
If you have a Feature-Policy header on your site, it is still interpreted by some browsers for backwards compatibility, but it is deprecated. The replacement is Permissions-Policy with a different syntax.
Feature-Policy (deprecated):
Feature-Policy: camera ''none''; microphone ''none''; geolocation ''self''Permissions-Policy (current):
Permissions-Policy: camera=(), microphone=(), geolocation=(self)Key syntax differences:
- Values use structured field syntax: feature=(allowlist) instead of feature value
- ''none'' becomes an empty list ()
- ''self'' becomes (self) without quotes
- Origins are specified directly: (self "https://partner.example.com")
- Multiple features are separated by commas
If you set both headers, modern browsers use Permissions-Policy. Set only Permissions-Policy going forward.
The Full List of Controllable APIs
The specification covers 30+ features, with browser support varying. Here are all notable ones as of 2026:
Sensor and Device APIs
| Feature | What It Controls |
|---|---|
camera | Access to video input devices |
microphone | Access to audio input devices |
geolocation | Access to navigator.geolocation |
accelerometer | DeviceMotionEvent acceleration data |
gyroscope | DeviceMotionEvent rotation data |
magnetometer | Orientation relative to Earth''s magnetic field |
ambient-light-sensor | AmbientLightSensor API |
Payment and Authentication
| Feature | What It Controls |
|---|---|
payment | Payment Request API |
publickey-credentials-get | WebAuthn navigator.credentials.get() |
Display and Fullscreen
| Feature | What It Controls |
|---|---|
fullscreen | Element.requestFullscreen() |
display-capture | Screen capture via getDisplayMedia() |
picture-in-picture | Picture-in-Picture API |
Hardware Access
| Feature | What It Controls |
|---|---|
usb | WebUSB API |
bluetooth | Web Bluetooth API |
serial | Web Serial API |
hid | WebHID API for human interface devices |
midi | Web MIDI API |
gamepad | Gamepad API |
Privacy and Tracking
| Feature | What It Controls |
|---|---|
interest-cohort | Google FLoC/Topics API (behavioral tracking) |
attribution-reporting | Privacy Sandbox Attribution Reporting |
browsing-topics | Topics API for interest-based advertising |
federated-credentials | Federated Credential Management (FedCM) |
identity-credentials-get | Identity credential access |
Content and Performance
| Feature | What It Controls |
|---|---|
autoplay | Automatic media playback |
encrypted-media | Encrypted Media Extensions (DRM) |
xr-spatial-tracking | WebXR augmented/virtual reality tracking |
document-domain | Setting document.domain (isolation breaking) |
sync-xhr | Synchronous XMLHttpRequest (deprecated but still used) |
shared-array-buffer | SharedArrayBuffer (Spectre mitigation context) |
cross-origin-isolated | High-resolution timer precision |
focus-without-user-activation | Focusing elements without user gesture |
Syntax Reference
Permissions-Policy: feature=(allowlist), feature=(allowlist), ...Allowlist values:
()— empty list, feature disabled for all origins including the page itself(self)— allowed for the same origin only, blocked for all third parties("https://specific.example.com")— allowed for specific origin only(self "https://specific.example.com")— allowed for self and a named partner- No value needed for "allow all" — just omit the feature from the policy (it defaults to allowed)
Building Your Permissions-Policy
Start from a restrictive baseline and allow only what your application uses.
Step 1: Identify what your application legitimately uses. A typical SaaS app might use payment (Stripe), fullscreen (video player), and nothing else.
Step 2: Block everything you do not use:
Permissions-Policy:
accelerometer=(),
ambient-light-sensor=(),
autoplay=(),
bluetooth=(),
browsing-topics=(),
camera=(),
display-capture=(),
document-domain=(),
encrypted-media=(),
fullscreen=(self),
gamepad=(),
geolocation=(),
gyroscope=(),
hid=(),
identity-credentials-get=(),
idle-detection=(),
interest-cohort=(),
magnetometer=(),
microphone=(),
midi=(),
payment=(self),
picture-in-picture=(),
publickey-credentials-get=(self),
screen-wake-lock=(),
serial=(),
sync-xhr=(),
usb=(),
xr-spatial-tracking=()Step 3: Grant exceptions for specific third-party needs:
Permissions-Policy:
camera=(),
microphone=(),
geolocation=(self "https://maps.googleapis.com"),
payment=(self "https://js.stripe.com"),
fullscreen=(self),
interest-cohort=()Why Restricting Unused APIs Matters
The security case for Permissions-Policy is stronger than it first appears.
XSS mitigation: If an attacker injects a script via XSS and that script tries to activate the microphone to eavesdrop on the user, microphone=() blocks the attempt even if your CSP failed to prevent the script from running in the first place.
Third-party script containment: Analytics, chat widgets, A/B testing tools, and marketing pixels often have broad API access if not restricted. A Permissions-Policy restricts these third-party scripts to the same API access as your own code.
Iframe security: When you embed third-party content in iframes (video embeds, ads, widgets), those iframes cannot use browser APIs you have restricted — even if those iframes have their own permissions from their origin. Your page''s policy creates a ceiling.
<!-- Even if the embedded content requests camera access,
the embedding page''s Permissions-Policy: camera=() blocks it -->
<iframe src="https://third-party-widget.example.com/embed"></iframe>You can also grant per-iframe permissions using the allow attribute:
<!-- Grant camera only to this specific iframe, not globally -->
<iframe
src="https://video-call.example.com/room/123"
allow="camera; microphone"
></iframe>The allow attribute on the iframe can only grant permissions that the embedding page already has — it cannot expand beyond the page-level Permissions-Policy.
The interest-cohort Directive
This one deserves special attention. Google''s Federated Learning of Cohorts (FLoC) — and its successor, the Topics API — work by having the browser classify the user''s browsing history into interest categories and share those categories with advertisers.
This happens at the browser level, not the page level. But a page can opt out on behalf of its visitors:
Permissions-Policy: interest-cohort=()This tells Chrome not to include visits to your site in the user''s FLoC/Topics calculation. It is a meaningful privacy protection for your users — especially relevant for healthcare, legal, financial, or other sensitive platforms where you do not want visits to your site feeding behavioral advertising profiles.
GitHub deployed this header site-wide in 2021 as a statement of intent regarding user privacy.
Implementation by Platform
Nginx
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=(), payment=(self), fullscreen=(self)" always;Apache
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=(), payment=(self), fullscreen=(self)"Express.js with Helmet
import helmet from ''helmet'';
app.use(helmet.permittedCrossDomainPolicies());
// Helmet does not yet have a built-in permissionsPolicy helper that covers all features,
// so set it directly:
app.use((req, res, next) => {
res.setHeader(
''Permissions-Policy'',
''camera=(), microphone=(), geolocation=(), interest-cohort=(), payment=(self), fullscreen=(self)''
);
next();
});Next.js (next.config.js)
module.exports = {
async headers() {
return [
{
source: ''/(.*)'',
headers: [
{
key: ''Permissions-Policy'',
value: ''camera=(), microphone=(), geolocation=(), interest-cohort=(), payment=(self), fullscreen=(self)'',
},
],
},
];
},
};Browser Support and Fallback Behavior
Permissions-Policy is supported in:
- Chrome 88+ (January 2021)
- Edge 88+ (January 2021)
- Firefox 74+ (partial support, improving)
- Safari 16+ (September 2022)
For browsers that do not support Permissions-Policy, there is no fallback enforcement — the header is silently ignored. This means your policy provides no protection in those browsers, but also causes no breaking changes. Maintain Feature-Policy alongside Permissions-Policy if you need coverage for older browser versions.
Feature-Policy: camera ''none''; microphone ''none''; geolocation ''self''
Permissions-Policy: camera=(), microphone=(), geolocation=(self)Verification
# Check your header
curl -I https://yourapp.com | grep -i permissions-policy
# Test in browser DevTools
# Open DevTools → Application → Permissions Policy
# Chrome shows each feature and whether it is allowedZeriFlow checks for Permissions-Policy as part of its 80+ security header audit, flagging missing headers and recommending specific values based on your application type. Run a free scan at zeriflow.com.
Check all your security headers instantly — free.
See exactly which headers are missing and how to fix them.