Remediation Guides

How to Set the Permissions-Policy Header (Missing Permissions Policy Fix)

26 September 2026 8 min read

A “Missing Permissions Policy” finding means your web server returns pages without a Permissions-Policy header, so the site never tells the browser which features (camera, microphone, geolocation, payment) its pages and embedded frames may use. Fix it by sending a header such as Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=() and removing any old Feature-Policy header.

What the scanner is actually detecting

All of these checks are passive: the scanner reads the response headers of pages it has already requested and sends no attack traffic.

Scanner Finding title ID Vendor rating
Tenable Web App Scanning Missing Permissions Policy Plugin 98526 (family: HTTP Security Header) Info
ZAP Permissions Policy Header Not Set Rule 10063 (alert 10063-1) Low
ZAP Deprecated Feature Policy Header Set Rule 10063 (alert 10063-2) Low
Acunetix Permissions-Policy header not implemented See your scan report Info

ZAP’s rule is a beta passive rule. It only inspects HTML and JavaScript responses and skips redirects unless it runs at the Low alert threshold. It reads two headers: if a Feature-Policy header is present it raises 10063-2, and it raises 10063-1 only when neither header is present. Tenable’s solution text simply asks you to add the Permissions-Policy header, and its references cover the rename from Feature-Policy. None of these checks judge whether your policy is sensible; they only look for the header.

Real-world risk

This is a missing hardening header, not a vulnerability. Tenable and Acunetix rate it informational and ZAP rates it low. Nobody can attack a site simply because the header is absent.

Browsers already restrict powerful features without it. Under the W3C Permissions Policy specification, features such as camera and geolocation are allowed by default only in the top-level page and in same-origin frames; a cross-origin iframe gets them only when your page grants them through the iframe allow attribute. Users still see a permission prompt for camera, microphone and location.

What the header adds is a site-wide ceiling. With camera=(), no frame on the page can use the camera, even if a developer or a third-party widget adds allow="camera" to an iframe, and a script injected through an XSS flaw or a compromised third-party library cannot open a camera or location request under your origin’s name. That matters most on origins where users have already granted a permission, for example an application with video calls, because a stored grant may not prompt again. According to MDN’s compatibility data, only Chromium-based browsers such as Chrome and Edge enforce the header; Firefox and Safari ignore it. Treat it as low-priority defense in depth.

How to confirm it on the host

Test the exact URLs listed in the finding. This prints either header if present:

curl -s -D - -o /dev/null https://app.example.com/ | grep -iE '^(permissions|feature)-policy'

Check an error page too, because headers are often added only to successful responses:

curl -s -D - -o /dev/null https://app.example.com/does-not-exist | grep -iE '^(permissions|feature)-policy'

On Windows, PowerShell gives the same answer for pages that return 200:

$r = Invoke-WebRequest -Uri https://app.example.com/ -UseBasicParsing
$r.Headers['Permissions-Policy']
$r.Headers['Feature-Policy']

No Permissions-Policy line confirms the finding. A Feature-Policy line on its own means ZAP reports 10063-2 and other scanners still treat the header as missing.

How to fix it

Step 1: decide what each feature should allow

The header is a comma-separated list of feature=allowlist pairs. The allowlist takes one of these forms:

Allowlist Effect
() Disabled for the page and every iframe on it
(self) Allowed for your origin and same-origin iframes
(self "https://maps.example.net") Your origin plus the listed origins, each in double quotes
* Allowed for any origin, including cross-origin iframes (avoid)

List what the application really uses: video calls need camera and microphone, store locators need geolocation, embedded payment or wallet buttons may need payment, and embedded video players usually need fullscreen. Everything else can be set to (). A reasonable baseline for a site that uses none of them:

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()

Use feature names from MDN’s directive list. The specification says unrecognized names are ignored, and if the header value cannot be parsed at all, the browser ignores the whole header. A typo can leave you with a header that satisfies the scanner and does nothing.

Step 2: replace Feature-Policy (feature-policy vs permissions-policy)

Permissions-Policy is the renamed successor of Feature-Policy, with a different syntax:

# Deprecated
Feature-Policy: camera 'none'; microphone 'none'; geolocation 'self'

# Current
Permissions-Policy: camera=(), microphone=(), geolocation=(self)

Chrome’s documentation says Permissions-Policy takes priority when both are sent, and ZAP raises 10063-2 for as long as Feature-Policy is present, so remove the old header instead of keeping both. Never paste old syntax into the new header: camera 'none' is not valid Permissions-Policy syntax and makes the header unparseable.

Step 3: add the header

nginx permissions-policy example (server or http block):

add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;

When you allow an origin, wrap the nginx value in single quotes so the double-quoted origin passes through unchanged:

add_header Permissions-Policy 'camera=(), microphone=(), geolocation=(self "https://maps.example.net"), payment=()' always;

