Permissions-Policy: What to Disable and Why
Permissions-Policy controls which browser APIs your page and embedded iframes can use. Here's what each feature does, which to block by default, and how to write the header.
Permissions-Policy Explained: API Access Control for Modern Web Security
Web browsers have evolved from simple document viewers into powerful application platforms, equipped with APIs capable of accessing local hardware, sensors, and sensitive user data. While these features enable rich user experiences, they also present significant security and privacy risks. Permissions-Policy (formerly known as Feature-Policy) is the definitive security header for controlling which browser APIs your site and its embedded third-party iframes are permitted to use.
Whether you are a Web Developer building complex interactive applications, a Security Engineer auditing attack surfaces, or a System Administrator configuring web servers, mastering the Permissions-Policy header is essential for a robust defense-in-depth strategy.
Quick Answer: What is Permissions-Policy?
Permissions-Policy is an HTTP response header that allows site owners to explicitly declare which browser features and APIs (like the camera, microphone, geolocation, and USB) can be used on their website and within embedded iframes.
By defining an allowlist of origins for each feature, you mitigate the risk of cross-site scripting (XSS) payloads abusing powerful APIs, prevent third-party embeds (like ads) from overreaching, and protect user privacy against browser fingerprinting.
Example of a strict, secure baseline policy:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=()
People Also Ask (PAA)
What happened to Feature-Policy?
Feature-Policy is the deprecated predecessor to Permissions-Policy. The primary difference is the syntax: Feature-Policy used space-separated lists without parentheses (camera 'none'), whereas Permissions-Policy uses a structured dictionary format with parentheses (camera=()). Browsers have phased out Feature-Policy in favor of the newer standard.
Does Permissions-Policy prevent XSS? No, it does not prevent Cross-Site Scripting (XSS) attacks from occurring-that is the job of the Content Security Policy (CSP). However, Permissions-Policy heavily mitigates the impact of XSS. If an attacker injects malicious JavaScript, they cannot access the user's camera, microphone, or precise location if the Permissions-Policy explicitly blocks those features.
How does Permissions-Policy interact with the allow attribute on iframes?
The HTTP header defines the top-level policy. To delegate permission to a cross-origin iframe, the feature must be allowed by the top-level header for that specific origin, AND the <iframe> tag must include the corresponding allow="feature" attribute.
The Anatomy of the Permissions-Policy Header
The modern Permissions-Policy header utilizes Structured Field Values (RFC 8941) to ensure consistent and parseable syntax. The structure defines a list of features, each followed by an allowlist.
Basic Syntax
Permissions-Policy: <feature>=<allowlist>, <feature>=<allowlist>
Allowlist Values Explained
The allowlist determines which origins are granted access to the specified feature:
-
()(Empty List)- Meaning: The feature is completely disabled for all origins, including the top-level site itself.
- Example:
usb=() - Use Case: Best for entirely disabling features your application never uses.
-
(self)- Meaning: The feature is allowed only for the same origin as the top-level document. Embedded cross-origin iframes cannot use it.
- Example:
geolocation=(self) - Use Case: When your main application needs the API, but you don't want third-party widgets accessing it.
-
(self "https://trusted-site.example.com")- Meaning: The feature is allowed for the same origin AND explicitly named third-party origins.
- Example:
camera=(self "https://video-vendor.com") - Use Case: When integrating a third-party service (like a WebRTC video player) that requires API access.
-
*(Wildcard)- Meaning: The feature is allowed in all contexts, including cross-origin iframes.
- Example:
fullscreen=* - Use Case: Rarely recommended for sensitive APIs, but useful for benign layout features like fullscreen if you extensively embed media.
[!WARNING] Never use the wildcard (
*) for privacy-sensitive APIs such ascamera,microphone, orgeolocation. This grants carte blanche access to any embedded script or iframe.
Why API Access Control Matters
1. Mitigating Post-Exploitation XSS
Imagine a scenario where a stored XSS vulnerability exists on your forum platform. An attacker injects JavaScript that silently requests the user's geolocation or attempts to access their microphone to record ambient audio.
Even if the browser prompts the user for permission, the prompt appears to come from your trusted domain, leading many users to blindly click "Allow."
By sending Permissions-Policy: microphone=(), geolocation=(), the browser fundamentally neuters the API. The injected script will simply receive a NotAllowedError or a hard exception, blocking the attack vector entirely.
🚨 SECURITY INTERVENTION: BROWSER API BLOCKED
---
Source: https://your-app.com/forum/post/123
Action Attempted: navigator.mediaDevices.getUserMedia({ audio: true })
Outcome: BLOCKED by Permissions-Policy
Reason: "microphone" feature is set to '()' (disabled globally).
Attacker payload successfully neutralized. No user prompt displayed.
2. Thwarting Browser Fingerprinting
Ad networks and malicious tracking scripts often use obscure browser APIs to generate unique device fingerprints. By querying sensor data (accelerometer, gyroscope, magnetometer), battery status, or ambient light sensors, trackers can stitch together a unique identifier for the user even if cookies are disabled.
Restricting these APIs natively severs the data supply for fingerprinting scripts.
3. Restricting Third-Party Embeds
Modern web pages are often amalgamations of first-party code and third-party widgets (ads, analytics, chat widgets, video players). A vulnerability or malicious update in a third-party script runs within the context of your application.
Permissions-Policy ensures that these third parties operate under the principle of least privilege. If a chat widget doesn't need Bluetooth access, the browser guarantees it cannot invoke Web Bluetooth.
What to Block by Default: A Comprehensive Guide
For the vast majority of web applications, you should adopt a "deny by default" stance for powerful APIs. Below is an exhaustive list of features and the rationale for disabling them.
Sensor and Hardware APIs
| Feature Directive | Default Stance | Rationale |
|---|---|---|
camera=() | Block | Unless you host video chats or profile picture capture, disable it. |
microphone=() | Block | Disable unless operating voice input or audio chat. |
usb=() | Block | WebUSB allows direct hardware interaction. Extremely high risk for normal sites. |
bluetooth=() | Block | Web Bluetooth should be blocked to prevent malicious device pairing. |
serial=() | Block | Web Serial API allows accessing local serial ports. Rarely needed. |
geolocation=() | Block | Block unless providing maps or local-centric content. |
battery=() | Block | Removed from many browsers due to fingerprinting, but block it explicitly. |
magnetometer=() | Block | Sensor APIs are prime targets for user fingerprinting. |
gyroscope=() | Block | Used for fingerprinting; rarely needed outside of AR/VR web apps. |
accelerometer=() | Block | Same as above. |
ambient-light-sensor=() | Block | Can be used to infer physical environment characteristics. |
UI and Media APIs
| Feature Directive | Default Stance | Rationale |
|---|---|---|
payment=() | Block | Disable unless you utilize the W3C Payment Request API (Apple Pay/Google Pay integrations). |
display-capture=() | Block | Screen sharing API. A massive privacy risk if abused by XSS. |
encrypted-media=() | Block | DRM (Digital Rights Management). Only needed for premium streaming platforms. |
fullscreen=(self) | Restrict | Allow your site to trigger fullscreen, but prevent malicious iframes from trapping the user. |
picture-in-picture=(self) | Restrict | Allow your own videos to pop out, block third parties. |
Legacy and Destructive Features
| Feature Directive | Default Stance | Rationale |
|---|---|---|
document-domain=() | Block | Legacy feature that historically weakened the Same-Origin Policy. Explicitly blocking this is highly recommended for modern isolation. |
sync-xhr=() | Block | Synchronous XHR blocks the main thread, leading to terrible UX. Blocking it enforces modern asynchronous practices. |
Example Baseline Policy String
A strong, general-purpose starting policy looks like this:
Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(self), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()
Inspecting Policies and Browser Behavior
When a feature is successfully blocked, the browser handles the rejection silently or throws a catchable exception, depending on the API.
Here is a look at what happens in the browser console when an attacker (or a misconfigured script) attempts to use a blocked feature.
========================================================================
🌐 Web Inspector: Console
------------------------------------------------------------------------
> navigator.geolocation.getCurrentPosition(console.log, console.error)
[Error] Geolocation has been disabled in this document by Permissions-Policy.
[Error] DOMException: User denied Geolocation
> navigator.mediaDevices.getUserMedia({ video: true })
[Error] Uncaught (in promise) DOMException: Failed to execute 'getUserMedia'
on 'MediaDevices': Document is not allowed to use the camera feature.
========================================================================
Technical Implementation: Server Configurations
Deploying the Permissions-Policy header requires configuring your web server, reverse proxy, or application framework to inject the HTTP response header on every relevant document request.
Nginx
In Nginx, utilize the add_header directive. We recommend using the always flag so the header is included even on error pages (like 404 or 500), which are occasionally targeted by XSS.
# /etc/nginx/conf.d/security.conf
server {
listen 443 ssl http2;
server_name example.com;
# Apply strict Permissions-Policy
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=(), ambient-light-sensor=(), display-capture=(), document-domain=()" always;
location / {
proxy_pass http://localhost:3000;
}
}
Apache
In Apache, use the Header directive provided by mod_headers.
# /etc/apache2/sites-available/000-default.conf
<VirtualHost *:443>
ServerName example.com
<IfModule mod_headers.c>
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), document-domain=()"
</IfModule>
</VirtualHost>
Node.js (Express)
For Express applications, you can write a simple middleware, or use the popular helmet security package.
Manual Middleware:
const express = require('express');
const app = express();
app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(self), payment=(), usb=(), document-domain=()');
next();
});
app.get('/', (req, res) => res.send('Secure App'));
app.listen(3000);
Next.js
In Next.js, define the headers in your next.config.js file to ensure they are statically injected during server-side rendering or static generation.
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=(), ambient-light-sensor=(), display-capture=(), document-domain=()'
}
],
},
];
},
};
Validating the Configuration
Once deployed, you should verify the header is being successfully transmitted using command-line tools.
$ curl -I https://your-secure-site.com
HTTP/2 200
server: nginx
content-type: text/html; charset=utf-8
strict-transport-security: max-age=31536000; includeSubDomains; preload
content-security-policy: default-src 'self'; script-src 'self'
x-frame-options: DENY
x-content-type-options: nosniff
permissions-policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), document-domain=()
Notice how permissions-policy is bundled with other critical security headers like CSP and HSTS. This layered approach is vital for comprehensive security.
Advanced Implementations and Troubleshooting
Permissions-Policy in Iframes
The HTTP header establishes the absolute upper limit of what is allowed. If the HTTP header sets camera=(), no iframe can ever use the camera, regardless of its HTML attributes.
However, if the HTTP header allows a feature for a third party (e.g., camera=(self "https://video-vendor.com")), you must also explicitly delegate that permission within the HTML markup using the allow attribute.
<!-- The HTTP header MUST allow https://video-vendor.com for this to work -->
<iframe
src="https://video-vendor.com/embed/123"
allow="camera; microphone"
></iframe>
If the allow attribute is missing, the iframe will be denied access, even if the HTTP header permits it. This double-layer requires explicit intent from both the backend server (the header) and the frontend developer (the markup).
Reporting Mode (Coming Soon/Draft)
Similar to Content-Security-Policy-Report-Only, there is ongoing work on a Permissions-Policy-Report-Only header. This allows administrators to observe policy violations without breaking functionality, which is highly useful when retrofitting strict policies onto large, legacy web applications.
Currently, observability can be achieved using the Reporting API and the Reporting-Endpoints header, hooking into the permissions-policy-violation report type.
Best Practices Checklist
- Deny by Default: Start by blocking every API your site does not explicitly require.
- Review Third-Party Needs: Audit your embeds (YouTube, Vimeo, Ads, Chat widgets). Determine what APIs they genuinely need and allowlist ONLY those origins.
- Migrate from Feature-Policy: If your web server is still returning
Feature-Policy: camera 'none', update your configuration immediately to the newPermissions-Policy: camera=()syntax. - Kill Legacy Features: Always include
document-domain=()to enforce strict origin isolation and prevent older DOM-based exploits. - Regular Audits: As new web APIs are introduced into browser standards, update your Permissions-Policy to explicitly block them.
Frequently Asked Questions (FAQ)
1. Is Permissions-Policy supported in all browsers? It is supported in all modern Chromium-based browsers (Chrome, Edge, Opera) and Safari. Firefox currently has partial support and is actively implementing the standard. Unsupported browsers simply ignore the header.
2. What happens if I make a syntax error in the header? If the syntax is invalid (e.g., missing parentheses or using the old Feature-Policy space separation), the browser will typically fail to parse the header and ignore it entirely, falling back to default browser behaviors (which usually means prompting the user for permission).
3. Do I need this if my site is just a static blog? Absolutely. Even a static blog can be targeted by Cross-Site Scripting (XSS) via a vulnerability in a comment system, a compromised ad network, or a malicious third-party analytics script. Blocking APIs proactively protects your readers.
4. Can I use wildcards for subdomains?
No. Permissions-Policy does not support wildcard subdomains like https://*.example.com in the allowlist. You must explicitly list each subdomain: (self "https://api.example.com" "https://chat.example.com").
5. How does this impact Web3 and cryptocurrency wallets? Many Web3 decentralized applications (dApps) rely on specific browser extensions and APIs. If your dApp requires specific hardware access or unique cryptography APIs, ensure they are explicitly allowed. Otherwise, keep standard hardware APIs blocked.
6. Does Permissions-Policy replace Content Security Policy (CSP)? No. CSP prevents unauthorized scripts from executing and controls resource loading. Permissions-Policy controls what authorized (or successfully injected) scripts can do regarding browser APIs. They are complementary mechanisms.
7. How do I test my Permissions-Policy? You can use browser developer tools (Network tab) to inspect the response headers. Additionally, security scanners like SecHead, Mozilla Observatory, or securityheaders.com can validate the syntax and presence of the header.
8. What is the document-domain feature?
Setting document.domain is an old technique that allowed subdomains (e.g., a.example.com and b.example.com) to bypass the Same-Origin Policy and access each other's DOM. It is a massive security risk. Setting document-domain=() disables this entirely.
9. Can I block JavaScript execution using Permissions-Policy? No, JavaScript execution is controlled via CSP or the sandbox attribute on iframes. Permissions-Policy only dictates which specific hardware and browser APIs that JavaScript is allowed to call.
10. What is sync-xhr and why block it?
Synchronous XMLHttpRequest halts the main thread of the browser until the HTTP request completes, freezing the entire UI. Blocking it forces developers to use modern, asynchronous fetch patterns, improving application performance and UX.
11. Can users override the Permissions-Policy? No. If the server delivers a strict Permissions-Policy, the browser enforces it at a structural level. The user cannot go into browser settings and override a site's self-imposed restriction.
12. Is Permissions-Policy case-sensitive?
HTTP headers are generally case-insensitive, but the feature names inside the directive (like camera, usb) must match the specification strictly. It's best practice to use all lowercase for feature names.
Conclusion
The Permissions-Policy header represents a significant step forward in giving web developers and security engineers granular control over their application environments. By explicitly turning off powerful browser APIs that are not needed, you drastically reduce the attack surface of your application, protect users from invasive fingerprinting, and ensure third-party scripts remain securely compartmentalized.
Implement a strict baseline today, audit it regularly, and integrate it as a core component of your web application firewall and proxy configurations.
Related Security Guides
Continue your journey into web security with these related, deep-dive articles from the SecHead team:
- X-Content-Type-Options: Stopping MIME Sniffing
- Access-Control-Allow-Origin: CORS Explained
- Secure File Download Headers
SEO Metadata
- Meta Title: Permissions-Policy Header Explained: Complete Guide & Best Practices
- Meta Description: Learn how the Permissions-Policy (formerly Feature-Policy) header secures your web app by controlling browser APIs. Includes setup guides for Nginx, Apache, and Node.
- URL Slug: /blog/permissions-policy-explained
- Keywords: Permissions-Policy, Feature-Policy, Browser Features, API Access Control, Web Security, SecHead, Security Headers
Related articles
Free tool
Check your own security headers
Instant grade, plain-language explanations, and a full remediation plan - no signup needed.
Scan your site now →