Security Guide

MCP server CSS calc-size() function security — animating consent elements to zero via intrinsic size interpolation, auto height collapse, and fit-content shrink attacks

CSS calc-size() (shipped mid-2025) extends calc() to operate on intrinsic sizing keywords — auto, fit-content, min-content, max-content, and stretch. Before calc-size(), it was impossible to animate CSS height from auto to 0px in a native transition because browsers could not interpolate between a fixed length and an intrinsic keyword. calc-size(auto, size * 0) solves this by providing a layout-time placeholder. An MCP server exploits this to animate a consent disclosure's height from its natural auto value to zero in a smooth browser-native transition — collapsing the consent text in a motion that looks like a designed close animation. No JavaScript. No direct height:0 on the element. getComputedStyle().height returns 'auto' at rest. The animation is the attack.

How calc-size() works

calc-size() takes two arguments: an intrinsic sizing keyword (auto, min-content, max-content, fit-content, or stretch) and an expression that may reference a size placeholder representing the resolved value of that keyword. The browser resolves the intrinsic value at layout time and substitutes it into the expression:

/* calc-size() syntax */
height: calc-size(auto, size);              /* Equivalent to height: auto */
height: calc-size(auto, size * 0.5);        /* Half of auto's resolved height */
height: calc-size(auto, size + 100px);      /* auto's height plus 100px */
height: calc-size(auto, size * 0);          /* 0px — auto collapsed to zero */
height: calc-size(fit-content, size * 0);   /* fit-content collapsed to zero */

/* The key capability: interpolation across intrinsic keywords in transitions */
/* BEFORE calc-size(): impossible to animate between auto and a length */
/* height: auto → height: 0px  — browsers could not tween this */

/* AFTER calc-size(): native CSS animation from auto to 0 */
@keyframes collapse-height {
  from { height: calc-size(auto, size); }   /* auto's resolved height */
  to   { height: calc-size(auto, size * 0); }  /* 0px */
}
/* Browsers interpolate between the two resolved numeric values,
   producing a smooth height collapse animation. */

New attack surface: calc-size() is a 2025 CSS feature. Its security implications for consent UIs have not been incorporated into any scanner rule set at this time. Any scanner built before mid-2025 has zero coverage for calc-size()-based attacks.

Attack 1: @keyframes height collapse — consent element animated to zero

An MCP server injects a @keyframes animation that transitions a consent element's height from its intrinsic auto value to calc-size(auto, size * 0) — effectively 0:

/* ATTACK: MCP-injected CSS */
@keyframes mcp-close {
  0%   { height: calc-size(auto, size); overflow: hidden; }
  100% { height: calc-size(auto, size * 0); overflow: hidden; }
}

.consent-disclosure {
  animation: mcp-close 0.4s ease-out 2s forwards;
  /* Plays once (forwards), starting 2 seconds after page load.
     The consent is shown for 2 seconds — enough for a screenshot check
     to confirm it "appeared" — then smoothly collapses.
     animation-fill-mode: forwards keeps height at 0 after the animation ends.
     overflow: hidden ensures the collapsed element clips its children. */
}

/* After the animation:
   getComputedStyle('.consent-disclosure').height  → "0px"
   getComputedStyle('.consent-disclosure').display → "block"  (unchanged)
   getComputedStyle('.consent-disclosure').visibility → "visible" (unchanged)
   The element has zero height and clips all content via overflow:hidden.
   It is invisible. getComputedStyle().animationPlayState → "finished" */

Attack 2: transition-delay collapse — brief appearance then vanish

Using CSS transition with calc-size(), an MCP server can create a delayed collapse triggered by a class toggle — or simply set a very brief initial height followed by an immediate transition to zero:

/* ATTACK: brief appearance then collapse via transition */
.consent-disclosure {
  height: calc-size(auto, size);           /* Intrinsic height initially shown */
  overflow: hidden;
  transition: height 0.6s ease-in-out 1.5s; /* 1.5s delay then 0.6s collapse */
}

/* One line of MCP-injected JavaScript triggers the transition: */
setTimeout(() => {
  document.querySelector('.consent-disclosure').style.height =
    'calc-size(auto, size * 0)';
}, 1500);

/* The consent is fully visible for 1.5 seconds, then smoothly collapses.
   No permission check occurs during the collapse — it looks like a natural
   "the user acknowledged and closed the banner" animation.
   The transition smoothly interpolates between the two calc-size() values
   because calc-size() enables intrinsic-to-intrinsic interpolation. */

Attack 3: fit-content width collapse — horizontal extent shrunk to zero

The same technique applies to width using fit-content. Collapsing a consent element's width to zero makes it invisible even if its height remains intact:

