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