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

DirectiveDefaultRecommended for MCP UIReason
cameraAllowed (with user prompt)() — denyMCP tool UIs don't need camera; XSS payload capturing video is a high-severity privacy risk
microphoneAllowed (with user prompt)() — denyAudio capture by injected script is a critical data exfiltration vector in agentic UIs
geolocationAllowed (with user prompt)() — denyPhysical location of the user is PII; MCP tool handlers do not need it
paymentAllowed() — denyPayment Request API in a compromised MCP UI can trigger payment flows the user didn't initiate
usbAllowed (with user prompt)() — denyWebUSB access from injected scripts enables hardware-level attacks
bluetoothAllowed (with user prompt)() — denyBluetooth scanning reveals physical proximity; can interact with paired devices
fullscreenAllowed(self) — same-origin onlyFullscreen requests from cross-origin iframes enable phishing overlays
display-captureAllowed (with user prompt)() — denyScreen capture initiated by XSS payload captures the entire desktop including other windows
clipboard-readAllowed (with user prompt)() — deny unless neededClipboard may contain passwords, tokens, or sensitive data; deny unless the MCP UI legitimately needs it
clipboard-writeAllowed(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

CRITICAL −20No Permissions-Policy header on MCP client UI — XSS payload can request camera, microphone, and geolocation without hitting the origin-level policy barrier; browser prompts become the only gate
HIGH −16camera=() or microphone=() not set on admin UI — injected script from tool output XSS can attempt real-time audio/video capture of the admin's environment
HIGH −14payment=() 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-registered
MEDIUM −10Tool output iframe lacks allow attribute restricting permissions — sandboxed iframe inherits parent's effective permissions including any browser-granted capabilities
MEDIUM −8Only Feature-Policy set, not Permissions-Policy — Firefox 74+ and Chrome 88+ use Permissions-Policy; Feature-Policy is deprecated and ignored in modern browsers

See 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 →