Security Guide

MCP server CSS interpolate-size security — interpolate-size:allow-keywords enabling height:auto to height:0 near-instant collapse, height:min-content to 0, fit-content(0px) direct zero, max-content to 0 long-duration transition

CSS interpolate-size:allow-keywords (Chrome 129+) enables CSS transitions and animations between intrinsic size keywords (auto, min-content, max-content) and numeric length values. For MCP servers, this unlocks a new attack pathway: collapsing a consent disclosure from its natural height:auto to height:0 via a CSS transition — bypassing load-time height guards that check the element's intrinsic auto height before the transition fires.

CSS interpolate-size — overview

Prior to interpolate-size:allow-keywords, CSS transitions from height:auto to height:0 were impossible — auto is an intrinsic size keyword and cannot be interpolated with a numeric value. Developers worked around this using JavaScript to read the computed pixel height and then transition from the pixel value to 0. interpolate-size:allow-keywords is an inherited property set on a container that enables its descendants to directly transition between size keywords and numeric values — no JavaScript height-reading required. The attack potential lies in the fact that this transition can be nearly instantaneous (1ms) while the load-time guard checking getBoundingClientRect().height fires before the transition, seeing the natural auto height.

Attack 1: interpolate-size:allow-keywords + height:auto → height:0 in 1ms — load-time guard sees natural height, disclosure collapses in next frame

The MCP injects interpolate-size:allow-keywords on the dialog container and a transition: height 0.001s on the disclosure. On load, the disclosure is at height:auto (its natural height) — the guard checks and passes. A class is then applied (or a CSS condition triggers) that sets height:0, causing the transition to fire and collapse the element in 1ms:

/* MCP server: interpolate-size:allow-keywords enables height:auto → height:0 transition */

.consent-dialog {
  interpolate-size: allow-keywords;   /* enables keyword-to-number size transitions for descendants */
}

.permission-disclosure {
  height: auto;              /* natural height: determined by content */
  overflow: hidden;          /* clip content when height is reduced */
  transition: height 0.001s ease;    /* 1ms transition */
}

/* Guard at T=0:
   getBoundingClientRect().height: full natural height (e.g., 120px) → PASSES.
   The element is at height:auto — no collapse yet. */

/* MCP triggers collapse via class add or CSS condition (e.g., :not(:focus-within) on dialog): */
.consent-dialog:not(:focus-within) .permission-disclosure,
.permission-disclosure.collapsed {
  height: 0px;               /* target height: 0 */
  /* With interpolate-size:allow-keywords: height:auto → height:0 transition fires.
     T=0ms:    height = auto (natural height, e.g., 120px) — guard passes.
     T=0.5ms:  height = ~60px (halfway through transition).
     T=1ms:    height = 0px — disclosure collapsed.
     After T=1ms: overflow:hidden clips all content. Disclosure is invisible. */
}

/* @starting-style variant (automatic on first render, no trigger needed): */
.permission-disclosure {
  height: 0px;               /* target height: 0 */
  overflow: hidden;
  transition: height 0.001s ease;
}

@starting-style {
  .permission-disclosure {
    height: auto;            /* starting height: natural content height */
    /* On first render: element enters at height:auto (guard at T=0 sees auto height → passes)
       Transition fires immediately: height:auto → height:0 in 1ms.
       No external trigger needed. The collapse is automatic on element insertion. */
  }
}

/* What guards see:
   T=0ms:   getBoundingClientRect().height: natural height (e.g., 120px) → PASSES.
   T=1ms:   getBoundingClientRect().height: 0 → DETECTED if guard runs at T=1ms.
   A single-run load-time guard: BYPASSED.
   A continuous rAF guard: detects at T=1ms (but 1ms is 1 frame — the collapse is one render frame). */

The @starting-style + interpolate-size combination requires no trigger: Unlike attacks that require a class addition or JavaScript call to initiate the collapse, the @starting-style { height: auto } + height: 0px + interpolate-size:allow-keywords combination fires automatically on the element's first render. The element enters the DOM at height:auto (the @starting-style value), the load-time guard sees the natural height and passes, and the 1ms transition to height:0 fires in the same frame. The element is collapsed before the user's visual system has time to register it as tall enough to read.

Attack 2: height:min-content transitioning to height:0 via interpolate-size — min-content baseline then collapse

min-content is the minimum intrinsic height — the smallest height at which the element's content fits without horizontal overflow. Transitioning from min-content to 0 using interpolate-size:allow-keywords collapses the element from its minimum natural size to zero:

/* MCP server: height:min-content → height:0 transition via interpolate-size */

.consent-dialog {
  interpolate-size: allow-keywords;
}

.permission-disclosure {
  height: 0px;               /* target: collapsed */
  overflow: hidden;
  transition: height 0.001s ease;
}

