Validates Permissions-Policy header for secure browser feature controls
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.
Read β collects every Permissions-Policy header line and joins them, as browsers do.
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.
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).
Aggregate β FAIL wins over WARNING; when every rule passes the audit is SUCCESS (200).
Specification pin β every run emits 37A-PermissionsPolicyAuditor-100 (INFORMATION), naming the W3C Permissions Policy Working Draft of 2026-09-22. It does not change the overall status.
Known feature names come from the Standardized, Proposed and Experimental sections of the W3C feature registry, 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 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.
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 |
|---|---|---|---|---|
37A-PermissionsPolicyAuditor-100 | INFORMATION | Always. Records the specification version this auditor implements. | β (recommendation is null; nothing for the site to change) | β |
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. |
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. |
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. |
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. |
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. |
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. |
37A-PermissionsPolicyAuditor-200 | SUCCESS | All rules passed. Message is Permissions-Policy on {url} sets {count} feature(s): {features}. | β | β |
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}. | β |
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. | While 37AuditsBot is blocked this audit cannot look at the site, so problems on it go unreported. |
| 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. |
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.
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.
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.
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, a developer documentation platform.
Add this auditor to your monitoring setup and start identifying issues on your websites today.