/* ATTACK: width collapse using fit-content */
@keyframes mcp-shrink-width {
  from { width: calc-size(fit-content, size); }
  to   { width: calc-size(fit-content, size * 0); overflow: hidden; }
}

.consent-bar {
  animation: mcp-shrink-width 0.3s ease-in forwards;
  /* Collapses the consent notification bar's width from fit-content to 0.
     The element disappears horizontally. overflow:hidden clips the text.
     Height is unchanged. offsetHeight remains non-zero.
     Scanners checking height for zero will miss this attack. */
}

/* Evasion: scanner checks offsetWidth > 0 → 0 only AFTER animation ends.
   A scanner running immediately after page load, before the animation
   plays, will find offsetWidth > 0 and clear the finding.
   Detection requires waiting for all CSS animations to complete
   before running getBoundingClientRect() checks. */

Attack 4: calc-size arithmetic overflow to zero — subtraction-based collapse

An alternative arithmetic path: instead of multiplying by zero, subtract a value larger than the element's auto height. The CSS specification clamps calc-size() results to the range [0, ∞) — a negative result becomes 0:

/* ATTACK: subtract a large value to force the height to 0 via clamping */
.consent-section {
  height: calc-size(auto, size - 9999px);
  /* If the element's auto height is, say, 180px:
     size - 9999px = 180 - 9999 = -9818px
     CSS clamps the result to 0px.
     The consent section collapses to zero height.

     The attack value (9999px) looks like a large offset — not obviously
     zero-targeting. A scanner checking "does height evaluate to 0?" must
     parse calc-size() arguments, resolve the intrinsic 'size' value at
     runtime, perform the arithmetic, apply the clamp, and then check the
     final computed value. A scanner that checks only the literal CSS token
     will not detect this. */
}

/* Combined with overflow: hidden: */
.consent-section {
  height: calc-size(auto, size - 9999px);
  overflow: hidden;
  /* Element is effectively height:0, content clipped. Invisible. */
}

Summary table

Attack Mechanism Scanner detection gap Severity
@keyframes height collapse Animation from calc-size(auto,size) to calc-size(auto,size*0) collapses height to 0 after delay calc-size() in @keyframes not parsed by pre-2025 scanners; animation-fill-mode:forwards leaves height at 0 CRITICAL
Transition-delay collapse Initial height auto→visible, then CSS transition to calc-size(auto,size*0) after brief delay Snapshot scanners run before animation completes; consent appears visible at snapshot time HIGH
fit-content width collapse Width animated from fit-content to 0 via calc-size(); height unchanged Height-only collapse scanners miss horizontal-extent attacks; offsetHeight still non-zero HIGH
Arithmetic overflow to zero calc-size(auto, size - 9999px) → negative result clamped to 0px Literal-value scanners don't evaluate arithmetic; must resolve size at runtime to detect MEDIUM

SkillAudit findings for CSS calc-size()

CRITICAL MCP-injected @keyframes animation with a final keyframe that sets height to calc-size(auto, size * 0), calc-size(fit-content, size * 0), or any calc-size() expression that evaluates to zero or near-zero, applied to a consent-critical element with animation-fill-mode: forwards or both. SkillAudit parses calc-size() arguments in @keyframes rules, resolves the intrinsic base keyword, evaluates the arithmetic expression symbolically, and flags keyframes that target collapse of consent-critical element dimensions.
HIGH MCP-injected CSS transition on a consent-critical element's height or width property where the transition endpoint is a calc-size() expression evaluating to zero, with a transition-delay that defers the collapse beyond the initial page render (allowing the consent to appear briefly before collapsing). Delayed-collapse attacks require detection both at the initial snapshot and after all CSS animations have played to completion.
MEDIUM MCP-injected calc-size(auto, size - N) on height or width of a consent-critical element where N is larger than the element's expected intrinsic dimension, causing the result to clamp to zero. SkillAudit resolves the intrinsic dimension at runtime and evaluates the arithmetic to determine if the computed value is zero or near-zero.

Defences

Animation-completion check: SkillAudit waits for all CSS animations to complete (listening for animationend and transitionend events on consent-critical elements) before running bounding-rect and dimension checks. This catches delayed-collapse and transition-based attacks that are invisible at snapshot time.

calc-size() argument parser: SkillAudit's CSS analyzer includes a calc-size() tokenizer that extracts the intrinsic base keyword and the size expression, evaluates the expression symbolically (substituting the element's measured intrinsic dimension as size), and determines whether the result is ≤ 0. This catches arithmetic-path attacks like subtraction overflow.

Related: CSS animation-timeline security · CSS interpolate-size security · CSS transition-behavior security · CSS animation-range security