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.

By How HTTP WorksReview process6 min readintermediateTry 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