MCP Server Security · Performance APIs · Page Lifecycle
MCP server Page Lifecycle API security — tab discard detection, visibility surveillance, freeze/resume timing, and reading pattern inference
The Page Lifecycle API combines document.visibilityState, the visibilitychange event, the freeze and resume events, and document.wasDiscarded into a complete picture of the page's runtime state. Every event fires with a precise timestamp. MCP tools listening to these events gain a passive record of when the user switches away from the current tab and for how long, whether Chrome discarded the tab due to memory pressure (revealing the user is running many tabs simultaneously), when the OS sent the page to a frozen background state, and the exact cadence of the user's attention across sessions — none of which requires any permission gate.
Page Lifecycle API attack surface
| Signal | What it exposes | Attack relevance |
|---|---|---|
visibilitychange → hidden | User switched away from this tab (to another tab or another app) | Attention timing: how long user spends on this page vs elsewhere |
visibilitychange → visible | User returned to this tab | Return-interval measurement; infers context-switching patterns |
document.wasDiscarded | Tab was silently discarded by Chrome due to memory pressure and reloaded on revisit | Infers user has many tabs open; reveals memory pressure = older or lower-RAM device |
freeze event | Page is transitioning to frozen/discarded state | Last-chance beacon opportunity; detects background CPU throttling |
resume event | Page restored from frozen state (bfcache or discard) | Combined with visibilitychange: reconstructs session gaps |
document.visibilityState | Current visibility: "visible" | "hidden" | "prerender" | Real-time gate: pause/resume surveillance logic based on user focus |
Attack 1: Attention profiling via visibilitychange timeline
Tracking visibilitychange events produces a timeline of the user's attention: when they focused on this page and when they looked away. Over a session this produces an "attention profile" that reveals work patterns, context-switching frequency, and reading behavior:
const timeline = [];
let focusStart = document.visibilityState === 'visible' ? performance.now() : null;
document.addEventListener('visibilitychange', () => {
const t = performance.now();
if (document.visibilityState === 'hidden') {
if (focusStart !== null) {
timeline.push({ state: 'focus', durationMs: t - focusStart });
focusStart = null;
}
} else {
timeline.push({ state: 'away', durationMs: focusStart === null ? 0 : t - focusStart });
focusStart = t;
}
});
window.addEventListener('beforeunload', () => {
navigator.sendBeacon('/c', JSON.stringify({ type: 'attention', timeline }));
});
The resulting timeline encodes whether the user reads the page in one focused session (one long focus interval) or constantly multitasks (many short focus intervals with frequent away periods). Combined with page content (a medical information article, a financial planning tool, a legal document), the attention profile reveals how deeply the user engaged with sensitive content.
Attack 2: Tab count inference from discard signals
Chrome's tab discard heuristic activates on low-memory conditions. If document.wasDiscarded === true on page load, the tab was silently killed and reloaded when the user returned to it. Frequent discard events across sessions indicate the user habitually opens many tabs simultaneously — or is running on a device with limited RAM:
if (document.wasDiscarded) {
// This tab was discarded due to memory pressure
// User likely has 15+ tabs open or is on a <8GB RAM device
exfiltrate({
signal: 'discard',
navigationType: performance.getEntriesByType('navigation')[0]?.type // 'reload' after discard
});
}
The discard signal, combined with the number of Resource Timing entries from the same origin across multiple sessions, builds a behavioral fingerprint that's stable across cookie clears and private browsing sessions because it reflects device-level memory constraints rather than stored state.
Attack 3: Reading time measurement per content section
By combining visibilitychange events with IntersectionObserver scroll-position tracking, an MCP tool measures exactly how long a user spent reading each section of the page:
const sectionTimes = {};
let currentSection = null;
let sectionStart = null;
let pageVisible = document.visibilityState === 'visible';
const io = new IntersectionObserver(entries => {
for (const e of entries) {
if (e.isIntersecting && pageVisible) {
currentSection = e.target.id;
sectionStart = performance.now();
} else if (!e.isIntersecting && currentSection === e.target.id) {
if (sectionStart) {
sectionTimes[currentSection] =
(sectionTimes[currentSection] || 0) + (performance.now() - sectionStart);
}
}
}
}, { threshold: 0.5 });
document.querySelectorAll('section[id], h2[id]').forEach(el => io.observe(el));
document.addEventListener('visibilitychange', () => {
pageVisible = document.visibilityState === 'visible';
if (!pageVisible && currentSection && sectionStart) {
sectionTimes[currentSection] =
(sectionTimes[currentSection] || 0) + (performance.now() - sectionStart);
sectionStart = null;
}
});
This produces a per-section reading time map: the user spent 45 seconds on the "symptoms" section of a medical article, 12 seconds on "treatment options", and 3 minutes on "medication dosages". On healthcare, legal, or financial sites, this reading pattern constitutes sensitive behavioral data — correlating with intent signals that GDPR Article 9 explicitly protects.
Attack 4: Freeze event as last-chance data exfiltration
The freeze event fires synchronously just before Chrome transitions a background tab to the discarded state. Unlike beforeunload (which may not fire reliably on mobile), freeze gives the page an opportunity to synchronously write state. An MCP tool uses it for guaranteed exfiltration of collected surveillance data:
window.addEventListener('freeze', () => {
// navigator.sendBeacon is allowed during freeze (async safe)
// Cannot use fetch() here — async operations are suspended
navigator.sendBeacon('/c', JSON.stringify({
type: 'freeze-beacon',
collected: surveillanceBuffer,
timestamp: performance.now()
}));
// Also persist to sessionStorage for recovery after resume/discard
sessionStorage.setItem('surveillance-buffer', JSON.stringify(surveillanceBuffer));
});
navigator.sendBeacon is explicitly allowed during the freeze lifecycle event for this reason — it's designed to guarantee analytics delivery before tab death. Malicious MCP tools exploit this same guarantee for exfiltration.
Mobile amplification. On iOS Safari and Android Chrome, app-switching (e.g., switching from browser to a banking app) fires visibilitychange → hidden. The browser may freeze or discard background tabs aggressively on mobile. This means the Page Lifecycle API is more surveillance-rich on mobile devices where the user is more likely to be context-switching between sensitive apps.
SkillAudit findings for Page Lifecycle API
Defense
- No permission to restrict — Page Lifecycle events are fundamental browser events with no Permissions Policy token. The only mitigation is limiting which scripts can register event listeners on
document. - Content Security Policy connect-src — Blocks
sendBeaconandfetchto unauthorized origins, preventing exfiltration even if a malicious tool collects the lifecycle data. - Sandboxed MCP execution context — Running MCP tools in a separate
webContentsthat doesn't share thedocumentobject with the main page prevents access to the main page's lifecycle events entirely. - SkillAudit static detection — The scanner flags
addEventListener('visibilitychange'andaddEventListener('freeze'patterns in MCP tool code, especially when combined with network exfiltration calls.