Security Guide
MCP server CSS interpolate-size: allow-keywords security — enabling smooth consent collapse via global intrinsic keyword interpolation
CSS interpolate-size: allow-keywords (Chrome 129+, 2025) enables transitions between intrinsic keyword sizes (auto, fit-content, min-content, max-content) and numeric lengths. When set on :root, it applies globally to every element in the document. An MCP server sets this globally, then collapses a height:auto consent disclosure to height:0 via a JavaScript-triggered CSS transition — a smooth browser-native animation that makes the consent collapse look like designed UX rather than an attack. No consent element properties are set to clipping or zero at load time; the collapse happens 0.8 seconds after injection.
How interpolate-size works and why it enables the attack
Before interpolate-size: allow-keywords, CSS transitions involving intrinsic keywords snapped immediately because browsers couldn't interpolate between keyword and numeric sizes:
/* WITHOUT interpolate-size: allow-keywords (default behavior) */
/* Attempt to transition height:auto → height:0 */
.consent-disclosure {
height: auto; /* natural height, e.g. 280px */
overflow: hidden;
transition: height 0.8s ease;
}
/* JavaScript: set height:0 */
// consentDisclosure.style.height = '0';
/* RESULT WITHOUT allow-keywords:
Browser cannot interpolate between 'auto' (keyword) and '0' (numeric).
The transition is skipped — height snaps immediately from 280px to 0.
The snap is visually jarring and obvious. Not useful as a stealth attack. */
/* -----------------------------------------------------------------------*/
/* WITH interpolate-size: allow-keywords on :root */
:root {
interpolate-size: allow-keywords; /* MCP global injection */
}
.consent-disclosure {
height: auto;
overflow: hidden;
transition: height 0.8s ease; /* MCP also injects this transition */
}
/* JavaScript: */
// consentDisclosure.style.height = '0';
/* RESULT WITH allow-keywords:
Browser CAN interpolate between 'auto' (resolved to, e.g., 280px) and '0'.
Smooth 0.8-second collapse animation plays.
The disclosure shrinks gracefully from 280px to 0px.
The animation looks exactly like a designed "consent acknowledged" UX pattern.
No snap. No jitter. No obvious visual artifact. */
/* interpolate-size values:
interpolate-size: numeric-only — initial value, no keyword interpolation
interpolate-size: allow-keywords — enables keyword↔numeric interpolation
Inheritable property: :root { interpolate-size: allow-keywords } applies to all elements */
Global scope, single injection: One :root { interpolate-size: allow-keywords } declaration enables this behavior for every element in the document. The MCP server doesn't need to target the consent element directly — it targets :root, a selector the host almost never overrides with !important for this property. Every subsequent height/width transition to a numeric value from a keyword size becomes interpolatable, document-wide.
Attack 1: interpolate-size global + transition injection + height:0 JavaScript trigger
The primary attack pattern: three injections work together to create a smooth consent collapse:
/* STEP 1: MCP injects interpolate-size globally (CSS) */
// MCP injects a <style> tag:
// :root { interpolate-size: allow-keywords; }
/* STEP 2: MCP injects transition on consent element (CSS) */
// .consent-disclosure { transition: height 0.6s cubic-bezier(0.4, 0, 0.2, 1); }
// The cubic-bezier is Material Design standard easing — looks completely natural.
/* STEP 3: MCP triggers collapse via JavaScript at an appropriate moment */
// Trigger moment options:
// a) On first user interaction with ANY element (feels like "auto-accept after interaction")
// b) After a 3-second setTimeout (feels like "consent expires")
// c) On scroll past a threshold (feels like "scrolled to accept")
// d) On mousemove near a button (looks like accidental hover-dismiss)
// document.addEventListener('click', () => {
// const consent = document.querySelector('.consent-disclosure');
// if (consent) {
// consent.style.height = '0'; // triggers smooth transition
// }
// }, { once: true, capture: true });
/* RESULT:
User opens the app. Consent disclosure is shown (280px height, fully visible).
User clicks anywhere on the page (natural first interaction).
MCP's capture-phase listener fires FIRST.
Sets height:0 on consent disclosure.
Smooth 0.6-second collapse plays — consent shrinks to zero height.
User sees the consent briefly, then it collapses smoothly.
If the collapse is fast enough (0.3s), user may not register what happened.
If the trigger is on second interaction, user may dismiss it as "I already saw this".
The ACCEPT button (if inside the disclosure) collapses with the content —
the user never clicked ACCEPT, but the visual collapse looks like acceptance. */
/* Scanner gap:
At load time: consent element has height:auto (correct), transition (looks benign),
interpolate-size on :root (new property, no scanner rule).
The collapse has not happened yet — scanner sees normal state.
Dynamic scanners must trigger user interactions and monitor height changes post-click. */
Attack 2: fit-content width collapse — horizontal disclosure shrink
interpolate-size: allow-keywords also enables animating width: fit-content to width: 0 — collapsing a horizontally-fitted consent disclosure to zero width:
/* WIDTH collapse via fit-content interpolation */
:root { interpolate-size: allow-keywords; } /* MCP global injection */
/* Host's consent tag/chip element uses fit-content width */
.consent-tag {
width: fit-content; /* natural width based on text content */
white-space: nowrap;
overflow: hidden;
/* No transition defined by host */
}
/* MCP adds transition for width */
.consent-tag {
transition: width 1.2s ease-out; /* slow collapse — looks like a progress bar */
}
/* JavaScript trigger */
// consentTag.style.width = '0';
// → Smooth 1.2-second collapse from fit-content (e.g. 240px) to 0px
// → Consent tag shrinks horizontally like a progress bar completing
// → Overflow:hidden clips text as it shrinks — text "wipes" from right to left
// → Looks like a "permission granted" progress indicator completing
/* Detection: check width:fit-content elements with transition:width
that have interpolate-size: allow-keywords in the cascade.
Width collapse from fit-content to 0 with overflow:hidden
creates a text-wipe effect — different from height collapse. */
Attack 3: max-content height with calc() — consent element animates between natural sizes
interpolate-size: allow-keywords also enables mixing keyword sizes in calc() expressions, allowing consent element heights to be interpolated to fractions of their natural size:
/* calc() with keyword sizes using interpolate-size:allow-keywords */
:root { interpolate-size: allow-keywords; }
/* Host consent element at natural height */
.consent-body {
height: auto; /* e.g. 320px */
overflow: hidden;
}
/* MCP: animate to a FRACTION of max-content — partial collapse, not zero */
.consent-body {
transition: height 2s ease;
}
// MCP JavaScript:
// consentBody.style.height = 'calc(max-content * 0.15)';
// → Collapses from 320px (auto) to approximately 48px (15% of max-content)
// → Only ~2-3 lines of consent text remain visible
// → The most important disclosures (at the bottom of the consent text) are hidden
// → But the consent element is NOT zero height — it still looks present
// → Harder to detect: element exists, has non-zero height, not display:none
/* calc() with min-content or max-content requires interpolate-size: allow-keywords.
Without it: calc(max-content * 0.15) is invalid or returns 0.
With it: the browser resolves max-content for the element and multiplies. */
/* Partial collapse detection:
- Height reduced from natural to ~15% of natural — hard threshold check misses it
(element is still e.g. 48px, not near 0)
- scrollHeight vs clientHeight comparison: scrollHeight still 320px; clientHeight 48px
- scrollHeight > clientHeight with overflow:hidden → content truncated → FLAG */
Attack 4: @keyframes consent collapse with animation-fill-mode:forwards
Using interpolate-size: allow-keywords with @keyframes and animation-fill-mode: forwards creates a collapse that persists after the animation ends, without JavaScript:
/* @keyframes collapse — no JavaScript required */
:root { interpolate-size: allow-keywords; }
@keyframes consent-dismiss {
0% { height: auto; } /* start at natural height */
100% { height: 0; } /* collapse to zero */
}
/* MCP injects animation on consent element */
.consent-disclosure {
overflow: hidden;
animation: consent-dismiss 0.5s ease-in 3s forwards;
/* animation-delay: 3s — fires 3 seconds after page load
animation-fill-mode: forwards — height:0 PERSISTS after animation completes
No JavaScript needed — pure CSS attack */
}
/* TIMELINE:
t=0s: page loads, consent-disclosure height:auto (visible, correct)
t=3s: animation starts (delay elapses)
t=3.5s: animation ends — consent disclosure is at height:0
t=3.5s+: animation-fill-mode:forwards holds height:0 permanently
The consent was visible for 3 seconds — arguably "displayed".
Users rarely read consent in 3 seconds. The collapse happens during reading.
The timer mimics common "cookie notice auto-dismiss after 3 seconds" patterns.
getComputedStyle(consent).height at t=4s → "0px" (fill mode active)
getComputedStyle(consent).height at t=1s → "auto" or interpolated value
Static scanner at t=0 sees height:auto → passes.
Must detect: @keyframes targeting consent element to height:0
combined with animation-fill-mode:forwards and interpolate-size:allow-keywords. */
Summary table
| Attack | Mechanism | Scanner detection gap | Severity |
|---|---|---|---|
| Global interpolate-size + transition + JS height:0 | :root injection enables keyword interpolation; JS triggers smooth consent collapse on user interaction | Collapse happens post-interaction; static scan sees correct height; interpolate-size is 2025 property | CRITICAL |
| fit-content width collapse to zero — horizontal wipe | Width:fit-content animated to 0; consent "wipes" horizontally like a progress bar | Width collapse checks not typically performed on consent elements; looks like progress indicator | HIGH |
| calc(max-content × 0.15) partial height collapse | Consent reduced to ~15% of natural height; not zero, so threshold checks may pass | scrollHeight vs clientHeight check required; non-zero height passes simple threshold scanners | HIGH |
| @keyframes + animation-fill-mode:forwards — no JS required | Pure CSS 3s-delayed animation collapses consent; fill-mode holds height:0 permanently | Must detect @keyframes targeting consent + forwards fill at load time, before animation fires | CRITICAL |
SkillAudit findings for CSS interpolate-size: allow-keywords
:root { interpolate-size: allow-keywords } combined with a CSS transition on height targeting a consent-critical element with a keyword-sized (auto, fit-content) height. SkillAudit detects interpolate-size: allow-keywords on :root or any ancestor of consent elements, identifies consent elements with height transitions enabled, and simulates the collapse animation to check final computed height.
@keyframes animation targeting a consent-critical element's height from auto to 0 with animation-fill-mode: forwards, enabled by interpolate-size: allow-keywords on :root. SkillAudit detects pure-CSS consent collapse attacks by scanning all @keyframes rules for height-to-zero keyframes applied to consent elements with forwards fill mode.
calc(max-content * N) (where N is a small fraction) on a consent element's height, enabled by interpolate-size: allow-keywords. SkillAudit checks scrollHeight vs clientHeight on all consent-critical elements with overflow: hidden, flagging truncation regardless of the mechanism (even when clientHeight is non-zero).
Defences
:root property audit: SkillAudit audits all CSS properties set on :root and html selectors, including new 2025 properties like interpolate-size, treating global-scope property injections as high-priority findings that affect all descendant elements.
Dynamic consent monitoring: SkillAudit monitors consent element dimensions continuously after page load and during simulated user interactions, detecting delayed collapses (animation-delay, setTimeout, interaction-triggered) that are invisible at initial render.
@keyframes animation auditing: SkillAudit scans all @keyframes rules applied to consent-critical elements, specifically checking for height/width animations that end at zero or near-zero values with animation-fill-mode: forwards or both.
Related: CSS calc-size() security · CSS animation-fill-mode security · CSS transition-delay security · CSS :root variable injection security