# Permissions-Policy: turning off camera, microphone and friends

> One response header switches off browser features your site never uses, for your own scripts and every embedded iframe. The syntax, a sensible default per site type, server configs and a test.

Updated 2026-09-25 · Security · HTML version: https://getreport.app/guides/permissions-policy-turning-off-camera-microphone-and-friends

A browser can open the camera, read the location, take a payment, go fullscreen or talk to a USB device, but only when a page asks. The `Permissions-Policy` header lets you say in advance which of those your site will never ask for, so that a script you did not write, or an iframe you do not control, cannot ask either. It is one line, it breaks nothing on a site that does not use the features, and this guide gives you the line for your kind of site.

## Quick answer

- Send `Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()` on every page. `()` means "nobody, not even this page".
- Keep a feature on for yourself with `(self)`, and for one specific embed with a quoted origin: `payment=(self "https://checkout.example")`.
- The header controls the top page and every iframe in it. An iframe additionally needs `allow="…"` on the `<iframe>` tag for anything the header permits it.
- It does not replace `Content-Security-Policy`; CSP decides *what loads*, Permissions-Policy decides *what loaded code may do*.
- The [security headers checker](https://getreport.app/tools/security-headers) reports the header as present or missing; the value is yours to get right.

## Why Permissions-Policy matters

Every script that runs in your page, including the analytics tag, the chat widget, the A/B testing snippet and whatever those load in turn, runs with the same powers as your own code. If a third-party script is compromised (it happens: tag managers, ad networks and abandoned widgets have all been hijacked), it can call `getUserMedia()` and the browser will show your visitors a permission prompt with *your* domain on it. Some visitors click "Allow".

Iframes are the other route. A cross-origin iframe cannot use the camera or location on its own; the top page has to grant it with the `allow` attribute. But the default allowlist for most features is `self`, which means your own origin, and your own origin is exactly where injected scripts run.

The header closes both routes with one statement: "this site does not use these features, at all". After that, the call fails, no prompt appears, and the browser logs a policy violation in the console instead. For a site that never intends to use the camera, there is no downside.

It also documents intent for the people who come after you. A `payment=(self "https://checkout.example")` line tells the next developer exactly which embed is allowed to open the Payment Request API, which is more than most sites can say about their third-party scripts.

## How getReport checks it

> **Free tool:** [Security headers checker](https://getreport.app/tools/security-headers): Check HSTS, CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy and cookie flags on any site. Free, no signup, with a fix for every missing header.

The checker fetches the page like a browser, follows redirects and looks for a `Permissions-Policy` response header on the final page. The check is a presence check: any non-empty value passes, and the evidence line shows the value so you can read what is actually being sent. It does not grade the directives, because there is no universally right list; a video-call app needs `camera=(self)` and a blog needs `camera=()`. The rest of this guide is about choosing the value.

> **Check: Permissions-Policy header is set.** The header switches off browser features this page never uses, such as camera, microphone or geolocation, so an injected script or a third-party iframe cannot turn them on.
>
> 1. Send the header: Permissions-Policy: camera=(), microphone=(), geolocation=() and list any feature you use with (self).

The panel shows the header table with a row per header, marked where a header is missing or weak:

![The security headers panel with the Permissions-Policy row: the header table lists Strict-Transport-Security, Content-Security-Policy, X-Frame-Options, Referrer-Policy and Permissions-Policy with the value each one was sent with, and the missing Permissions-Policy row marked](https://getreport.app/guides/img/permissions-policy-turning-off-camera-microphone-and-friends/headers-panel.webp "A missing header is one finding; the evidence line under it shows the value the server sent, or (absent).")

Two neighbours in the same panel are part of the same idea, limiting what a page can do to visitors:

> **Check: Other sites cannot embed this page in a frame.** A site that loads your page in an invisible frame can trick visitors into clicking your buttons (clickjacking), for example "confirm order" or "delete account".
>
> 1. Add frame-ancestors 'self' to your Content-Security-Policy, or send X-Frame-Options: SAMEORIGIN.
> 2. Use frame-ancestors 'none' (or X-Frame-Options DENY) if the page never needs to be embedded.

> **Check: Content-Security-Policy header is set.** A CSP blocks most cross-site scripting attacks by listing where scripts may load from. Without one, a single injected script can steal sessions or card data.
>
> 1. Start in report-only mode with Content-Security-Policy-Report-Only to see what would break.
> 2. Move to an enforcing policy once the report is quiet; keep "unsafe-inline" out of script-src.

Framing protection is about other sites embedding *you*; Permissions-Policy is about what you and the sites you embed may do. CSP is about which code is allowed to load in the first place. A site with all three has covered load, capability and embedding.

## The syntax

The header is a comma-separated list of `feature=(allowlist)` entries:

```text
Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=(self "https://checkout.example"), fullscreen=*
```

Allowlist values:

| Allowlist | Meaning |
| --- | --- |
| `()` | Nobody. The top page and every iframe are denied. |
| `(self)` | The page's own origin, and same-origin iframes. Cross-origin iframes are denied. |
| `(self "https://a.example" "https://b.example")` | Own origin plus the quoted origins, if their `<iframe>` also carries `allow`. |
| `*` (no parentheses) | Any origin, including every iframe. Rarely what you want. |

Origins are quoted, with the scheme, no path, no trailing slash. A feature you do not mention keeps its browser default, which for most features is `self`. Unknown feature names are ignored, so listing a feature the browser does not know is harmless.

### The features people actually set

- `camera`, `microphone`: video and audio capture via `getUserMedia()`.
- `geolocation`: the Geolocation API. Keep `(self)` if you show "stores near me".
- `payment`: the Payment Request API (Apple Pay, Google Pay buttons in the browser). Shops allow their checkout provider's origin.
- `usb`, `serial`, `bluetooth`, `hid`: hardware access. Almost no site needs these.
- `fullscreen`: `requestFullscreen()`. Video embeds (YouTube, Vimeo) need it, and their `<iframe>` code includes `allow="fullscreen"` for that reason; a header value of `fullscreen=()` breaks their fullscreen button.
- `autoplay`: media playing without a click. Video embeds ask for it too.
- `display-capture`: screen sharing.
- `browsing-topics`: opts your page out of Chrome's Topics API for interest-based advertising. `interest-cohort=()`, which you will find in older configs, targeted FLoC, which was withdrawn in 2022; the entry is ignored and can be deleted.

### Permissions-Policy vs Feature-Policy

`Feature-Policy` was the first version of this header, with a different syntax (`Feature-Policy: camera 'none'; geolocation 'self'`). Chrome renamed it and changed the syntax to structured fields in version 88 (January 2021). If a config sends both, browsers that understand both apply the newer one. There is no need to keep the old header for a site that is being set up today; if you inherit one, replace it rather than maintaining two.

### Browser support

Chrome and Edge honour the header since version 88. Firefox and Safari implement the underlying policy for iframes (the `allow` attribute) but their support for the response header itself has lagged; check the current state on [caniuse](https://caniuse.com/permissions-policy) before assuming a visitor's browser enforces it. This is a defence-in-depth header: it protects Chrome-family visitors today and costs nothing for the rest. The [MDN Permissions-Policy reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy) lists every directive with its support table.

## Step by step

### 1. List what your site uses

Search the site's own JavaScript and your third-party embeds for the APIs. A quick way for a content site: open the site, DevTools, Console, and run

```js
document.featurePolicy.allowedFeatures()
```

in Chrome. It prints every feature currently allowed for the page. Anything in that list you do not recognise as needed is a candidate for `()`.

Then look at the `<iframe>` tags on your pages: a YouTube embed carries `allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"` and a `allowfullscreen` attribute. Those are the features the embed will ask for, and the header must not deny them to the embed's origin if you want it to keep working.

### 2. Pick a starting value

**A content site** (blog, brochure, documentation):

```text
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), serial=(), bluetooth=(), display-capture=(), browsing-topics=()
```

Nothing here is used, so everything is off. `fullscreen` and `autoplay` are left at their defaults so video embeds keep working.

**A shop** with a hosted checkout that renders a payment button in an iframe:

```text
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(self "https://checkout.example"), usb=(), browsing-topics=()
```

Replace `https://checkout.example` with the origin of the payment provider's iframe (look at the `src` of the iframe on your checkout page). The provider's embed code will include `allow="payment"` on the iframe; if it does not, the header alone does not grant it.

**A site with a map or store finder** that asks for the visitor's location from its own code:

```text
Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=(), usb=(), browsing-topics=()
```

If the map is an embedded iframe from another origin that asks for location itself, add that origin: `geolocation=(self "https://www.google.com")`, and make sure the iframe has `allow="geolocation"`.

### 3. Set it on the server

**nginx**, in the `server` block:

```nginx
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), browsing-topics=()" always;
```

**Apache**, in `.htaccess` at the site root or in the virtual host (`mod_headers` enabled):

```apache
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), browsing-topics=()"
```

**Caddy**:

```caddy
example.com {
    header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), browsing-topics=()"
    reverse_proxy app:3000
}
```

**Cloudflare**: Rules, Transform Rules, Modify Response Header, "Set static", header name `Permissions-Policy` and the value above. Choose "Set", not "Add", so an origin that already sends one is overwritten rather than duplicated.

**Next.js**, in `next.config.js`:

```js
module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Permissions-Policy',
            value: 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), browsing-topics=()',
          },
        ],
      },
    ];
  },
};
```

**WordPress** without server access, as a must-use plugin in `wp-content/mu-plugins/permissions-policy.php`:

```php
<?php
/**
 * Plugin Name: Permissions-Policy header
 */
add_action('send_headers', function () {
    header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), browsing-topics=()');
});
```

PHP-set headers reach only pages PHP renders; a page served from a cache plugin's static file or from the host's cache may not get it. Prefer the server config where you can reach it.

### 4. Grant iframes explicitly

The header allowlist and the `<iframe allow>` attribute work together: the iframe gets a feature only if both permit it. For a checkout iframe:

```html
<iframe src="https://checkout.example/pay/48213"
        allow="payment"
        title="Payment"></iframe>
```

With `payment=(self "https://checkout.example")` in the header, this iframe may open the Payment Request API; any other iframe on the page may not, and neither may a script injected into your own page, because the header says `self` only for your origin and the checkout origin, and your own code never calls it.

> **Tip:**
> An iframe without `allow` inherits nothing beyond the browser default, which for most features is *deny cross-origin*. So a stray embed on a page cannot use the camera even before you add the header. The header's job is to close the `self` route: scripts running as your origin.

### 5. Test in the browser

Load a page with the header, open DevTools:

- **Application, Frames, top**: Chrome lists "Permissions Policy" with each feature marked allowed or disabled, and says which header or attribute disabled it.
- **Console**: try `navigator.mediaDevices.getUserMedia({ video: true })`. With `camera=()` the promise rejects with `NotAllowedError` and no prompt appears. Try `document.featurePolicy.allowsFeature('camera')`: it returns `false`.
- Embedded videos still go fullscreen, the payment button still renders, the store finder still asks for location. If any of those broke, the header denies a feature that embed needs; add the origin.

## Platform notes

**Shopify, Wix, Squarespace**: response headers are set by the platform and cannot be changed by you. Run the checker to see what is there; there is nothing to configure.

**Sites behind a tag manager**: the header is still set at the server or CDN. The tag manager cannot loosen it, which is the point.

**Video-call, kiosk or field-service apps** that genuinely use the camera: `camera=(self)` and `microphone=(self)` keep them working for your own code while still denying every iframe and, importantly, still documenting that use.

## Verify

1. Re-run the [security headers checker](https://getreport.app/tools/security-headers). The finding reads "Permissions-Policy header is set" and the evidence line shows your directives.
2. `curl -sI https://example.com/ | grep -i permissions-policy` prints the header once, with the value you configured. Two lines mean two sources (server and plugin, or origin and CDN); keep one.
3. In Chrome, DevTools, Application, Frames, top: camera and microphone show as disabled by the header.
4. Video embeds go fullscreen and the checkout still takes a payment. Test the checkout with a real transaction on a staging store or a refundable order.

## Common mistakes

- **`fullscreen=()` or `autoplay=()` on a site with video embeds.** The fullscreen button in YouTube and Vimeo players stops working. Leave those features at their defaults, or allow the player's origin.
- **Denying `payment` on a shop whose provider uses the Payment Request API.** Apple Pay and Google Pay buttons disappear from the checkout. Allow the provider's origin and keep `allow="payment"` on the iframe.
- **Copying the old `Feature-Policy` syntax into the new header.** `camera 'none'` is not valid `Permissions-Policy`; the browser ignores the malformed entry and the feature stays allowed. Use `camera=()`.
- **Setting the header only in `.htaccess` while a CDN or cache serves the page.** The response the visitor gets never touched Apache. Check with `curl` from outside and set it at the edge if needed.
- **Treating it as a substitute for CSP.** A script from a malicious origin still loads and runs; it just cannot open the camera. The header that stops it loading is CSP, rolled out in [Content-Security-Policy from report-only to enforced](https://getreport.app/guides/content-security-policy-rollout). For the rest of the set, and the order to add them in, see [Security headers from zero to A](https://getreport.app/guides/security-headers-from-zero) and, for the framing headers next to it in the panel, [X-Content-Type-Options, X-Frame-Options and frame-ancestors](https://getreport.app/guides/x-content-type-options-x-frame-options-frame-ancestors).