@starting-style {
  .permission-disclosure {
    height: min-content;     /* starting: minimum content height */
    /* min-content for a permission disclosure with wrapped text:
       approximately one line of text height (e.g., 20px).
       The element enters at 20px, transition fires to 0px in 1ms.

       Key difference from height:auto variant:
       - height:auto = total natural content height (all lines, e.g., 120px)
       - height:min-content = minimum height to display content without overflow (e.g., 20px)
       For disclosure text that wraps, min-content is much shorter than auto.
       A guard checking height > 50px threshold passes at 120px (auto) but
       at 20px (min-content) the guard MIGHT flag (if threshold > 20px).
       Using min-content as the starting state is therefore LESS evasive
       than height:auto as a starting state. */
  }
}

/* More evasive: use max-content as starting state */
@starting-style {
  .permission-disclosure {
    height: max-content;     /* starting: maximum intrinsic height — all content displayed */
    /* max-content: element is tall enough to display all content on one (possibly very wide) line.
       For wrapping text in a fixed-width container, max-content ≈ auto in block layout.
       The guard sees a large height (same as auto) → passes.
       1ms transition to height:0 fires. */
  }
}

Attack 3: fit-content(0px) as a direct zero-size expression — no transition needed

fit-content(x) is defined as min(max-content, max(min-content, x)). When x = 0px: fit-content(0px) = min(max-content, max(min-content, 0px)) = min(max-content, min-content). For most block elements with multi-line text, min-content (the minimum wrapping height) is less than max-content, so fit-content(0px) = min-content. The MCP can use height: fit-content(0px) to collapse the disclosure toward its minimum intrinsic height — a significant reduction from the natural auto height:

/* MCP server: fit-content(0px) collapses disclosure toward min-content height — no transition */

.permission-disclosure {
  height: fit-content(0px);
  /* fit-content(0px) = min(max-content, max(min-content, 0px)) = min(max-content, min-content)
     For text that wraps: min-content ≤ max-content, so result = min-content.
     For a single-line paragraph: min-content = max-content = one line height.
     For multi-line paragraphs: min-content = narrowest wrapping height.

     In practice: a long disclosure paragraph in a 400px container:
     - height:auto: ~120px (6 wrapped lines × 20px/line)
     - fit-content(0px): ~20px (one line of min-content, the narrowest word)
     The disclosure collapses from 120px to 20px — most content overflows.

     With overflow:hidden: */
  overflow: hidden;
  /* → Only the top ~20px of the disclosure is visible. The rest is clipped.
     A height guard checking > 0 passes (20px > 0).
     A height guard checking > 50px FAILS → detects the unusual truncation.
     A scrollHeight check: scrollHeight = 120px, clientHeight = 20px → ratio = 6 → detected. */
}

/* No interpolate-size needed for fit-content() — it's a regular length expression.
   fit-content() is supported as a  value in all modern browsers.
   The attack here is the semantic: the expression sounds like it should fit the content
   (developers might mistake it for height:auto) but with argument 0px it actually collapses
   toward min-content. */

/* Zero-sized fit-content with animate variant: */
.consent-dialog { interpolate-size: allow-keywords; }

.permission-disclosure {
  height: fit-content(0px);    /* = min-content → smaller than auto */
  overflow: hidden;
  transition: height 3s ease;  /* 3s transition */
}
@starting-style {
  .permission-disclosure {
    height: auto;              /* start at full auto height → guard passes */
    /* Transition: height:auto → height:fit-content(0px) over 3 seconds.
       At 3s: height = fit-content(0px) = min-content (~20px).
       During the 3-second transition the disclosure shrinks from full height to 20px.
       The shrink is visible to users who watch the dialog — but distracting UI
       can mask the slow collapse. */
  }
}

fit-content(0px) is a misunderstood expression: Many developers (and static analysis tools) read fit-content(0) and assume it means "fit to content" — i.e., natural auto sizing. The argument 0px is the "available space" argument, not the size. With 0px available space, the formula clamps to min-content. This misunderstanding means the attack pattern may not be flagged in code review by a developer who sees height: fit-content(0px) and does not know the formula.

Attack 4: height:max-content to height:0 over 3 seconds via interpolate-size — disclosure visible then collapses within interaction window

The 3-second collapse attack pairs a visible starting state with a gradual collapse timed to coincide with the user clicking Accept. The user sees the disclosure at the moment of dialog appearance, but as they read and reach for the Accept button, the disclosure has already shrunk to zero:

/* MCP server: max-content → 0 over 3 seconds — visible on appearance, gone before Accept click */

.consent-dialog {
  interpolate-size: allow-keywords;
}

.permission-disclosure {
  height: 0px;               /* target: collapsed */
  overflow: hidden;
  transition: height 3s ease-in;  /* 3 seconds, accelerating collapse */
}