Without always (nginx 1.7.5 and later), the header is only added to 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308 responses. add_header directives are inherited only when the current level defines none, so a location block with its own add_header drops the server-level header; repeat it there, or on nginx 1.29.3 and later use add_header_inherit merge;. Delete any add_header Feature-Policy line, and if a proxied application still emits one, strip it with proxy_hide_header Feature-Policy; (fastcgi_hide_header for PHP-FPM).

sudo nginx -t && sudo systemctl reload nginx

Apache HTTP Server (requires mod_headers; a2enmod headers on Debian and Ubuntu):

Header onsuccess unset Permissions-Policy
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()"
Header unset Feature-Policy
Header always unset Feature-Policy

The first line follows the mod_headers documentation’s pattern for avoiding a duplicate when a proxied or FastCGI backend also sets the header. For an origin, single-quote the whole value as in the nginx example. Then run apachectl configtest and sudo systemctl reload apache2 (the unit is httpd on RHEL-family systems).

IIS (site web.config):

<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Permissions-Policy" value="camera=(), microphone=(), geolocation=(), payment=(), usb=()" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>

In IIS Manager: select the site, open HTTP Response Headers, click Add. If the list already shows a Feature-Policy entry, select it and click Remove. Inside web.config, write the double quotes around an origin as &quot;.

Application code or proxy layer. In a Node.js application, one middleware covers every route:

app.use((req, res, next) => {
  res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()');
  next();
});

A CDN, load balancer or reverse proxy that can add response headers covers every backend at once. Whichever layer you choose, make it the only one that sets the header so two different policies never reach the browser.

How to verify the fix and rescan

Rerun the curl commands against the finding’s URLs and an error page. Each HTML response should carry one Permissions-Policy header and no Feature-Policy header. In Chrome, open DevTools, go to the Application panel and select the frame under Frames to see its allowed and disallowed features. If a feature you set to () still appears as allowed, the header did not parse; recheck the syntax and the Console. Then walk through any workflow that uses the features you restricted, such as video calls, maps or checkout.

Finally rescan: rerun the Tenable WAS scan, re-spider the site through ZAP (its passive rules run on every response it sees), or rescan in Acunetix. Header findings are runtime observations that only a dynamic scan produces (see what SAST, DAST and SCA scanners can each detect), so a code change alone will not close them.

What can break and how to roll back

  • Video and audio features: camera=() and microphone=() block calls, recording and QR scanning; use (self) or add the provider’s origin.
  • Maps and store locators: geolocation=() stops “use my location” buttons.
  • Embedded payment or wallet buttons: payment=() blocks the Payment Request API in the provider’s iframe; list that origin instead.
  • Embedded video players: adding fullscreen=() disables their fullscreen button.
  • Stale edge caches: a CDN may keep serving old headers until you purge it.

Rollback is immediate because only a response header changes. Loosen the one directive that broke to (self), or remove the line, then run the config test and reload (on IIS, saving web.config applies the change).

Common false positive reasons

  • Edge devices: a CDN, WAF or load balancer that strips or rewrites headers means the scanner sees a different response from the origin. See how a WAF changes what your web scanner reports.
  • Different virtual host: scanning by IP address often lands on an unconfigured default site.
  • Script files: ZAP also checks JavaScript responses. A script loaded by a page runs under that page’s policy, so the header on the .js response adds little; setting it globally is still the cheapest way to clear these.
  • Stale results: the scan ran before the reload, or a cache served old headers.

A Feature-Policy header on its own, a header missing only on error pages, and a site that uses no browser features at all are not false positives. The last case is a fair candidate for a documented low-priority acceptance, but adding the header usually takes less effort.

FAQ

Is a missing Permissions-Policy header a vulnerability?

No. It is a missing mitigation, which is why vendors rate it informational or low. It limits the damage injected or third-party scripts can do with browser features.

Should I keep Feature-Policy for older browsers?

No. Feature-Policy is deprecated, Chrome gives Permissions-Policy priority when both are present, and ZAP flags Feature-Policy for as long as you send it.

Can I set Permissions-Policy in a meta tag?

No. The specification delivers the policy only through the HTTP header and the iframe allow attribute.

Does the header work in Firefox and Safari?

MDN’s compatibility data lists the header as unsupported in both. They ignore it harmlessly, so the same header is safe to send to every browser.

Tracking this finding across many hosts

Header findings tend to appear on every virtual host at once. SITEY, a self-hosted vulnerability management platform, imports findings from 16 scanners and merges duplicates within each scanner but not across them, so the same missing header reported by two scanners stays as two records. For Acunetix, Nessus and Burp findings it can run a per-finding retest to confirm the header is now sent before the item is closed.

Sources

SITEY closes the loop, not just the report.Discover, validate, fix and verify in your own infrastructure.

See pricing