allow attribute on the <iframe>. Adoption is low: despite clear growth, only 3.7 percent of websites send the header in the mobile crawl (Web Almanac 2025). This article shows how the header and the allow attribute work together, which frame needs which feature, why payment and publickey-credentials-get do not belong on a blanket block list, and how to roll the list out in stages without breaking checkout.What the Permissions-Policy controls
The specification defines a mechanism that lets developers selectively enable and disable individual browser features and APIs (W3C). Each feature has a token such as camera, microphone, geolocation or payment and is called a policy-controlled feature. Before it was renamed, the mechanism was called Feature Policy. The old Feature-Policy header is deprecated and, according to the Web Almanac, keeps losing ground (Web Almanac 2025). The specification itself is still a Working Draft at the W3C; so far the header is implemented in Chromium-based browsers, more on that below.
The key to understanding it is the default of each feature, called the default allowlist in the specification. It applies as long as no header says otherwise. For camera and microphone it is self (W3C Media Capture), and the same goes for location (W3C Geolocation). self means: the page itself and frames from the same origin may request the feature, frames from other origins may not (W3C). An embedded video from another domain therefore cannot simply open the camera, even without a header, unless someone explicitly grants it.
The Permissions-Policy does not replace the browser's prompt to the visitor, it sits in front of it. If a feature is blocked by policy, the call fails without anyone being asked: with the camera blocked, getUserMedia() rejects with a NotAllowedError (MDN). Conversely, a grant in the policy does not replace the visitor's consent. One detail in the specification deserves attention: because browsers generally attach permissions to the top-level domain, a delegated frame may be able to use the camera or microphone without another prompt if the visitor has already allowed the camera on your page (W3C). Delegating a feature to a frame therefore passes on the trust the visitor placed in your domain.
The Permissions Policy is open by default, which means that in order to restrict the use of a feature, it has to be explicitly mentioned in the header.
— HTTP Archive, Web Almanac 2025, Security chapter
How rarely this work gets done is visible in the HTTP Archive data: the share of websites with a Permissions-Policy grew clearly over the previous year, yet on mobile it stands at 3.7 percent (Web Almanac 2025). According to the report, modern headers such as Permissions-Policy and Cross-Origin-Opener-Policy are still in the early stages of adoption. For an overview of all security headers and what each one does, see HTTP security headers explained.
Header and allow attribute: two switches that both have to agree
The Permissions-Policy lives in two places. The header Permissions-Policy arrives with the server response and applies to the document and everything embedded in it (W3C). The allow attribute sits on an individual <iframe> and determines what exactly that frame gets. For a frame from another origin, a feature only counts as granted if both places agree: the page's header has to admit the frame's origin, and the allow attribute has to name the feature for that frame (W3C).
This creates a clear division of roles. The header is the ceiling that no frame can exceed, including one that an injected script adds to the page later. The specification describes exactly this case: with geolocation=(self "https://example.com"), only the page itself and frames from example.com get the location, even if an attacker can embed frames of their own (W3C). The allow attribute is the case-by-case grant. An empty list in the header, such as camera=(), disables the feature for all documents including every nested one, regardless of origin (W3C).
The following overview shows what a frame from video.example gets under the policy when it is embedded on a page from shop.example. Where access is granted, the visitor's prompt still follows.
| Page header | allow on the frame | Camera in the video.example frame |
|---|---|---|
| no header | no allow | blocked, default self |
| no header | allow="camera" | granted |
camera=() | allow="camera" | blocked, including the page itself |
camera=(self) | allow="camera" | blocked, the frame's origin is missing from the header |
camera=(self "https://video.example") | no allow | blocked, the grant on the frame is missing |
camera=(self "https://video.example") | allow="camera" | granted, only for video.example |
The Web Almanac also measures how common the attribute is: of 33.3 million iframes in the mobile crawl, 29.2 percent carry an allow attribute (Web Almanac 2025). The most frequent entries are encrypted-media and autoplay, both media playback features (Web Almanac 2025). Such entries usually arrive with the provider's embed code. Whoever copies the code copies the grants along with it, and it is worth reading them once before they go live.
<!-- Video: playback yes, camera and microphone no -->
<iframe src="https://video.example/embed/123"
allow="autoplay; encrypted-media; fullscreen; picture-in-picture"></iframe>
<!-- Payment: Payment Request and passkey confirmation for the payment provider -->
<iframe src="https://pay.example/checkout"
allow="payment; publickey-credentials-get"></iframe>
<!-- Map: no location, the visitor types the address -->
<iframe src="https://map.example/embed?place=store"></iframe>
A special case is the older allowfullscreen attribute. It grants fullscreen to any origin that loads into the frame. If fullscreen appears in the allow attribute, however, allowfullscreen has no effect (W3C).
Inventory: which frame needs which feature?
Before a line goes into the server configuration, take inventory. Two questions matter: which features does your own page use, and which frames does it embed for what purpose? On a typical business or shop site, four groups of frames come together.
Embedded videos usually need playback features: autoplay, encrypted-media for protected content, fullscreen and picture-in-picture. They do not need camera, microphone or location.
A map shows store locations without location access. Only a "My location" button inside the map frame needs geolocation, and then specifically for the map provider's origin.
Payment frames may use the Payment Request API (payment) and, when the bank confirms the payment, passkeys (publickey-credentials-get, publickey-credentials-create). Which of these applies is stated in the payment provider's integration guide.
A text-only chat needs no device features. A video or voice consultation inside a frame needs camera and microphone, and only for the consultation service's origin.
Then there are the page's own features. A store locator that asks for the visitor's location needs geolocation=(self). A passkey sign-in needs publickey-credentials-get for your own page. The following table summarizes the tokens most likely to matter on business and shop sites; the descriptions follow the MDN reference (MDN).
| Token | controls | typical need |
|---|---|---|
camera | video input via getUserMedia() | video consultation, video identification |
microphone | audio input | voice or video consultation |
geolocation | location requests | store locator, "My location" in a map |
display-capture | screen sharing via getDisplayMedia() | remote support, co-browsing |
payment | Payment Request API | browser payment sheet, payment confirmation |
publickey-credentials-get | retrieving existing passkeys | passkey sign-in, confirmation by the bank |
publickey-credentials-create | creating new passkeys | registering a passkey |
fullscreen | fullscreen via requestFullscreen() | videos, image galleries |
autoplay | automatic media playback | videos that start on their own |
encrypted-media | Encrypted Media Extensions | protected video content |
usb, serial, hid | device interfaces | usually none on business and shop sites |
Not every third-party component lives in a frame. A chat widget or tag loaded as a script directly into the page runs in the page's document and shares its rights: whatever the header allows for your own page applies to that script, and there is no allow attribute for it. Setting camera=(self) therefore also grants the camera to every embedded script. Which scripts these are and how to contain their risk is covered in third-party scripts as a security risk.
If you look after many websites, it makes sense to keep this list together with the rest of your site inventory: domains, software, responsibilities, embedded services. How such an inventory makes the difference when a vulnerability hits is shown in the article on PHP2Shell and the website inventory. For the Permissions-Policy, one row per page is enough: frame, origin, purpose, features needed.
Do not block payment and publickey-credentials-get blindly
Catch-all templates circulate online that switch off almost everything a browser knows in one go. One of them is clearly visible in the Web Almanac: a long list that sets, among others, camera=(), microphone=(), geolocation=(), payment=() and publickey-credentials-get=() appears in 10 percent of the Permissions-Policy headers measured on mobile (Web Almanac 2025). For a site without checkout and without passkey sign-in, that may be fine. For a shop, exactly this line can stop the purchase.
payment controls the Payment Request API, through which the browser mediates the payment flow between shop and buyer (W3C Payment Request). The default is self; in a cross-origin frame the API only works with allow="payment" (W3C Payment Request). If the feature is blocked, even creating a payment request fails with an error (W3C Payment Request). Payment confirmation with a passkey, which the W3C describes as Secure Payment Confirmation, depends on the same grant (W3C SPC).
publickey-credentials-get controls retrieving existing passkeys through the Web Authentication API (MDN). The WebAuthn Level 3 Recommendation sets the default for this feature and for publickey-credentials-create to self and states: if the feature is disabled in a document, no content in that document may use the methods (W3C WebAuthn). In cross-origin frames the API is disabled anyway without an allow attribute (W3C WebAuthn). An empty list in the header, by contrast, also blocks your own page. Setting publickey-credentials-get=() while offering passkey sign-in shuts down your own login.
A catch-all template is a list of assumptions about your site. Before payment=() or publickey-credentials-get=() goes live, run through checkout, sign-in and the bank's confirmation step completely on a staging environment, in a Chromium-based browser, because only there does the header take effect.
A targeted grant holds up better than a block: your own page and the payment provider may, everyone else may not. In the header that means payment=(self "https://pay.example") and publickey-credentials-get=(self "https://pay.example"), and the payment frame carries allow="payment; publickey-credentials-get". Take the provider's origins from its documentation instead of guessing them. If the provider opens the bank's confirmation in a separate frame directly on your page, another origin comes into play that the list has to know. That is one more reason to run through checkout with test data in the provider's test mode before the header goes live.
Writing the header correctly
The header is a structured field according to RFC 8941, specifically a dictionary (W3C). Each feature comes with its allowlist in parentheses, and multiple features are separated by commas (MDN). In practice, three forms occur:
camera=(): empty list, blocked for the page and all frames.geolocation=(self): only the page itself and frames from the same origin.payment=(self "https://pay.example"): the page plus one named external origin, written in quotes.
The allow attribute follows different rules: features are separated by semicolons, origins are not quoted, and if no origin is given, the origin from the frame's src attribute applies (MDN). Mixing up the two notations produces entries that the browser silently skips.
An error that makes the whole line unreadable weighs more. If the header cannot be parsed as a dictionary, the browser treats it as if it did not exist (W3C). A typical trigger is the old Feature-Policy notation such as camera 'none' sent under the new name. Unknown feature names, on the other hand, are skipped one by one and the rest of the line stays in effect (W3C). An outdated directive in the list does no harm, wrong syntax does.
The opposite of a block is the wildcard *: it admits a feature for every origin and thereby lifts the ceiling the header would otherwise set. A frame from another origin still needs an allow attribute for camera, microphone or location, because without a grant on the frame the default self applies (W3C). In the desktop crawl, 0.27 percent of Permissions-Policy headers set this wildcard, and it did not appear in the mobile crawl at all (Web Almanac 2025). It belongs in a line only if granting the feature to every origin is really intended.
# In the server block: applies to every location without its own add_header
add_header Permissions-Policy 'camera=(), microphone=(), geolocation=(self), payment=(self "https://pay.example"), publickey-credentials-get=(self "https://pay.example")' always;
# Caution: an add_header inside a location replaces the inherited lines.
location /stores/ {
add_header Cache-Control "no-cache";
add_header Permissions-Policy 'camera=(), microphone=(), geolocation=(self)' always;
}
With nginx there is a pitfall that has nothing to do with the Permissions-Policy itself: add_header lines are inherited from the previous level only if the current level has no add_header line of its own (nginx). A single cache directive inside a location makes all security headers disappear there unless they are repeated. Newer nginx versions offer add_header_inherit to change this behavior (nginx). The always parameter makes sure the header is sent regardless of the status code, including on error pages (nginx). The same principle applies in Apache, Caddy, IIS or Node/Express: the header has to arrive on every response where it is meant to take effect.
Roll it out in stages, not all at once
A Permissions-Policy cannot be flipped like a switch without knowing what it will break. A staged process makes sense, similar to HSTS, where the value is also observed first and then raised; the parallel is described in the article on the requirements for HSTS preload.
- Finish the inventory. Record your own features and every frame with origin and purpose, as described above.
- Observe first. The specification defines a
Permissions-Policy-Report-Onlyheader that reports violations without enforcing them (W3C). According to MDN's compatibility data, Chrome supports it from version 120 as an experimental feature (MDN). Reports go to an endpoint named in theReporting-Endpointsheader, or can be read on the page with aReportingObserver(MDN). - Block what nobody needs. Start with the features that, according to the inventory, neither the page nor any frame uses, typically
camera,microphone,display-capture,usborserial. This line usually breaks nothing. - Grant deliberately. For each feature you need, name your own origin and the frames' origins in the header and set the matching
allowon each frame. - Run through it. Test checkout, sign-in, videos, map and chat on staging in a Chromium-based browser, and only then go live.
- Measure instead of assuming. After the rollout, check that the header arrives on every response, including subpages served from their own
locationblocks and error pages.
The second request shows the pattern from the nginx example: the subpage lacks the header because its location carries its own add_header lines and the Permissions-Policy was not repeated there. If you prefer not to use the command line, the free HTTP header check shows every header of the final response.
When testing, it helps to know who evaluates the header at all. According to MDN's compatibility data, Chrome does so from version 85, while Firefox and Safari do not evaluate it (MDN). The allow attribute on the frame, by contrast, is understood by all three: Firefox from version 74 and Safari from version 11.1, Safari however not for every feature, for example not for location (MDN). This leads to a practical rule: the grant on the frame is what works across browsers, and the header is the additional ceiling in Chromium-based browsers. Skipping one because the other is set gives away protection.
What the Permissions-Policy does not do
The policy governs device features and APIs, not which content loads. Which scripts, frames and connections a page allows at all is decided by the Content-Security-Policy; how to build one is shown in configuring a Content Security Policy. A script that skims form input needs neither the camera nor the location. Against that kind of web skimming, the Permissions-Policy does not help.
What it does do is summarized briefly by the OWASP project: it prevents an injection such as XSS from enabling the camera, the microphone or other browser features (OWASP). The recommendation there is to disable every feature the site does not need or to allow it only for authorized origins (OWASP). Together with window isolation through COOP and the other cross-origin headers, described in setting COOP, COEP and CORP correctly, this gives a picture in which every header has its own job.
For data protection, the policy is a building block, not proof. Art. 25(2) GDPR requires default settings ensuring that, by default, only personal data necessary for each specific purpose is processed (GDPR). Blocking location where no purpose calls for it implements this idea technically. Whether an embedded service transmits data before consent, however, is not something the policy answers; that takes a measurement of the requests on page load, as performed by the GDPR check in XICTRAQ®.
Permissions-Policy in the XICTRAQ® security scan
The XICTRAQ® security scan reads the headers of the response the browser displays and awards 8 out of 100 points of the security score for a present Permissions-Policy (XICTRAQ manual). If the header is missing, the finding "Permissions-Policy header missing" appears in the list. The point allocation and an nginx template that blocks camera, microphone and location are in the security headers manual.
The scan checks whether the header is present, not the quality of the individual directives. A line containing only interest-cohort=() counts just like a carefully tuned list, and even a line with a syntax error that the browser discards earns the points. The scan measures the address registered for the website. The free security score check shows the value that was sent in plain text and also works for a single subpage. Whether it blocks the intended features and grants the required ones remains a manual review, as described in this article.
The scan is most useful over time. A manually started scan records in the changelog when the header was added or removed. If the score drops below the configured minimum, XICTRAQ® reports it through the notification channels you have set up. A weekly summary describes the score and the core headers of each website in plain language, Permissions-Policy included. To additionally watch embedded third-party scripts for unnoticed changes, use script integrity monitoring.
Grant only what is needed
The Permissions-Policy is not a header you copy from a template once. It is the written answer to the question of which device features your page and each embedded provider should get. The answer starts with the inventory, blocks the uncontroversial features first, grants checkout and sign-in deliberately and is measured once it is live. Camera, microphone and location are the easy cases: where the inventory shows no service that needs them, blocking them takes one line.
The header is the ceiling for the page and all frames, the allow attribute is the grant per frame; a cross-origin frame only gets a feature if both agree. camera, microphone and geolocation can be blocked wherever the inventory shows no need, while payment and publickey-credentials-get are granted deliberately to your own page and the payment provider. And because Firefox and Safari do not evaluate the header according to MDN, the allow attribute on the frame remains what also works outside Chromium, in Safari only for some features.
This article is based on: W3C, Permissions Policy, Working Draft of 26 October 2026, sections 2, 4.8, 5.2, 6, 8.1 and 9; W3C, Media Capture and Streams, section Permissions Policy Integration; W3C, Geolocation, section Permissions policy; W3C, Payment Request API, sections on iframes and the Permissions Policy; W3C, Web Authentication Level 3, sections 5.9 and 5.10; W3C, Secure Payment Confirmation, section Permissions Policy integration; HTTP Archive, Web Almanac 2025, Security chapter, section Permissions Policy with figures 9.22 to 9.24; MDN Web Docs, Permissions-Policy reference page, and MDN's compatibility data (browser-compat-data) for the Permissions-Policy header, the Permissions-Policy-Report-Only header and the iframe allow attribute; OWASP Cheat Sheet Series, HTTP Security Response Headers Cheat Sheet; nginx documentation, module ngx_http_headers_module; Regulation (EU) 2016/679 (GDPR), Article 25. The information on XICTRAQ® refers to the security scan, the security headers manual and the free security score check in their current state.
Do I need a Permissions-Policy if my site does not use the camera?
What is the difference between the header and the allow attribute?
allow attribute sits on a single <iframe> and grants features to that frame. A frame from another origin typically only gets a feature if both agree (W3C).Can payment=() disrupt my checkout?
payment feature (W3C Payment Request). Instead of blocking it outright, it usually makes more sense to grant payment to your own page and the provider's origin and to run through checkout in test mode before the rollout.Does the header work in Safari and Firefox?
allow attribute on the frame, however, is also understood by Firefox and Safari, though Safari does not support it for every feature. For embedded frames, the attribute is therefore what works across browsers.How do I test a Permissions-Policy without breaking anything?
Permissions-Policy-Report-Only header, which only reports violations (W3C); Chrome supports it as an experimental feature (MDN). Only when no unexpected reports come in is the policy enforced.