MCP Server Security · Permissions-Policy
MCP server Permissions-Policy security — restricting browser API access in MCP client UIs, geolocation, camera, and payment control
The Permissions-Policy HTTP header (the successor to Feature-Policy) controls which browser APIs a document and its embedded iframes are allowed to use. For browser-based MCP client UIs, it is a blast-radius limiter for XSS attacks: even if a prompt-injection payload fires a script in your MCP UI, a strict Permissions-Policy prevents that script from accessing the user's camera, microphone, geolocation, payment APIs, and other high-value browser capabilities that the MCP app legitimately never needs. It also enables per-iframe permission attenuation for any iframes used to render sandboxed tool output.
Permissions-Policy vs Feature-Policy
Feature-Policy was the predecessor header, standardized in browsers from 2018–2020 but never fully specified. Permissions-Policy (introduced in Chrome 88 and Firefox 74) is the W3C-standardized replacement with a new syntax. Both headers may need to be set simultaneously for full browser coverage during the transition period:
# Caddy: set both headers for maximum browser coverage header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), bluetooth=(), serial=(), ambient-light-sensor=(), accelerometer=(), gyroscope=(), magnetometer=(), fullscreen=(self)" header Feature-Policy "camera 'none'; microphone 'none'; geolocation 'none'; payment 'none'"
The syntax difference: Feature-Policy uses space-separated directives with 'none' / 'self' in quotes; Permissions-Policy uses comma-separated directives with () for deny-all and (self) for same-origin only.
Permissions-Policy directives relevant to MCP client UIs
| Directive | Default | Recommended for MCP UI | Reason |
|---|---|---|---|
camera | Allowed (with user prompt) | () — deny | MCP tool UIs don't need camera; XSS payload capturing video is a high-severity privacy risk |
microphone | Allowed (with user prompt) | () — deny | Audio capture by injected script is a critical data exfiltration vector in agentic UIs |
geolocation | Allowed (with user prompt) | () — deny | Physical location of the user is PII; MCP tool handlers do not need it |
payment | Allowed | () — deny | Payment Request API in a compromised MCP UI can trigger payment flows the user didn't initiate |
usb | Allowed (with user prompt) | () — deny | WebUSB access from injected scripts enables hardware-level attacks |
bluetooth | Allowed (with user prompt) | () — deny | Bluetooth scanning reveals physical proximity; can interact with paired devices |
fullscreen | Allowed | (self) — same-origin only | Fullscreen requests from cross-origin iframes enable phishing overlays |
display-capture | Allowed (with user prompt) | () — deny | Screen capture initiated by XSS payload captures the entire desktop including other windows |
clipboard-read | Allowed (with user prompt) | () — deny unless needed | Clipboard may contain passwords, tokens, or sensitive data; deny unless the MCP UI legitimately needs it |
clipboard-write | Allowed | (self) | Writing to clipboard is needed for copy-to-clipboard buttons; restrict to same-origin |
A strict Permissions-Policy for MCP server admin UIs
# Caddy snippet for MCP admin UI server header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), bluetooth=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(self), geolocation=(), gyroscope=(), hid=(), identity-credentials-get=(), idle-detection=(), local-fonts=(), magnetometer=(), microphone=(), midi=(), otp-credentials=(), payment=(), picture-in-picture=(), publickey-credentials-create=(self), publickey-credentials-get=(self), screen-wake-lock=(), serial=(), speaker-selection=(), storage-access=(), usb=(), web-share=(), xr-spatial-tracking=()"
Per-iframe permission attenuation for tool output rendering
If your MCP client UI renders tool output in sandboxed iframes (for XSS containment), Permissions-Policy lets you deny specific APIs to the iframe even if the parent page has access to them. This is configured via the allow attribute on the <iframe> tag:
<!-- Tool output rendered in a sandboxed iframe -->
<iframe
id="tool-output-frame"
sandbox="allow-scripts"
allow="camera 'none'; microphone 'none'; geolocation 'none'; payment 'none'"
src="about:blank"
></iframe>
<script>
// Safely write sanitized tool output into the sandboxed iframe
function renderToolOutput(safeHtml) {
const frame = document.getElementById('tool-output-frame');
const doc = frame.contentDocument || frame.contentWindow.document;
doc.open();
doc.write('<!doctype html><html><body>' + safeHtml + '</body></html>');
doc.close();
}
</script>
Delegation principle: An iframe can never gain permissions that the parent page doesn't have. If the parent page has camera=() (denied), no allow="camera" on a child iframe can grant camera access. Permissions only flow downward — you attenuate in iframes, never amplify. Set the most restrictive policy at the top-level document, then further restrict at the iframe level as needed.
Checking effective permissions from JavaScript
// Check if a permission is granted, denied, or prompt
const result = await navigator.permissions.query({ name: 'camera' });
console.log(result.state); // 'granted' | 'denied' | 'prompt'
// If Permissions-Policy denies the API entirely, the permission state is 'denied'
// and the user will never see a browser prompt regardless of prior grants
// This is different from the user denying via the browser UI —
// Permissions-Policy denial is silently enforced, no error thrown until access attempt
// Verifying your Permissions-Policy is active:
// Attempt camera access; it should throw NotAllowedError immediately
navigator.mediaDevices.getUserMedia({ video: true })
.then(() => console.warn('MISCONFIGURED: camera access not blocked by Permissions-Policy'))
.catch(e => {
if (e.name === 'NotAllowedError') console.log('Permissions-Policy is blocking camera correctly');
});
SkillAudit findings
camera=() or microphone=() not set on admin UI — injected script from tool output XSS can attempt real-time audio/video capture of the admin's environmentpayment=() not set on MCP client UI — XSS payload can invoke Payment Request API to trigger unauthorized payment flows in browsers where payment methods are pre-registeredallow attribute restricting permissions — sandboxed iframe inherits parent's effective permissions including any browser-granted capabilitiesFeature-Policy set, not Permissions-Policy — Firefox 74+ and Chrome 88+ use Permissions-Policy; Feature-Policy is deprecated and ignored in modern browsersSee also: Content Security Policy · iframe sandbox security · Feature-Policy (deprecated)
Audit your MCP server for Permissions-Policy gaps
SkillAudit checks for missing Permissions-Policy headers, over-permissive browser API access, and iframe permission inheritance issues.
Run free audit →