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

SignalWhat it exposesAttack relevance
visibilitychangehiddenUser switched away from this tab (to another tab or another app)Attention timing: how long user spends on this page vs elsewhere
visibilitychangevisibleUser returned to this tabReturn-interval measurement; infers context-switching patterns
document.wasDiscardedTab was silently discarded by Chrome due to memory pressure and reloaded on revisitInfers user has many tabs open; reveals memory pressure = older or lower-RAM device
freeze eventPage is transitioning to frozen/discarded stateLast-chance beacon opportunity; detects background CPU throttling
resume eventPage restored from frozen state (bfcache or discard)Combined with visibilitychange: reconstructs session gaps
document.visibilityStateCurrent 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

HIGH
visibilitychange listener with per-interval timing + exfiltration — Recording focus/away intervals and sending them to a remote endpoint. Produces a complete attention timeline that reveals reading depth, multitasking patterns, and engagement with sensitive content.
HIGH
freeze event handler calling sendBeacon with surveillance data — Using the last-chance freeze event to guarantee delivery of collected data before tab death. Exploits the same mechanism legitimate analytics tools use for session-end beacons.
MEDIUM
document.wasDiscarded check on load + exfiltration — Reading the discard flag at startup and sending it as a device/behavior signal. Infers tab-count habits and device RAM constraints as behavioral fingerprint components.
MEDIUM
visibilitychange + IntersectionObserver combined reading timer — Combining page focus state with scroll position to measure per-section reading time. Produces sensitive behavioral data on healthcare, financial, and legal content.
LOW
resume event with sessionStorage read of prior surveillance state — Using the resume event to reload a surveillance buffer from sessionStorage after a freeze/discard cycle. Enables multi-session state continuity without server-side storage.

Defense

Related: Performance Timeline deep dive · Intersection Observer security · Event Timing security

Scan your MCP server for Page Lifecycle surveillance risks

Paste a GitHub URL. Get a graded security report in 60 seconds.

Run free audit →