HTTP

Header

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.

6 min read intermediate Try in Playground

What is Permissions-Policy?

TL;DR: Controls which browser features and APIs can be used by your site and embedded content. Blocks access to camera, geolocation, and other sensitive features for security.

The Permissions-Policy header (formerly Feature-Policy) allows you to control which browser features and APIs can be used in your site and any embedded iframes. It’s a powerful security mechanism to prevent malicious scripts or third-party content from accessing sensitive features like camera, microphone, or geolocation.

Think of it as a security guard that says “these features are allowed on this site, and these third-party embeds can only use specific features we approve.”

How Permissions-Policy Works

Restrict camera and microphone access:

HTTP/1.1 200 OK
Content-Type: text/html
Permissions-Policy: camera=(), microphone=()

<!DOCTYPE html>
<html>...
```text

**Allow geolocation for specific origin:**

```http
HTTP/1.1 200 OK
Content-Type: text/html
Permissions-Policy: geolocation=(self "https://maps.example.com")

<!DOCTYPE html>
<html>...

Syntax

Permissions-Policy: <feature>=(<allowlist>)
Permissions-Policy: <feature1>=(<allowlist1>), <feature2>=(<allowlist2>)
```text

### Allowlist Values

- **`*`** - Allow for all origins (default for most features)
- **`self`** - Allow for same origin only
- **`()`** - Block for all origins (including same origin)
- **`"https://example.com"`** - Allow for specific origin

## Common Features

### Media Capture

```http
# Block camera access
Permissions-Policy: camera=()

# Block microphone access
Permissions-Policy: microphone=()

# Allow camera for same origin only
Permissions-Policy: camera=(self)

# Allow microphone for specific domains
Permissions-Policy: microphone=(self "https://video-chat.example.com")

Geolocation

# Block geolocation
Permissions-Policy: geolocation=()

# Allow for same origin only
Permissions-Policy: geolocation=(self)

# Allow for same origin and maps provider
Permissions-Policy: geolocation=(self "https://maps.googleapis.com")
```text

### Payment

```http
# Block payment request API
Permissions-Policy: payment=()

# Allow only on same origin
Permissions-Policy: payment=(self)

# Allow for payment provider
Permissions-Policy: payment=(self "https://checkout.stripe.com")

Autoplay

# Block autoplay
Permissions-Policy: autoplay=()

# Allow autoplay on same origin
Permissions-Policy: autoplay=(self)

# Allow autoplay for specific video provider
Permissions-Policy: autoplay=(self "https://www.youtube.com")
```text

### Fullscreen

```http
# Block fullscreen
Permissions-Policy: fullscreen=()

# Allow fullscreen on same origin
Permissions-Policy: fullscreen=(self)

# Allow fullscreen for video player
Permissions-Policy: fullscreen=(self "https://player.vimeo.com")

Complete Feature List

Sensor APIs

Permissions-Policy: accelerometer=(self)
Permissions-Policy: ambient-light-sensor=()
Permissions-Policy: gyroscope=(self)
Permissions-Policy: magnetometer=()
```http

### Media APIs

```http
Permissions-Policy: camera=()
Permissions-Policy: microphone=()
Permissions-Policy: speaker-selection=()
Permissions-Policy: display-capture=()

Privacy APIs

Permissions-Policy: geolocation=(self)
Permissions-Policy: usb=()
Permissions-Policy: serial=()
Permissions-Policy: bluetooth=()
```http

### Presentation APIs

```http
Permissions-Policy: fullscreen=(self)
Permissions-Policy: screen-wake-lock=()
Permissions-Policy: picture-in-picture=(self)

Other Features

Permissions-Policy: autoplay=(self)
Permissions-Policy: encrypted-media=(self)
Permissions-Policy: payment=(self)
Permissions-Policy: interest-cohort=()
Permissions-Policy: browsing-topics=()
```text

## Real-World Scenarios

### Secure Default Policy

```http
HTTP/1.1 200 OK
Content-Type: text/html
Permissions-Policy:
    camera=(),
    microphone=(),
    geolocation=(),
    payment=(),
    usb=(),
    interest-cohort=()

Blocks all sensitive features by default.

Video Conference Application

HTTP/1.1 200 OK
Content-Type: text/html
Permissions-Policy:
    camera=(self),
    microphone=(self),
    display-capture=(self),
    fullscreen=(self),
    autoplay=(self)
