RedScore.ai

Fixes

Web Application Security · Updated 2026-05-02

Permissions-Policy

One-line header. Pass requires any non-empty Permissions-Policy. Recommended: deny camera, microphone, geolocation, payment, USB.

Permissions-Policy (formerly Feature-Policy, renamed in 2020) lets you tell browsers which device APIs and powerful features your site is allowed to use. By default, browsers grant most features when the user agrees in a permission prompt; with Permissions-Policy you can pre-emptively deny features you do not need (camera, microphone, geolocation, payment, USB, etc.). The benefit is defense in depth: even if XSS lands on your page, the malicious script cannot silently invoke features you have denied.

How the check works

Per primary host, the check looks at the Permissions-Policy response header. If the header value is non-empty, the host scores 4/4. Empty header or missing header scores 0/4. The check does NOT validate which features you deny or whether the syntax parses; any non-empty value satisfies the score. The recommended values below are best practice.

How the verdict maps to evidence

  • Pass (4/4 per host): Permissions-Policy header is present and non-empty.
  • Fail (0/4 per host): header missing or empty.

Fix: deny features you do not use

List every feature you do NOT need and lock it down. Allowed lists in parentheses control who can use the feature: () means deny everyone, (self) means only your origin, (self "https://partner.com") means your origin plus a named partner.

Recommended baseline (deny common high-risk APIs)

If you do not embed video calls, payment widgets, or AR/VR experiences, deny everything. If you do, leave a (self) entry for the specific feature instead of empty parens.

Baseline deny-all-risky-features policy

Permissions-Policy:
  accelerometer=(),
  ambient-light-sensor=(),
  autoplay=(),
  battery=(),
  camera=(),
  display-capture=(),
  document-domain=(),
  encrypted-media=(),
  fullscreen=(self),
  geolocation=(),
  gyroscope=(),
  magnetometer=(),
  microphone=(),
  midi=(),
  payment=(),
  picture-in-picture=(),
  publickey-credentials-get=(),
  screen-wake-lock=(),
  sync-xhr=(),
  usb=(),
  web-share=(),
  xr-spatial-tracking=()

The fullscreen=(self) entry permits the page itself to enter fullscreen (used for video players and image lightboxes); empty parens would block that too. Adjust the list to your actual use cases.

Allow specific origins

If a partner site embeds your page in an iframe and needs camera access, list them explicitly:

Allow camera and microphone from a specific partner

Permissions-Policy: camera=(self "https://video.partner.com"), microphone=(self "https://video.partner.com")

Per-server snippets

nginx

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

Apache

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

Caddy

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

Cloudflare (Transform Rules)

Set response header via Transform Rules:
  Header name:  Permissions-Policy
  Header value: camera=(), microphone=(), geolocation=(), payment=(), usb=()

Express / Node.js (Helmet)

import helmet from "helmet";

// Helmet does not set Permissions-Policy out of the box; use a custom middleware:
app.use((req, res, next) => {
  res.setHeader(
    "Permissions-Policy",
    "camera=(), microphone=(), geolocation=(), payment=(), usb=()"
  );
  next();
});

Verify the fix

  • curl -sI https://yourdomain.tld | grep -i permissions-policy should show the header.
  • Open the page in Chrome DevTools → Application → Frames → top → Permissions-Policy. Lists every feature and whether it is allowed for the page.
  • Try a denied feature in DevTools console: navigator.geolocation.getCurrentPosition(p => console.log(p), e => console.error(e)). With geolocation denied via Permissions-Policy, the call returns a permissions error.
  • Re-run the RedScore lookup. Pass requires any non-empty header on every primary HTTPS host.

Common pitfalls

  • Old Feature-Policy syntax. Permissions-Policy uses the new structured-headers syntax: feature=() not feature 'none'. The old Feature-Policy header is deprecated; modern browsers prefer Permissions-Policy. Sending the old header gives partial coverage; send the new one.
  • Wildcard syntax differences. allow-everywhere is feature=*, deny-everywhere is feature=(). It is easy to write feature=(*) by mistake (which is invalid syntax and ignored).
  • Origin quoting. URLs in the allow list must be in double quotes: feature=(self "https://partner.com"). Missing quotes cause silent parse failure.
  • Policy denies a feature your iframe widgets need. Embedded payment, video chat, and map widgets often need camera, microphone, payment, or geolocation. Audit before tightening; add (self "https://widget-host") for legitimate embeds.
  • Header at one layer, missing at another. CDN sets the policy, origin does not (or vice versa). The closest-to-client wins. Set at one layer and confirm with curl.
  • Score passes on gibberish. Permissions-Policy: foo=bar passes the check (any non-empty value), but it is not a real policy. Use a real policy that actually denies features you do not need.

What to do next

See how these recommendations apply to your site's current scan results.

Scan domain