Permissions-Policy Header Generator

Developers hardening a site or an embed reach for this when a third-party iframe goes on the page or a security review asks which response headers are actually shipping. The header tells the browser which capabilities each origin may use at runtime, and browser capability leaks through embeds show up as real findings in audits. Tick directives or load a preset, and the widget below assembles a validated header value you can paste into your server or edge config. A parser panel on the same card checks a header you already serve.

Presets
The header rebuilds on every change.
Deliver this value as an HTTP response header, never as a meta tag.

Parse an existing header

Paste a Permissions-Policy value from a live response. The panel lists recognized directives and reports malformed entries.

How It Works

The page ships a catalog of 29 directives running from accelerometer through xr-spatial-tracking, each carrying a flag for whether it accepts an allowlist and, where one exists, the old Feature-Policy name. Checking a box adds that directive to the selected map, and the dropdown beside it contributes one token: self, none, or star. Assembly walks the catalog in fixed order and prints directive=(allowlist) joined by commas, so the output stays stable regardless of the order you clicked. Tokens pass through validation where only star, self, none, src, or an http(s) origin are accepted, and an origin may not carry a path, query string, or fragment. An empty list or a choice of none collapses to (), self is written as quoted 'self', and full URLs are wrapped in double quotes.

The parser splits a pasted header on commas that sit outside parentheses, then requires every chunk to carry an = sign and a parenthesized allowlist. Unknown directive names and entries missing parentheses are reported with the same messages a browser would effectively ignore. For migration work, buildLegacyFeaturePolicy runs your selection through a rename pass that swaps in the legacy names before you ship a Feature-Policy header. Delivery is the step people miss: no meta tag supports this header, so the value has to be attached by the server, an edge worker, or the hosting config on every response you care about.

Before and After

Builder output for a selected map
selected = {
  camera: ['self'],
  microphone: ['self'],
  geolocation: ['none'],
  usb: ['none']
}

buildPermissionsPolicy(selected).header
// camera=('self'), microphone=('self'),
// geolocation=(), usb=()
Parser and token errors on bad input
input:
  camera, microphone=(ftp://mic.example.com)

parsePolicyHeader errors:
  Directive "camera" must have an allowlist
  in parentheses.

validateAllowToken("ftp://mic.example.com"):
  "ftp://mic.example.com" must be *, 'self',
  'none', 'src', or an http(s) origin like
  https://example.com

Use Cases

Developer Production Workflow
Locking down a page that embeds third-party iframe demos Passing security audits that flag camera and mic access for embeds Shipping the copied value through a Cloudflare Pages _headers file
Building a video-call page that allows the microphone for self only Keeping sensors and display capture denied on marketing pages Re-checking the live header with the parser after each deploy
Renaming a Feature-Policy header during a stack migration Working through a compliance hardening checklist before launch Diffing lockdown against media presets to review what changed

Common Mistakes and Troubleshooting

  • Setting the header with a meta http-equiv tag. Browsers ignore Permissions-Policy in HTML, so the fix is a real response header from the server, edge worker, or host config.
  • Forgetting self in the allowlist. camera=("https://partner.example.com") with no self entry kills your own pages’ camera access along with everyone else’s, so add 'self' before you deploy.
  • Mixing Feature-Policy into the Permissions-Policy header. The old name belongs to the old header; use the legacy builder for a Feature-Policy value and keep the new name for the new header.
  • Pasting origins with trailing paths. The token https://x.com/app is rejected with “Origins must not contain a path, query string or fragment.” Trim it to https://x.com.
  • Assuming star is safe for camera. A wildcard opens the camera to every origin, including ad and analytics frames you did not review; reserve star for capabilities you truly do not mind sharing.
  • Not testing embeds after a change. A widget can silently lose geolocation or fullscreen, so load each embedded page in devtools and watch the console for permission errors.

Best Practices

  • Start from the lockdown preset and open capabilities one at a time as a real feature needs them, rather than beginning permissive and tightening later.
  • Keep camera, microphone, and the motion sensors denied for cross-origin frames on every page that does not deliberately grant them.
  • During migration, run the legacy builder to emit Feature-Policy alongside the new header, then drop the old one once your traffic no longer requires it.
  • Paste your live header into the parser panel after each deploy so a truncated _headers line or a CDN rewrite shows up immediately.
  • Pair the header with Content-Security-Policy: CSP handles which resources load, while Permissions-Policy handles which capabilities run.
  • Read Chrome’s release notes on deprecated directives before you copy an old snippet; dropped names in a live header are ignored without warning.

Frequently Asked Questions

How is Permissions-Policy different from Content-Security-Policy?

Content-Security-Policy governs which resources and scripts a document may load, covering script sources, frame hosts, and connection endpoints. Permissions-Policy answers a different question: which browser capabilities, such as camera, microphone, and geolocation, a given origin may use at runtime. A page can pass a strict CSP and still let an embedded frame open the webcam when Permissions-Policy is absent. The two headers cover separate layers and work best together on the same response.

What does the allowlist syntax mean?

Each directive takes a parenthesized list of origins, as in camera=(self "https://x.com"). The token self becomes 'self' with quotes in the final header, * means every origin, and an empty list () denies the capability for everyone, which equals 'none'. Full URLs are wrapped in double quotes when assembled. The builder rejects anything that is not *, self, none, src, or a plain http(s) origin, and origins carrying a path, query string, or fragment are flagged as errors.

Can I migrate an old Feature-Policy header?

Yes. Feature-Policy was renamed to Permissions-Policy: most directive names carried over unchanged, a few were renamed, and some were dropped by browsers. The buildLegacyFeaturePolicy function in this site’s toolkit takes the same selected map and emits a legacy header with the old names, which helps while you still serve clients that only read Feature-Policy. Run both headers during the transition, then retire Feature-Policy once traffic no longer needs it.

Where do I set the header?

Permissions-Policy must arrive as an HTTP response header. A meta http-equiv tag is ignored for this header, so editing the HTML head changes nothing. On Cloudflare Pages, add a _headers file with a /* section followed by a Permissions-Policy line; on Nginx use add_header, on Apache use Header set, and most CDNs expose a custom-header field in the dashboard. After deploying, confirm the value with the parser panel here or the network tab of your browser devtools.

How does this affect embedded third-party iframes?

An iframe runs under its own origin, and capabilities not listed for that origin are denied inside the frame. If you embed a partner video-chat demo and only set camera=('self'), the partner page cannot call getUserMedia, which is usually the goal. When a deliberate embed does need the camera, add its origin to that directive alone. Re-test every embed after a change, because a widget that silently loses geolocation is hard to spot in logs.

What do the four presets cover?

Cross-origin lockdown sets every capability that accepts an allowlist to 'self', so your pages keep full access while embeds lose camera, microphone, geolocation, and the rest. Media for own origin only touches camera, microphone, autoplay, fullscreen, encrypted-media, and picture-in-picture. Sensors and identity off denies the motion sensors, ambient light, local fonts, idle detection, display capture, and browsing topics to every origin. Payments and share locks payment and web-share to the first party and leaves everything else alone.

Will this break features on my own pages?

Not when you keep self in the allowlist. camera=('self') preserves getUserMedia on your own origin while denying it to cross-origin frames, which suits pages that both render user video and embed outside widgets. Choosing none, or an empty allowlist, denies the capability everywhere including your own scripts, so reserve that for APIs you never call. After any edit, open the pages that use the affected features and confirm they still behave.