```text

Allows necessary media features for video calls.

### E-commerce Site

```http
HTTP/1.1 200 OK
Content-Type: text/html
Permissions-Policy:
    payment=(self "https://checkout.stripe.com"),
    geolocation=(self "https://maps.googleapis.com"),
    camera=(),
    microphone=(),
    interest-cohort=()

Allows payment and maps, blocks unnecessary features.

Blog with Embedded Videos

HTTP/1.1 200 OK
Content-Type: text/html
Permissions-Policy:
    autoplay=(self "https://www.youtube.com" "https://player.vimeo.com"),
    fullscreen=(self "https://www.youtube.com" "https://player.vimeo.com"),
    camera=(),
    microphone=(),
    geolocation=()
```text

Allows video features for embeds, blocks sensitive APIs.

### Privacy-Focused Site

```http
HTTP/1.1 200 OK
Content-Type: text/html
Permissions-Policy:
    interest-cohort=(),
    browsing-topics=(),
    camera=(),
    microphone=(),
    geolocation=(),
    payment=(),
    usb=(),
    serial=(),
    bluetooth=()

Blocks tracking and all sensitive features.

Server Configuration

Node.js (Express)

const express = require('express')
const app = express()

// Secure default policy
app.use((req, res, next) => {
  res.set(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=(), payment=(), interest-cohort=()'
  )
  next()
})

// Route-specific policy
app.get('/video-call', (req, res) => {
  res.set('Permissions-Policy', 'camera=(self), microphone=(self), display-capture=(self)')
  res.sendFile('video-call.html')
})

app.get('/checkout', (req, res) => {
  res.set('Permissions-Policy', 'payment=(self "https://checkout.stripe.com")')
  res.sendFile('checkout.html')
})
```javascript

### Helmet.js Middleware

```javascript
const helmet = require('helmet')

app.use(
  helmet.permissionsPolicy({
    features: {
      camera: ["'none'"],
      microphone: ["'none'"],
      geolocation: ["'self'"],
      payment: ["'self'", 'https://checkout.stripe.com'],
      fullscreen: ["'self'"],
      autoplay: ["'self'", 'https://www.youtube.com'],
      'interest-cohort': ["'none'"]
    }
  })
)

Nginx

# Global policy
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()";

# Location-specific policy
location /video-call {
    add_header Permissions-Policy "camera=(self), microphone=(self), display-capture=(self)";
}

location /checkout {
    add_header Permissions-Policy "payment=(self \"https://checkout.stripe.com\")";
}
```javascript

### Apache

```apache
# Global policy
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()"

# Directory-specific policy
<Directory "/var/www/html/video-call">
    Header always set Permissions-Policy "camera=(self), microphone=(self), display-capture=(self)"
</Directory>

<Directory "/var/www/html/checkout">
    Header always set Permissions-Policy "payment=(self \"https://checkout.stripe.com\")"
</Directory>

Python (Flask)

from flask import Flask, Response

app = Flask(__name__)

# Default policy
@app.after_request
def set_permissions_policy(response):
    response.headers['Permissions-Policy'] = 'camera=(), microphone=(), geolocation=(), interest-cohort=()'
    return response

# Route-specific policy
@app.route('/video-call')
def video_call():
    response = Response(render_template('video-call.html'))
    response.headers['Permissions-Policy'] = 'camera=(self), microphone=(self), display-capture=(self)'
    return response

@app.route('/checkout')
def checkout():
    response = Response(render_template('checkout.html'))
    response.headers['Permissions-Policy'] = 'payment=(self "https://checkout.stripe.com")'
    return response
```text

## HTML Equivalent

Permissions-Policy can also be set via iframe attributes:

### HTTP Header

```http
Permissions-Policy: camera=(self "https://trusted.com")

iframe allow attribute

<iframe src="https://trusted.com/widget" allow="camera 'self' https://trusted.com"></iframe>
```text

The HTTP header sets the policy for the entire page, while `allow` attribute controls individual iframes.

## Migration from Feature-Policy

Permissions-Policy replaces the deprecated Feature-Policy header:

### Old (Feature-Policy)

```http
Feature-Policy: camera 'self'; microphone 'none'; geolocation 'self' https://maps.example.com

New (Permissions-Policy)

