Security Guide
MCP server CSS env() security — safe-area-inset device fingerprint, keyboard-inset-height typing detection, titlebar-area PWA context oracle, expanding fingerprinting surface
CSS environment variables via env() (Chrome 69+, Firefox 65+, Safari 11.1+) allow pages to read hardware-specific layout values set by the browser, not by CSS or JavaScript. These values — notch insets, keyboard height, titlebar dimensions — are optimized for responsive layout, but they also encode device identity, user behaviour, and runtime context that an MCP server can read via getComputedStyle. Unlike standard CSS properties that encode layout choices, env() values are set by the OS and browser and cannot be spoofed by the host application.
How CSS env() works
The env() function reads values from the browser's environment variable registry — a set of named values provided by the browser (not the page) that reflect hardware and OS state. The most common use is env(safe-area-inset-top) for iPhone notch compensation. Unlike var() (which reads CSS custom properties set by the page), env() reads values set by the browser based on the device, display configuration, and runtime context. Resolving an env() value in JavaScript requires either reading an inline style or a computed property value from an element that uses env() in its CSS.
Attack 1: env(safe-area-inset-*) values fingerprint device model and orientation
The safe-area-inset-top, safe-area-inset-bottom, safe-area-inset-left, and safe-area-inset-right environment variables expose the dimensions of the device's non-interactive display areas — the notch, home indicator, and rounded corners. These values are unique per device model and screen orientation. An MCP server that reads all four inset values constructs a device fingerprint that narrows down the exact phone model.
// Device fingerprint via safe-area-inset values
function readSafeAreaInsets() {
const probe = document.createElement('div');
probe.style.cssText = `
position: fixed;
top: env(safe-area-inset-top);
bottom: env(safe-area-inset-bottom);
left: env(safe-area-inset-left);
right: env(safe-area-inset-right);
width: 0; height: 0;
visibility: hidden; pointer-events: none;
`;
document.body.appendChild(probe);
const computed = getComputedStyle(probe);
const insets = {
top: parseFloat(computed.top),
bottom: parseFloat(computed.bottom),
left: parseFloat(computed.left),
right: parseFloat(computed.right)
};
document.body.removeChild(probe);
return insets;
}
// Known device inset signatures (portrait, viewport-fit: cover):
// iPhone 14 Pro: { top: 59, bottom: 34, left: 0, right: 0 }
// iPhone 14 Pro Max: { top: 59, bottom: 34, left: 0, right: 0 }
// iPhone 14/15: { top: 47, bottom: 34, left: 0, right: 0 }
// iPhone SE (3rd): { top: 20, bottom: 0, left: 0, right: 0 }
// iPad Pro 12.9" M2: { top: 24, bottom: 20, left: 0, right: 0 }
// No notch (Android): { top: 0, bottom: 0, left: 0, right: 0 }
// Combined with screen.width/height → identifies specific device in 2-3 probes
Requires viewport-fit:cover to be set: The safe-area-inset values are non-zero only when viewport-fit=cover is set in the page's viewport meta tag. If the host page doesn't use it, the values are all zero. However, an MCP server that can set the viewport meta tag (or override it via document.querySelector('meta[name=viewport]').content) can enable viewport-fit:cover itself — though this is a visible page change.
Attack 2: env(keyboard-inset-height) detects when the virtual keyboard is shown
keyboard-inset-height (Chrome 94+, part of the Virtual Keyboard API) is a CSS environment variable that equals the height of the virtual keyboard when it is shown, and 0 when the keyboard is hidden. This variable is non-zero only when the user has tapped on an input field and the software keyboard is displayed. An MCP server monitoring this value detects keyboard open/close transitions — revealing when the user is actively typing, when they dismiss the keyboard (indicating they finished input), and the exact height of the keyboard (which varies by device and user keyboard app).
// Keyboard visibility oracle via env(keyboard-inset-height)
const probe = document.createElement('div');
probe.style.cssText = `
position: fixed;
height: env(keyboard-inset-height, 0px);
bottom: 0; left: 0; width: 0;
visibility: hidden; pointer-events: none;
`;
document.body.appendChild(probe);
function getKeyboardHeight() {
return parseFloat(getComputedStyle(probe).height) || 0;
}
// Poll for keyboard state changes
let wasKeyboardOpen = false;
setInterval(() => {
const height = getKeyboardHeight();
const isKeyboardOpen = height > 0;
if (isKeyboardOpen !== wasKeyboardOpen) {
wasKeyboardOpen = isKeyboardOpen;
if (isKeyboardOpen) {
// User just opened a text input — they are about to type
// height value fingerprints which keyboard app they're using:
// iOS default: ~336px (iPhone 14), ~258px (landscape)
// Gboard: varies by user settings
} else {
// User dismissed keyboard — input session ended
// Timing gap between open and close encodes typing session duration
}
}
}, 100);
Typing session surveillance: Keyboard open/close timing creates a behavioral signal: the duration between keyboard show and hide encodes how long the user typed. Consecutive sessions (short gaps between open events) can be correlated with form field interactions. This works without access to the focused input element, without keyup/keydown event listeners, and without the Virtual Keyboard API permission.
Attack 3: env(titlebar-area-*) reveals PWA installation context
The Window Controls Overlay API (Chrome 98+) provides four env() variables — titlebar-area-x, titlebar-area-y, titlebar-area-width, titlebar-area-height — that reflect the geometry of the app's title bar area. These variables are non-zero only when the page is running as an installed PWA with display: window-controls-overlay in its manifest, and when the OS chrome is visible. Reading them lets an MCP server determine: (1) whether the page is running as an installed PWA or in a regular browser tab; (2) the exact OS platform (titlebar height varies: Windows ≈ 33px, macOS ≈ 28px, Linux/GNOME ≈ 37px); (3) the window width (titlebar-area-width changes as the user resizes the window).
// PWA context and OS fingerprint via titlebar-area env() variables
function readTitlebarArea() {
const probe = document.createElement('div');
probe.style.cssText = `
position: fixed;
top: env(titlebar-area-y, -1px);
left: env(titlebar-area-x, -1px);
width: env(titlebar-area-width, 0px);
height: env(titlebar-area-height, 0px);
visibility: hidden; pointer-events: none;
`;
document.body.appendChild(probe);
const computed = getComputedStyle(probe);
const titlebar = {
x: parseFloat(computed.left),
y: parseFloat(computed.top),
width: parseFloat(computed.width),
height: parseFloat(computed.height)
};
document.body.removeChild(probe);
return titlebar;
}
const titlebar = readTitlebarArea();
if (titlebar.height > 0) {
// Running as installed PWA with window-controls-overlay
// titlebar.height = 33 → Windows 10/11
// titlebar.height = 28 → macOS 12+
// titlebar.height = 37 → Linux GNOME
// titlebar.width = window width - window controls button area
// titlebar.x / .y always (0, 0) on Windows, non-zero on macOS RTL
} else {
// Running in a normal browser tab — not an installed PWA
}
Attack 4: expanding env() variables for foldables and display cutouts
The set of CSS environment variables is not frozen — Chromium regularly ships new env() names for new hardware features. Variables already available or in development include: env(fold-top), env(fold-left), env(fold-width), env(fold-height) (for foldable devices — encodes hinge position and crease dimensions); env(safe-area-inset-*) for secondary displays; and future variables for display cutout geometry (dynamic island, punch-hole cameras). Each new variable is an additional fingerprinting dimension that MCP servers can read without any permission prompt.
// Foldable device hinge position oracle
// (Available in origin trials; fold-* variables encode hinge geometry)
function readFoldableGeometry() {
const probe = document.createElement('div');
probe.style.cssText = `
position: fixed;
top: env(fold-top, -1px);
left: env(fold-left, -1px);
width: env(fold-width, 0px);
height: env(fold-height, 0px);
visibility: hidden; pointer-events: none;
`;
document.body.appendChild(probe);
const computed = getComputedStyle(probe);
const fold = {
top: parseFloat(computed.top),
left: parseFloat(computed.left),
width: parseFloat(computed.width),
height: parseFloat(computed.height)
};
document.body.removeChild(probe);
// If fold.width > 0 or fold.height > 0: device is foldable
// fold.top/fold.left encodes hinge axis position on screen
// fold.width encodes crease width (device-specific constant)
// Combined with screen dimensions: identifies Galaxy Fold vs Pixel Fold vs Surface Duo
return fold;
}
| env() variable | What it encodes | Attack | Browser support |
|---|---|---|---|
safe-area-inset-top/bottom/left/right | Notch size, home indicator, rounded corner insets | Device model + orientation fingerprint (requires viewport-fit:cover) | Chrome 69+, FF 65+, Safari 11.1+ |
keyboard-inset-height | Virtual keyboard height — 0 when hidden | Keyboard open/close state — typing session detection | Chrome 94+ |
titlebar-area-x/y/width/height | Window controls overlay titlebar geometry | PWA vs browser tab detection + OS platform fingerprint | Chrome 98+ (PWA only) |
fold-top/left/width/height | Foldable device hinge position and crease dimensions | Foldable device model identification — Samsung/Google/Microsoft distinction | Origin trial / Chromium experimental |
SkillAudit findings for CSS env() variables
safe-area-inset device fingerprinting: reading the four safe-area-inset env() values via a positioned probe element fingerprints the device model to a small set (2–4 candidates per inset signature), usable as a cross-site tracking identifier without navigator.userAgent or permissions.keyboard-inset-height typing detection: polling env(keyboard-inset-height) at 100ms intervals detects keyboard open and close events without element focus listeners, input event handlers, or any Virtual Keyboard API permission — enabling passive typing-session surveillance.titlebar-area context oracle: reading the titlebar-area env() variables distinguishes PWA from browser-tab context and identifies the OS platform (Windows / macOS / Linux) from the titlebar height — a stable hardware signal usable for tracking across sessions.Defences
Avoid viewport-fit=cover unless the app requires it: Safe-area-inset values are zero on pages without viewport-fit=cover in the viewport meta tag. If your AI agent host application doesn't need edge-to-edge display, omitting this parameter eliminates the safe-area fingerprinting attack surface.
Audit MCP server code for env() usage: SkillAudit flags MCP servers that inject elements with inline styles using env() or that inject CSS rules using env() on positioned probe elements. Legitimate MCP server UIs have rare use for hardware-specific env() variables in their own UI; applying them to probe elements is a strong indicator of fingerprinting intent.
Disable Virtual Keyboard API exposure in controlled runtimes: Electron apps can disable the Virtual Keyboard API that populates keyboard-inset-height via --disable-features=VirtualKeyboard. AI agent desktop apps should evaluate whether they need this API and disable it if MCP servers are untrusted.
Monitor for env() probes in MCP server output: MCP servers that create and immediately remove probe elements (append-read-remove pattern) are often executing device fingerprinting. Runtime monitoring of element creation/removal within short time windows can detect these probes even when the CSS injection does not persist.
Related: CSS container queries security · CSS individual transforms security · CSS trigonometric math security