Skip to content
GDPR compliant. Hosted in the EU.
14-day free trial →
Permissions-Policyallow attributeThird-party content
Security 17 min read 11 Nov 2026

Permissions-Policy: Block Camera, Microphone and Location

Set a Permissions-Policy that blocks camera, microphone and location, grants embedded frames access via allow and keeps checkout and passkey sign-in working.

X

XICTRAQ®

XICTRAQ®

Security HeadersPermissions-PolicyThird-party content
3.7%
of websites send the header, mobile (Web Almanac 2025)
29.2%
of iframes carry an allow attribute, mobile (Web Almanac 2025)
10%
of headers use a catch-all block incl. payment and passkeys, mobile (Web Almanac 2025)
Permissions-Policy: the page and its embedded framesThe header sets the ceiling, the allow attribute grants access frame by framePermissions-PolicyHeader sent by shop.examplecamera=()microphone=()geolocation=()payment=(self "https://pay.example")No grant: blockedCamera, microphone, locationGranted: paymentonly for pay.exampleVideo framevideo.exampleCameraMicrophoneLocationPaymentMap framemap.exampleCameraMicrophoneLocationPaymentChat framechat.exampleCameraMicrophoneLocationPaymentPayment framepay.exampleCameraMicrophoneLocationPaymentallow attributeblockedgranted3.7%of websites send Permissions-Policy, mobileSource: HTTP Archive, Web Almanac 2025
A page embeds a video, a map, a payment form and a chat. Each of these frames is a separate document from another provider, and each one could ask the browser for the camera, the microphone or the visitor's location. The Permissions-Policy defines which browser features your own page and every embedded third-party page may use (W3C). It is set as an HTTP response header on the page and, for each frame, through the 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 headerallow on the frameCamera in the video.example frame
no headerno allowblocked, default self
no headerallow="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 allowblocked, 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.

embeds.html
<!-- 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.

Video

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.

Map

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

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.

Chat and consultation

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).

Tokencontrolstypical need
cameravideo input via getUserMedia()video consultation, video identification
microphoneaudio inputvoice or video consultation
geolocationlocation requestsstore locator, "My location" in a map
display-capturescreen sharing via getDisplayMedia()remote support, co-browsing
paymentPayment Request APIbrowser payment sheet, payment confirmation
publickey-credentials-getretrieving existing passkeyspasskey sign-in, confirmation by the bank
publickey-credentials-createcreating new passkeysregistering a passkey
fullscreenfullscreen via requestFullscreen()videos, image galleries
autoplayautomatic media playbackvideos that start on their own
encrypted-mediaEncrypted Media Extensionsprotected video content
usb, serial, hiddevice interfacesusually 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.

Do not adopt templates unchecked

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.

server-block.conf
# 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.

  1. Finish the inventory. Record your own features and every frame with origin and purpose, as described above.
  2. Observe first. The specification defines a Permissions-Policy-Report-Only header 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 the Reporting-Endpoints header, or can be read on the page with a ReportingObserver (MDN).
  3. 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, usb or serial. This line usually breaks nothing.
  4. Grant deliberately. For each feature you need, name your own origin and the frames' origins in the header and set the matching allow on each frame.
  5. Run through it. Test checkout, sign-in, videos, map and chat on staging in a Chromium-based browser, and only then go live.
  6. Measure instead of assuming. After the rollout, check that the header arrives on every response, including subpages served from their own location blocks and error pages.
$ curl -sI https://www.shop.example/ | grep -i '^permissions-policy'
permissions-policy: camera=(), microphone=(), geolocation=(self), payment=(self "https://pay.example"), publickey-credentials-get=(self "https://pay.example")
$ curl -sI https://www.shop.example/stores/ | grep -ic '^permissions-policy'
0

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.

What the scan measures and what it does not

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 short version

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.

Sources and studies

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?
The header is usually not mandatory. That is exactly when it is easy to set, though: it prevents injected code or an embedded frame from requesting the camera, microphone or location. The OWASP project recommends disabling every feature the site does not need (OWASP).
What is the difference between the header and the allow attribute?
The header applies to the whole page including all embedded frames and sets the ceiling. The 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?
It can if the payment provider uses the Payment Request API or a passkey payment confirmation, because both depend on the 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?
According to MDN's compatibility data, currently only Chromium-based browsers evaluate the header (MDN). The 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?
The gentlest way is a staging environment on which checkout, sign-in and embedded services are run through completely once. In addition, the specification defines the 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.
How does XICTRAQ® rate the Permissions-Policy?
The security scan awards 8 out of 100 points of the security score for a present header (XICTRAQ manual) and reports a missing header as a finding. It checks presence, not the quality of the directives. The free security score check shows the value sent; reviewing its content remains manual work.

Related in XICTRAQ®

Start in 60 seconds

Let XICTRAQ® watch your website.

Free plan with no fixed term available. No credit card, no installation, no hidden fees.