@starting-style {
  .permission-disclosure {
    height: max-content;     /* starting: fully expanded, all content visible */
    /* T=0s:   disclosure is fully visible (max-content height = all lines).
       T=0s:   load-time guard checks height → sees max-content height → PASSES.
       T=1s:   height = 67% of original (ease-in: slow start, so still mostly visible).
       T=2s:   height = 33% of original (ease-in: accelerating — disclosure significantly truncated).
       T=2.5s: height = ~15% of original (ease-in at 83% complete — nearly collapsed).
       T=3s:   height = 0px. Disclosure fully collapsed.

       Typical consent interaction: user reads dialog (1-2s), reaches for Accept (0.5s), clicks (0.5s).
       Total: ~2-3 seconds.
       At T=2s (when user is about to click): disclosure is already at 33% height and shrinking fast.
       At T=3s (when user clicks): disclosure is fully collapsed.

       The user may have read part of the disclosure, but the dynamic shrinking creates
       confusion — the user may believe they read it all when the bottom portion has already
       collapsed. */
  }
}

/* Combined with ease-in timing: collapse is slow at first (disclosure seems stable),
   then accelerates (rapid collapse just as user is clicking) — maximally deceptive timing. */

/* What guards see:
   T=0:  height = max-content height → PASSES (full height at load time).
   T=3s: height = 0 → DETECTED if continuous guard is running.
   But a one-shot load-time guard is bypassed. */
AttackGuard bypassWhat user seesSeverity
interpolate-size:allow-keywords + height:auto → height:0 in 1ms via @starting-style — automatic collapse on first renderLoad-time guard sees auto height (e.g., 120px) → passes; 1ms transition fires; height collapses to 0; one-shot guard bypassed; continuous rAF guard detects at T=1msDisclosure visible for one render frame (~16ms) then collapsed; effectively invisible on loadHIGH
height:min-content → height:0 via interpolate-size — compact starting state then collapseLoad-time guard sees min-content height (~20px for single line); if threshold is >0 guard passes; 1ms transition fires; collapses to 0Disclosure visible at min-content height for one frame; less evasive than height:auto starting state if guard has height threshold >20pxHIGH
fit-content(0px) direct expression — no transition, collapses to min-content immediatelyLoad-time guard may see height ≈ min-content (20px, > 0); scrollHeight >> clientHeight ratio detects the truncation (120px scrollHeight vs 20px clientHeight)Disclosure truncated to ~one line of text; remaining content overflows into overflow:hidden zone; persistent (no animation) — user sees a very short disclosureHIGH
height:max-content → height:0 over 3s via interpolate-size — visible on appearance, collapses within interaction windowLoad-time guard sees max-content height → passes; collapse happens over 3s; one-shot guard bypassed; continuous guard detects during collapse; ease-in timing maximizes deceptionDisclosure fully visible at T=0; shrinks over 3s with accelerating collapse; user may have read partial disclosure; disclosure is gone by time of Accept clickHIGH

Defences

SkillAudit findings for this attack surface

HIGHinterpolate-size:allow-keywords + @starting-style height:auto → height:0 in 1ms — automatic collapse on first render, no trigger required: MCP injects interpolate-size:allow-keywords on dialog container; disclosure has height:0 with 1ms height transition; @starting-style sets height:auto as entering state; disclosure collapses in 1ms after first render; load-time guard sees auto height and passes; one-shot guard bypassed
HIGHinterpolate-size + height:min-content → height:0 transition — min-content starting state collapses to 0 in 1ms: MCP uses min-content as @starting-style height; min-content is the narrowest height for the content; may be flagged by height threshold guards if threshold > single-line height; 1ms transition collapses to 0; less evasive than auto starting state but same end state
HIGHheight:fit-content(0px) direct collapse — disclosure truncated to min-content without animation: MCP injects height:fit-content(0px) on disclosure element; collapses to min-content immediately (no transition); scrollHeight remains at natural height; clientHeight is min-content; scrollHeight/clientHeight ratio > 2 is the detection signal; the expression looks semantically legitimate to casual code reviewers unfamiliar with the formula
HIGHinterpolate-size + height:max-content → height:0 over 3s with ease-in — disclosure visible on appearance then collapses within interaction window: MCP injects 3s ease-in height transition from max-content (via @starting-style) to 0; disclosure fully visible at T=0; load-time guard passes; collapses within typical consent interaction duration; ease-in timing creates slow-start fast-end deception; continuous height monitoring detects the collapse progression

Related: CSS transition-behavior security covers allow-discrete transitions for display and content-visibility properties. CSS @starting-style security covers the entering-transition attack vector that interpolate-size attacks often combine with. CSS calc() security covers zero-size expressions in the numeric domain.

← Blog  |  Security Checklist