> ## Documentation Index
> Fetch the complete documentation index at: https://docs.37audits.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Permissions Policy Auditor

> Checks that the Permissions-Policy header restricts camera, microphone, location and other sensitive browser features

<Info>
  **Security Auditor** — checks that the `Permissions-Policy` response header is present, parses, and keeps sensitive browser features away from code you do not control.
</Info>

<CardGroup cols={2}>
  <Card title="Security" icon="shield">
    Stops injected scripts and embedded frames from asking visitors for their camera, microphone or location.
  </Card>

  <Card title="Standards" icon="book">
    Follows the W3C Permissions Policy specification and the W3C feature registry.
  </Card>
</CardGroup>

The `Permissions-Policy` header tells the browser which features (camera, microphone, geolocation, payment, USB and many more) the page and the frames it embeds may use. This auditor reads that header from the audited URL and checks that it is valid and that sensitive features are not left open.

## How it works

1. **Fetch** — sends a `HEAD` request (or `GET` when `HEAD` is not supported) to the audited URL as 37AuditsBot. A blocked request ends the audit with `590`; any other failure (timeout, DNS, TLS, non-2xx answer) ends it with `500`.
2. **Read** — collects every `Permissions-Policy` header line and joins them, as browsers do.
3. **Parse** — splits the header into members and reads each as `feature=allowlist`. Both forms the specification allows are accepted: an inner list such as `camera=(self "https://a.example")` and a single item such as `camera=*`, `camera=self` or `camera="https://a.example"`. Parameters after `;` (for example `;report-to=main`) are ignored.
4. **Rules** — a missing header stops the audit with `400`. Otherwise every rule runs: malformed members (`401`), sensitive features open to every origin (`402`), unknown feature names (`300`), expected sensitive features left unset (`301`) and retired features (`302`).
5. **Aggregate** — `FAIL` wins over `WARNING`; when every rule passes the audit is `SUCCESS` (`200`).
6. **Specification pin** — every run emits `37A-PermissionsPolicyAuditor-100` (`INFORMATION`), naming the [W3C Permissions Policy Working Draft of 2026-09-22](https://www.w3.org/TR/2026/WD-permissions-policy-1-20260922/). It does not change the overall status.

Known feature names come from the Standardized, Proposed and Experimental sections of the [W3C feature registry](https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md), plus features Chromium ships that the registry does not list. Any `ch-*` name (Client Hints delegation, such as `ch-dpr` or `ch-save-data`) is known. Names in the registry's [Retired Features](https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md#retired-features) section get their own warning instead.

Sensitive features (`402`): `camera`, `microphone`, `geolocation`, `payment`, `usb`, `bluetooth`, `serial`, `hid`, `clipboard-read`, `clipboard-write`, `display-capture`. Expected features (`301`): `camera`, `microphone`, `geolocation`, `payment`.

## What it audits

Every row maps to one `checkRule*` method and one `CheckCode`, in the order the auditor runs them. Rules `401`–`302` are skipped when the header is missing (`400`). CheckCode suffixes use status ranges: `100` information, `200` success, `300–399` warning, `400–499` fail, `500–599` error.

| CheckCode | Status | When it fires | Recommendation | Why it matters |
| - | - | - | - | - |
| <a id="37A-PermissionsPolicyAuditor-100" />`37A-PermissionsPolicyAuditor-100` | `INFORMATION` | Always. Records the specification version this auditor implements. | — (recommendation is `null`; nothing for the site to change) | — |
| <a id="37A-PermissionsPolicyAuditor-400" />`37A-PermissionsPolicyAuditor-400` | `FAIL` | The page sends no `Permissions-Policy` header, or an empty one. | Add "Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()" to the responses of `{url}`, and allow only the features the page uses. | Without this header any script or embedded frame on the page can ask visitors for their camera, microphone or location, so one compromised widget can spy on them under your brand. |
| <a id="37A-PermissionsPolicyAuditor-401" />`37A-PermissionsPolicyAuditor-401` | `FAIL` | A header member is not `feature=allowlist` syntax (one check per member). | Rewrite "`{member}`" as name=(allowlist), for example camera=(self) or camera=(); browsers drop a header that does not parse. | Browsers ignore a Permissions-Policy header they cannot parse, so the protection you meant to set is not applied at all. |
| <a id="37A-PermissionsPolicyAuditor-402" />`37A-PermissionsPolicyAuditor-402` | `FAIL` | A sensitive feature's allowlist contains `*` (one check per feature). | Restrict `{feature}` to self or the origins that need it, for example `{feature}`=(self), or disable it with `{feature}`=(). | Allowing every origin lets any embedded frame, including ads and third-party widgets, ask visitors for this access under your site's name. |
| <a id="37A-PermissionsPolicyAuditor-300" />`37A-PermissionsPolicyAuditor-300` | `WARNING` | A feature name is neither known nor retired (one check per name). | Check the spelling of "`{feature}`", or remove it; browsers ignore features they do not know. | Browsers ignore features they do not know, so a misspelled name leaves the feature you meant to restrict open. |
| <a id="37A-PermissionsPolicyAuditor-301" />`37A-PermissionsPolicyAuditor-301` | `WARNING` | One of `camera`, `microphone`, `geolocation` or `payment` is not set (one check per feature). | Add `{feature}`=() to Permissions-Policy, or `{feature}`=(self) if the page uses it. | An unset feature keeps the browser default, so scripts on the page, including third-party ones, can still ask visitors for that access under your name. |
| <a id="37A-PermissionsPolicyAuditor-302" />`37A-PermissionsPolicyAuditor-302` | `WARNING` | The header sets a retired feature: `document-domain` or `window-placement` (one check per feature). | `window-placement`: Rename window-placement to window-management (window-management=`{allowlist}`). `document-domain`: Remove document-domain=`{allowlist}` from Permissions-Policy; it has no effect. | Browsers no longer read a retired feature, so the restriction it was meant to apply is not in effect. |
| <a id="37A-PermissionsPolicyAuditor-200" />`37A-PermissionsPolicyAuditor-200` | `SUCCESS` | All rules passed. Message is `Permissions-Policy on {url} sets {count} feature(s): {features}.` | — | — |
| <a id="37A-PermissionsPolicyAuditor-500" />`37A-PermissionsPolicyAuditor-500` | `ERROR` | The page could not be fetched (timeout, DNS, TLS, or a non-2xx answer other than a block). | Make sure `{url}` answers with HTTP 2xx, then run the audit again. Last error: `{error}`. | — |
| <a id="37A-PermissionsPolicyAuditor-590" />`37A-PermissionsPolicyAuditor-590` | `ERROR` | The audited page answered HTTP 403, HTTP 429 or a Cloudflare challenge, so 37AuditsBot was blocked before it could read the page. | Allow 37AuditsBot in your firewall or bot protection: allowlist its user agent, or verify its Web Bot Auth signature. See [https://www.37audits.com/bot](https://www.37audits.com/bot). | While 37AuditsBot is blocked this audit cannot look at the site, so problems on it go unreported. |

## Output documentation

| Status | Description | Test logic |
| - | - | - |
| **SUCCESS** | The header is valid and restricts sensitive features | Header present, every member parses, no sensitive feature allows `*`, every name is known, `camera`, `microphone`, `geolocation` and `payment` are set (`200`). |
| **WARNING** | The header works but leaves gaps | Unknown feature name (`300`); expected sensitive feature not set (`301`); retired feature (`302`). |
| **FAIL** | The header is missing, broken or too open | Missing or empty header (`400`); malformed member (`401`); sensitive feature open to every origin (`402`). |
| **ERROR** | The auditor could not read the page | Fetch failure or non-2xx answer (`500`); site blocked 37AuditsBot (`590`). |
| **INFORMATION** | Specification pin | Always emitted (`100`); does not change overall status. |

## Risks and considerations

### Why the header matters

* Without a policy, the page and every frame it embeds with an `allow` attribute can request powerful features. An injected script or a compromised third-party widget can then prompt visitors for their camera, microphone or location, and the prompt carries your site's name.
* `feature=()` disables a feature everywhere on the page; `feature=(self)` allows it only for your own origin. Allow the minimum the page needs.

### Parsing is all-or-nothing

* The header is a Structured Fields dictionary. A browser that cannot parse it drops the whole header, so one malformed member removes every restriction in it.

### Feature names change

* Browsers silently ignore unknown names, so a typo leaves the feature at its default. Retired names such as `window-placement` (renamed `window-management` in Chrome 111) no longer have any effect.
* The known-feature list is a copy of the W3C registry. A brand-new feature may be reported as unknown (`300`) until the list is refreshed.

### What this auditor does not check

* The legacy `Feature-Policy` header and the `allow` attribute on iframes are not read.
* Only the audited URL is fetched; other pages of the site may send a different policy.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.