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();
});
Nginx
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), browsing-topics=()";
Vercel (vercel.json)
{
"headers": [{
"source": "/(.*)",
"headers": [{
"key": "Permissions-Policy",
"value": "camera=(), microphone=(), geolocation=(), browsing-topics=()"
}]
}]
}
The empty parentheses () mean "no one is allowed to use this API." Not your page, not any embedded iframe. That's usually what you want.
What to restrict
At minimum, restrict these four:
camera=()— Unless your app does video calls, lock this down.microphone=()— Same reasoning.geolocation=()— Unless you need location services, block it.browsing-topics=()— Google's Topics API for ad targeting. You almost certainly don't want this.
If your app legitimately needs camera access (a video conferencing tool, a QR scanner), you can allow it for your own origin:
camera=(self), microphone=(self), geolocation=()
But most web apps don't need any of these. The empty-parentheses default is correct for the vast majority of projects.
For a more comprehensive policy, you can also restrict these lesser-known APIs:
camera=(), microphone=(), geolocation=(), browsing-topics=(),
payment=(), usb=(), bluetooth=(), serial=(), hid=(),
screen-wake-lock=(), idle-detection=(), display-capture=()
Each one controls a specific browser capability. payment=() blocks the Payment Request API. usb=() blocks WebUSB. display-capture=() prevents screen recording. None of these should be available to arbitrary third-party scripts on a typical web application.
The AI coding tool blind spot
This check is particularly interesting in the context of AI-generated code. We've tested every major AI coding tool — Claude Code, Cursor, Bolt, Windsurf — and none of them add Permissions-Policy when generating a new project. They'll set up auth, database connections, API routes, and deployment configs. They'll even sometimes add X-Frame-Options or X-Content-Type-Options. But Permissions-Policy? Never.
It's not in their training data as a "standard thing to do." It's not in the Next.js docs as a recommended setup step. It's not in any popular starter template. So the AI doesn't suggest it, the developer doesn't know to ask for it, and the header never gets set.
This is exactly the kind of gap that structured audits catch. Not the stuff you're already worried about — the stuff you've never heard of.
A broader pattern
The Permissions-Policy failure is a symptom of a deeper problem: security headers are invisible infrastructure. Developers (and AI coding tools) focus on what's visible — the UI works, the API returns data, the auth flow redirects correctly. The HTTP headers wrapping every response? Nobody looks at those until a penetration test flags them.
In our benchmark data, the Security Headers category consistently scores lower than any other category across all 30 projects. Basic Hygiene (.env in .gitignore, no hardcoded secrets, lockfile present) averages above 85%. Security Headers averages below 60%. The stuff developers think about is fine. The stuff they don't think about is where the gaps are.
Permissions-Policy is the most extreme example: a useful security control, a trivial fix, and a 100% failure rate across projects that otherwise take security seriously.
Run the check yourself
The Security Headers & Basics audit is free — no account required. It checks for Permissions-Policy along with 20 other security configurations. Copy the prompt, run it in your AI coding tool, and see where you stand.
The fix takes 30 seconds. The only hard part is knowing you need it.