Permissions-Policy: camera=(self), microphone=(), geolocation=(self "https://maps.example.com")
```text

**Key Differences:**

- Semicolon (`;`) → Comma (`,`)
- Space-separated → Parentheses with allowlist
- `'none'` → `()`
- `'*'` → `*`

## Best Practices

### 1. Block Unnecessary Features

```http
# ✅ Block features you don't use
Permissions-Policy: camera=(), microphone=(), usb=(), serial=()

# ❌ Allow everything by default
Permissions-Policy: camera=*, microphone=*, usb=*

2. Use Principle of Least Privilege

# ✅ Only allow what's needed
Permissions-Policy: payment=(self "https://checkout.stripe.com")

# ❌ Too permissive
Permissions-Policy: payment=*
```text

### 3. Block Tracking Features

```http
# ✅ Opt out of FLoC/Topics
Permissions-Policy: interest-cohort=(), browsing-topics=()

4. Audit Third-Party Embeds

# ✅ Explicitly list allowed origins
Permissions-Policy: autoplay=(self "https://www.youtube.com" "https://player.vimeo.com")

# ❌ Allow all origins
Permissions-Policy: autoplay=*
```javascript

### 5. Test Before Deploying

```javascript
// Test in development
app.use((req, res, next) => {
  if (process.env.NODE_ENV === 'development') {
    console.log('Permissions-Policy:', res.get('Permissions-Policy'))
  }
  next()
})

Common Patterns

Public Website

Permissions-Policy:
    camera=(),
    microphone=(),
    geolocation=(),
    payment=(),
    usb=(),
    interest-cohort=()
```text

### Video Chat App

```http
Permissions-Policy:
    camera=(self),
    microphone=(self),
    display-capture=(self),
    fullscreen=(self)

Banking App

Permissions-Policy:
    camera=(),
    microphone=(),
    geolocation=(self),
    payment=(self),
    usb=(),
    serial=(),
    bluetooth=(),
    interest-cohort=()
```text

### News Site with Ads

```http
Permissions-Policy:
    autoplay=(self "https://ads.example.com"),
    camera=(),
    microphone=(),
    geolocation=(),
    interest-cohort=()

Testing Permissions-Policy

Browser DevTools

  1. Open DevTools (F12)
  2. Go to Console
  3. Try accessing restricted feature:
// Will fail if camera=()
navigator.mediaDevices.getUserMedia({ video: true })
```text

### Security Headers Checker

```bash
curl -I https://example.com | grep -i permissions-policy

Online Tools

Browser Support

Permissions-Policy is supported in:

  • Chrome 88+
  • Edge 88+
  • Firefox 74+ (partial support)
  • Safari 15.4+ (partial support)

Feature-Policy is deprecated but still works for backward compatibility.

Common Issues

Conflicting Policies

# Header
Permissions-Policy: camera=(self)

# iframe attribute overrides
<iframe src="..." allow="camera 'none'"></iframe>
```text

iframe `allow` attribute can only further restrict, not loosen.

### Incorrect Syntax

```http
# ❌ Wrong syntax (using old Feature-Policy style)
Permissions-Policy: camera 'self'; microphone 'none'

# ✅ Correct syntax
Permissions-Policy: camera=(self), microphone=()

Missing Quotes for Origins

# ❌ Missing quotes for specific origin
Permissions-Policy: camera=(self https://example.com)

# ✅ Quotes required for specific origins
Permissions-Policy: camera=(self "https://example.com")
```text

## Security Considerations

### Defense in Depth

Permissions-Policy is one layer of security:

```http
# Combine with other security headers
Permissions-Policy: camera=(), microphone=()
Content-Security-Policy: default-src 'self'
X-Frame-Options: DENY

Iframe Sandboxing

<!-- Further restrict iframes -->
<iframe
  src="https://third-party.com"
  sandbox="allow-scripts allow-same-origin"
  allow="autoplay 'self'"></iframe>

Frequently Asked Questions

What is Permissions-Policy?

Permissions-Policy controls which browser features your page and embedded iframes can use. It can disable geolocation, camera, microphone, and other powerful APIs.

What happened to Feature-Policy?

Feature-Policy was renamed to Permissions-Policy. The syntax changed from Feature-Policy: geolocation none to Permissions-Policy: geolocation=().

How do I disable a feature?

Use empty parentheses to disable: Permissions-Policy: geolocation=(), camera=(). This prevents your page and all iframes from using these features.

How do I allow features for specific origins?

List origins in parentheses: Permissions-Policy: geolocation=(self "https://maps.example.com"). self allows your origin, quoted URLs allow specific others.

Keep Learning