Blog · MCP Server Security
MCP server ResizeObserver security — layout-based exfiltration, content-size oracle, data length inference, and ResizeObserver loop DoS in MCP UIs
ResizeObserver fires its callback every time a watched element's size changes. In an MCP UI, the tool result container grows to fit new content each time a tool call returns. An attacker with injected same-origin script can observe every resize event on every tool result container — inferring output length, detecting when results arrive, and correlating size changes with tool identity. A second attack is the ResizeObserver loop: if the observer callback modifies the element's size, the browser fires the callback again, creating an infinite loop that freezes the tab.
Content-size oracle via ResizeObserver
Every time an MCP tool returns a result and the UI renders it into a container, that container's layout changes. If the result is 50 characters long, the container is shorter than if the result is 5,000 characters. An attacker who can inject a ResizeObserver on the tool result container receives:
entry.contentRect.height— the new height after content insertionentry.contentRect.width— the container width (constant, but narrows inference)entry.borderBoxSize[0].blockSize— border-box height including padding- Callback timing — when the resize fired (correlated with tool call completion)
The height reveals the approximate content length. For structured output (JSON with a known schema), the height distribution is often narrow enough that an attacker can distinguish between a short "no results" response and a long "found N records" response.
// Attacker's ResizeObserver on tool result containers
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const height = entry.contentRect.height;
const toolId = entry.target.dataset.toolId;
// Height → approximate content size
// Combined with tool schema knowledge → narrow the output
exfiltrate({ toolId, height, timestamp: performance.now() });
}
});
document.querySelectorAll('[data-tool-result]').forEach(el => ro.observe(el));
// Also observe future results via MutationObserver
const mo = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.dataset?.toolResult) ro.observe(node);
}
}
});
mo.observe(document.getElementById('results'), { childList: true, subtree: true });
Combined attack: ResizeObserver (size oracle) + MutationObserver (content text) + IntersectionObserver (visibility timing) running in parallel from a single injected script gives the attacker a complete picture of tool output: when it arrived, how large it is, what it says, and when the user viewed it — all from standard Web APIs with no permissions required.
ResizeObserver loop denial of service
If the ResizeObserver callback modifies the size of the observed element (directly or via style change), the browser schedules another resize notification, which fires the callback again, which changes the size again — infinitely. Browsers detect this loop and throw a ResizeObserver loop completed with undelivered notifications error, but the detection is not guaranteed to terminate the loop before significant page freeze. A malicious MCP skill can trigger this intentionally:
// DoS via ResizeObserver loop
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
// Modify width → triggers another resize → callback fires again → infinite
entry.target.style.width = (entry.contentRect.width + 1) + 'px';
}
});
ro.observe(document.body); // observe the root — affects entire page layout
The loop causes continuous layout recalculation, consuming 100% of one CPU core and making the UI unresponsive. The defense at the application level is to not expose modifiable root elements to injected scripts, and at the CSP level to prevent inline script execution from tool output.
Defense patterns for MCP UIs
| Attack | Severity | Defense |
|---|---|---|
| ResizeObserver height oracle on tool result containers | High | Render tool results in sandboxed cross-origin iframe; same-origin scripts cannot observe iframe element dimensions via ResizeObserver in the way they can observe same-origin elements |
| Content-size inference for output classification | Medium | Pad tool result containers to fixed heights; use overflow: hidden on fixed-height containers so layout height does not vary with content length |
| ResizeObserver loop DoS | High | Block inline script execution via strict CSP script-src 'nonce-X'; injected scripts from tool output cannot create ResizeObserver instances |
| Combined observer attack (Resize + Mutation + Intersection) | Critical | Sandbox all tool output; strict CSP prevents any of these observers from being instantiated by injected code |
Fixed-height container pattern
For MCP UIs that cannot use sandboxed iframes for tool output, fixed-height containers with scrollable overflow prevent height-based content-length inference:
/* Tool result container — fixed height prevents content-size oracle */
.tool-result {
height: 400px; /* fixed — does not change with content length */
overflow-y: auto; /* content scrolls within fixed container */
contain: strict; /* layout containment — changes inside don't affect outside */
}
The contain: strict property adds layout, style, and size containment, preventing layout changes inside the container from propagating to ancestor elements. This limits ResizeObserver signal to the container itself (which no longer changes height) rather than its parent chain.
SkillAudit detection: SkillAudit checks MCP server UI manifests for dynamic-height tool result containers and flags them as a ResizeObserver attack surface when combined with missing CSP script-src directives. Audit your MCP server →
SkillAudit findings
script-src restriction. Injected scripts can create ResizeObserver instances that report content-length-correlated height changes for every tool result. −18 pts
contain: strict. Layout changes inside tool result containers propagate to ancestor elements, expanding the ResizeObserver observable surface. −10 pts
See also: MCP server IntersectionObserver security · MCP server MutationObserver security