Blog · MCP Server Security

MCP server IntersectionObserver security — visibility timing side channels, element presence oracle, and cross-origin iframe leakage in MCP UIs

IntersectionObserver reports whether a DOM element intersects with the viewport or a parent container. In an MCP UI, that report is available to any same-origin script — including a compromised MCP skill's injected code. The callback fires with the element's boundingClientRect, intersectionRatio, and time, revealing when tool output enters the viewport, how tall the rendered output is, and whether specific result elements are present — all without reading the element's text content.

The visibility oracle attack in MCP UIs

Consider an MCP UI that renders each tool result in a separate container element with a data-tool-id attribute. A compromised MCP skill injects a script that creates an IntersectionObserver targeting all elements matching [data-tool-id]. When any tool result container enters the viewport, the observer callback fires with:

From the callback the attacker can infer: which tool result is now being viewed by the user (presence oracle), the approximate length of the tool output (element height correlates with content length for text-heavy results), and how long the user spent viewing each result (by correlating isIntersecting: true and isIntersecting: false timestamps). The callback fires with no user gesture required and no permission prompt — IntersectionObserver is a standard Web API available to all scripts with DOM access.

Why this matters for MCP: If the MCP UI renders tool results that contain sensitive data (API responses, database query results, user records), the visibility oracle reveals which results were rendered and viewed — even if the attacker cannot read the text content directly. Combined with a MutationObserver (which can read content), IntersectionObserver adds a timing dimension: the attacker knows when sensitive content became visible.

Content-length inference via bounding rect

For text-based MCP tool output, element height correlates with content length. A result containing a 10-line API response renders taller than a one-line "success" confirmation. The boundingClientRect.height from the IntersectionObserver callback does not reveal the text, but it narrows the space of possible outputs. In combination with knowledge of the tool's output schema (available from the MCP server manifest), height correlation can identify specific outputs from a finite set of expected responses.

// Attacker's IntersectionObserver in injected script
const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      // Height reveals approximate content length
      const approxLength = Math.round(entry.boundingClientRect.height / 20); // chars per line estimate
      exfiltrate({
        toolId: entry.target.dataset.toolId,
        height: entry.boundingClientRect.height,
        time: entry.time,
        approxContentLines: approxLength
      });
    }
  }
}, { threshold: 0.1 }); // fires when 10% visible

document.querySelectorAll('[data-tool-id]').forEach(el => observer.observe(el));

Cross-origin iframe content fingerprinting

IntersectionObserver can observe elements inside the same-origin document. For cross-origin iframes embedded in an MCP UI (e.g., a sandboxed tool result preview), IntersectionObserver cannot read the iframe's DOM — but it can observe the iframe element itself. The intersection callback reports the iframe's boundingClientRect, which reflects the iframe's rendered dimensions. If the cross-origin content in the iframe causes the iframe's height to change (e.g., via postMessage height adjustment from the iframe to the parent), those dimension changes are observable via IntersectionObserver on the parent frame.

This is a weaker vector than direct DOM access, but it leaks whether specific content was loaded in the iframe (by correlating height changes with known content fingerprints) and whether the user scrolled to view the iframe (via isIntersecting transitions).

Defense: limit IntersectionObserver attack surface in MCP UIs

Attack vectorDefense
Injected script observing tool result containers Render tool results in a separate browsing context (sandboxed iframe with sandbox attribute) so injected scripts from tool output cannot access parent DOM
Height-based content length inference Normalize container heights to fixed values (e.g., fixed-height scrollable container) so bounding rect does not reveal content length
Visibility timing as user behavior oracle Do not expose tool execution timestamps to injected scripts; use CSP script-src to block inline scripts from tool output
IntersectionObserver on cross-origin iframe Avoid exposing cross-origin iframes that auto-resize based on content; use fixed-height iframe containers

The most effective defense is strict Content Security Policy that prevents any script from tool output from executing in the MCP UI context. IntersectionObserver is a DOM API — it requires script execution. If injected scripts cannot execute, the observer cannot be created.

// Strict CSP preventing tool-output script injection
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}';
  style-src 'self' 'unsafe-inline';
  frame-src 'none';
  object-src 'none'

SkillAudit check: SkillAudit scans MCP server manifests and accompanying UI documentation for patterns that suggest tool output is rendered without sandboxing, and flags UIs that lack a strict script-src CSP — the prerequisite for IntersectionObserver-based side channel attacks. Audit your MCP server →

SkillAudit findings

High Tool output rendered directly in parent document without sandboxed iframe. Injected scripts from tool results have full DOM access and can create IntersectionObserver instances targeting sensitive elements. −18 pts
High No script-src CSP. Inline scripts injected via tool output execute without restriction, enabling IntersectionObserver, MutationObserver, and other DOM observation APIs. −16 pts
Medium Tool result containers have dynamic heights that correlate with content length. Height information in IntersectionObserver callbacks can be used to infer approximate tool output length. −10 pts
Medium Cross-origin iframes with content-driven height resizing embedded in MCP UI. Height changes observable via IntersectionObserver reveal content fingerprint. −8 pts

See also: MCP server MutationObserver security (DOM exfiltration of tool output) · MCP server PerformanceObserver security (timing side channels)