Permissions-Policy: The Security Header Nobody Sets
Permissions-Policy: The Security Header Nobody Sets
We ran the Security Headers & Basics audit against 30 well-known open-source projects. Supabase, Cal.com, Formbricks, PostHog, Directus, Infisical, Plausible. Big names. Well-maintained codebases with real security teams.
Every single one failed the Permissions-Policy check. 30 out of 30.
What Permissions-Policy actually does
Browsers ship with access to dozens of powerful APIs: camera, microphone, geolocation, payment, USB, Bluetooth, screen capture. By default, any JavaScript running on your page — including third-party scripts, ads, and embedded iframes — can request access to all of them.
Permissions-Policy (formerly Feature-Policy) lets you explicitly restrict which browser APIs your site and its embedded content can use. It's a declarative allow-list at the HTTP header level.
Without it, a compromised third-party script can silently request microphone access. An embedded iframe from an ad network can attempt geolocation. Your analytics snippet can trigger the payment API. The browser will prompt the user, sure — but the request itself is allowed.
With the header set, those requests are blocked before they even reach the permission prompt.
Why nobody sets it
Three reasons:
1. Frameworks don't default it. Next.js, Nuxt, SvelteKit, Remix, Astro — none of them include Permissions-Policy in their default configuration. Compare that to X-Content-Type-Options: nosniff, which some frameworks do set automatically. The header simply isn't on anyone's starter template.
2. Nothing visibly breaks without it. Unlike a missing CSP (which at least shows up in security scanners) or missing HSTS (which browsers will warn about), a missing Permissions-Policy has zero visible symptoms. Your app works fine. Your Lighthouse score doesn't change. No console warnings.
3. Developers don't know it exists. Content-Security-Policy gets all the security header press. Strict-Transport-Security is well-understood. But Permissions-Policy lives in a quiet corner of the spec that most web developers have never encountered.
The fix: one line per framework
This is what makes the 30/30 failure rate frustrating. The fix is trivial.
Next.js (next.config.js)
const nextConfig = {
headers: async () => [{
source: '/(.*)',
headers: [{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()'
}]
}]
}
Express
app.use((req, res, next) => {
res.setHeader(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=(), browsing-topics=()'
);
next();
});