A voice or vision app leaves every other API open by default
This note is for AI apps that legitimately use a powerful browser feature: a voice assistant that needs the microphone, a vision or document tool that opens the camera, a feature that reads geolocation. The concrete launch risk is not that those features exist. It is that with no Permissions-Policy header, every other powerful API stays available to any code running on the page, including code you did not write.
A Permissions-Policy header tells the browser which features your top-level document and its iframes are allowed to use. With no header, the browser applies its own defaults, which for most features means the top-level page is allowed. So a third-party script that gets onto your page, through a compromised dependency or a tag you embedded, can attempt to call getUserMedia for camera and microphone, or the Geolocation API, the same way your own code does. The browser still shows the user a permission prompt, but the API surface is reachable rather than denied at the document level.
Separate what is observable from what is only possible here. What is observable is the header itself: present or absent, and what it allows. What is only possible is an actual injected script abusing an open feature; that depends on a separate flaw such as a cross-site scripting hole or a malicious dependency. Permissions-Policy does not fix that flaw. It shrinks what the flaw can reach.
Why AI-built apps ship without it
Permissions-Policy is opt-in. The browser works fine without it, the app works fine without it, and a generated build has no reason to add a header nobody asked for.
When you prompt an AI tool to add voice input or a camera upload, it wires up the API call that makes the feature work. That is the visible, testable part. The header that scopes which features the rest of the page may use is invisible from inside the running app, so it does not appear in the generated output and nobody misses it during a quick check.
- The feature you asked for works, so the build looks complete.
- The header is a deploy-time concern, not application code, so a prompt focused on the feature rarely produces one.
- Fast deploy means the public URL exists before anyone reviews which browser features the page leaves open. This is the same class as the broader missing-header problem covered in security headers for AI-built apps.
Check the header on your own app
You can confirm the current state from the outside, no attack involved.
Open your deployed app, open DevTools, and look at the Network tab. Click the document request (the top HTML response) and read its response headers. You are looking for Permissions-Policy. A command-line check works too: a HEAD or GET request to your public URL and reading the returned headers shows the same thing.
One note for embeds: if your app shows a third-party iframe (a payment widget, a map, an embedded player), the parent page's Permissions-Policy plus the iframe's allow attribute together decide what that frame may use. A frame is not granted a feature unless you delegate it.
- Suspicious result: no Permissions-Policy header at all, or one that allows features you do not use, for example camera=* or geolocation=*. The asterisk and the absent header both mean any origin on the page may try to use that feature.
- Safer result: a Permissions-Policy header that names each feature and either denies it outright or allows only the origins that need it, for example camera=(self) and geolocation=().
- Repeat for the features you actually use versus the ones you do not. The goal is that everything you do not use is denied, and everything you do use is allowed only where it is needed.
Set default-deny, then allowlist what you use
The pattern is the same one that makes Row Level Security safe: start from deny, then open exactly the paths you intend.
Send a Permissions-Policy response header that denies every feature you do not use and allowlists only the ones you do. An empty allowlist, written as a feature name followed by (), denies that feature for everyone including your own page. The token self allows your own origin. A specific origin in the list allows only that origin, which is how you let a named payment or video provider use a feature without opening it to the whole page.
What this does not cover: Permissions-Policy controls browser feature access at the document level. It does not stop a script from being injected in the first place, it does not validate what your own feature does with the camera or location once allowed, and it does not replace the user's own permission prompt. It reduces the blast radius of an injected script; it is not a substitute for fixing the injection path, which is the job of a Content Security Policy and clean dependencies. The CSP side is covered in the note on CSP for analytics, payments, auth, and embeds.
- For features you never use, deny them: microphone=(), camera=(), geolocation=(), and the others your app does not touch.
- For a feature your own page uses, scope it to self: microphone=(self) for a voice assistant whose recording runs on your own origin.
- For a feature a known third party needs, allowlist that origin explicitly rather than using a wildcard, and delegate it to the iframe with the allow attribute.
- Set the header at the layer that serves your app, your host or CDN or framework response config, so it applies to the document for every visitor.
Permissions-Policy is a containment control, not a vulnerability fix. Default-deny means that if hostile code does run on your page, it cannot reach the microphone, camera, or location of a feature you never built. The underlying injection still has to be prevented separately. This maps to the OWASP Security Misconfiguration class: a safe-by-default header you can set and forget.
Where VibeCodeGuard fits, and what it does not prove
VibeCodeGuard's Launch Check reads the Permissions-Policy header on your public URL. If the header is absent, the scan flags it and returns a remediation snippet scoped to the unused features, so you get a starting allowlist that denies what you do not need. It checks the thing a visitor's browser actually receives.
What it does not do is know which features your app legitimately uses. Deciding that your voice assistant needs microphone=(self) while everything else stays denied is your call, because only you know which features are real. The scan sees the header; it does not read your application code or confirm that your allowlist matches your actual feature set.
Next step: list the powerful browser features your app actually uses, deny everything else with a Permissions-Policy header, and scope the rest to self or to named origins. Run a focused Launch Check against the public URL before sharing it, so a missing header shows up in your scan rather than going out with the launch.
A present Permissions-Policy header does not prove your app is safe from feature abuse, and it does not prove your camera or mic code is correct. The scan confirms the header exists and reports what it allows. It does not verify that your allowlist is right for your app or that the underlying injection path is closed. Treat an absent header as an easy fix to make before launch, and treat a present one as one signal among several.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.