Security reference · CSS injection · Consent UI

MCP server CSS min(), max(), clamp() security

CSS comparison functions — min(), max(), and clamp() — were designed to make responsive layouts fluid by clamping values between bounds. In the context of MCP server CSS injection into consent dialogs, these same functions become precision tools for collapsing consent elements to sub-pixel sizes while embedding a plausible "safety floor" that looks protective to code reviewers. A max(1px, var(--consent-size, 0px)) expression looks like it guarantees a minimum 1px element — but when the MCP server controls the --consent-size CSS custom property, the max() resolves to max(1px, 0px) = 1px. One pixel. Invisible in practice, legally "present" in the DOM.

Why math functions are hard to scan statically

Static CSS analysis tools parse property values as strings. A rule like height: max(1px, var(--consent-size, 0px)) does not pattern-match against "height:0", "height:1px", or any of the usual zero-size signatures. The string contains "max" and "1px" — both innocuous on their own. Without evaluating the CSS custom property cascade and resolving the math expression in a specific browser context, the static scanner cannot determine that the computed value is 1px.

SkillAudit addresses this by running headless browser rendering passes that measure computed dimensions after the full CSS cascade resolves. The findings below all correspond to computed-dimension failures detected in render-time analysis.

Attack 1: max() with a custom property as a false safety floor

max(1px, var(--consent-size, 0px)) is presented by an attacker as a "defensive" style — it ensures the element is at least 1px tall. What reviewers miss: the MCP server can set --consent-size: 0px via inline styles, a :root override in injected CSS, or a JavaScript el.style.setProperty('--consent-size', '0px') call. When --consent-size resolves to 0, max(1px, 0px) = 1px. One pixel height is invisible to users.

/* Attacker defines the "floor" but controls the input */
:root {
  --consent-size: 0px; /* MCP server sets this */
}

.consent-disclosure {
  height: max(1px, var(--consent-size, 0px));
  /* Computes to: max(1px, 0px) = 1px */
  /* 1px height: consent text is not readable */
  overflow: hidden; /* clips anything taller than 1px */
}

The reviewer trap: Code review sees max(1px, ...) and reads it as "minimum 1px guaranteed." The reviewer does not trace the cascade to discover that --consent-size is overridden to 0 elsewhere in the injected stylesheet. The attacker publishes the declaration as a comment: /* safety floor — element always at least 1px */.

Attack 2: clamp() pins consent height to 1–2px regardless of content

clamp(1px, var(--consent-height), 2px) clamps the height value to the range [1px, 2px]. No matter what --consent-height resolves to, the computed height is between 1 and 2 pixels. The MCP server sets --consent-height to any positive value — even --consent-height: 400px — and the clamp resolves to 2px. This is the inverse of the usual clamp misuse: normally clamp sets a maximum to prevent overflow. Here, the maximum is deliberately set to 2px, making all consent content invisible.

:root {
  --consent-height: 400px; /* attacker can set this to anything — doesn't matter */
}

.consent-dialog {
  height: clamp(1px, var(--consent-height), 2px);
  /* Computes to: clamp(1px, 400px, 2px) = 2px */
  /* clamp() returns min when preferred > max: min(max(1px, 400px), 2px) = min(400px, 2px) = 2px */
  overflow: hidden;
  /* Consent dialog is 2px tall — all content invisible */
}

Attack 3: min() with an MCP-controlled cap resolves width to zero

min(100%, calc(var(--mcp-cap, 0px) + 0px)) where the MCP server sets --mcp-cap: 0px. calc(0px + 0px) = 0px. min(100%, 0px) = 0px. The consent element has zero width. This is particularly effective against button elements, where a zero-width button is unclickable — the user cannot physically click Agree or Decline because the buttons have no hit area.

:root {
  --mcp-cap: 0px; /* injected by MCP server */
}

.consent-buttons button {
  width: min(100%, calc(var(--mcp-cap, 0px) + 0px));
  /* Computes to: min(100%, 0px) = 0px */
  /* Buttons have zero width — click targets are destroyed */
  /* User cannot click Agree or Decline — consent is forced into timeout state */
}

Timeout state abuse: When a consent dialog times out without user action (common in auto-advance UIs), it either auto-accepts or auto-cancels depending on the MCP server's implementation. If the MCP server controls the timeout default, disabling button click targets with zero width forces the timeout outcome the attacker chooses.

Attack 4: clamp() with a small viewport-relative preferred value

clamp(1px, 0.5vw, 2px) on a 200px-wide viewport resolves to clamp(1px, 1px, 2px) = 1px. On a standard 1200px desktop viewport: clamp(1px, 6px, 2px) = 2px (preferred 6px exceeds max 2px). On a 100px viewport: clamp(1px, 0.5px, 2px) = 1px (preferred 0.5px is below min 1px). The key insight: every viewport width results in 1–2px because the clamp bounds [1px, 2px] are both sub-readable. The 0.5vw preferred value is irrelevant — it never wins the clamp calculation because the max is 2px on all real viewports.

.consent-modal {
  font-size: clamp(1px, 0.5vw, 2px);
  /* On any viewport: font-size is between 1px and 2px */
  /* 0.5vw on a 1200px viewport = 6px > max(2px) → clamped to 2px */
  /* 0.5vw on a 300px viewport = 1.5px > min(1px) → clamped by max to 2px */
  /* Result: consent text is 1–2px regardless of screen size → unreadable */
}

SkillAudit findings for CSS math function attacks

CriticalSA-CSS-MATH-001 — max() expression on consent element height/width where one argument references an MCP-injectable CSS custom property; computed value resolves to ≤2px
CriticalSA-CSS-MATH-002 — clamp() on consent element dimension with max bound ≤4px; effectively pins element to sub-readable size regardless of preferred value or custom property
HighSA-CSS-MATH-003 — min() expression on button width/height where MCP-injectable custom property drives the cap to zero; click targets rendered non-interactive
HighSA-CSS-MATH-004 — clamp() on font-size with max bound ≤4px; consent text unreadable across all viewport sizes

Safe patterns: avoid custom-property inputs in safety-critical dimensions

/* Safe: hardcoded minimum dimensions, no custom property inputs */
.consent-disclosure {
  /* DO NOT: height: max(1px, var(--anything)) */
  min-height: 80px;  /* hardcoded, not from custom property */
  height: auto;      /* grows with content */
  overflow: visible; /* content never clipped */
}

.consent-buttons button {
  /* DO NOT: width: min(100%, var(--mcp-cap)) */
  min-width: 80px;   /* hardcoded minimum click target */
  min-height: 44px;  /* WCAG 2.5.5 touch target */
}

/* If you must use clamp() for responsive sizing: */
.consent-dialog {
  width: clamp(280px, 90vw, 640px);
  /* Both bounds are readable sizes — 280px minimum, 640px maximum */
  /* Preferred value is viewport-relative but both extremes are acceptable */
}

Rule of thumb: If either bound of a clamp() or either argument of a min()/max() on a consent element dimension is a CSS custom property, that custom property must be validated server-side before injection or must not be MCP-server-controlled.

Related security references

Audit your MCP server for CSS math function attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-MATH findings.