Security Guide

MCP server CSS font-size fluid clamp() security — mobile viewport collapse to 0px, consent text invisibility, and scanner gap

CSS font-size: clamp(min, preferred, max) enables fluid typography that scales smoothly with viewport width. An MCP server with CSS injection can construct a clamp formula where the minimum is set to 0px and the preferred expression is calibrated to evaluate below zero at common mobile viewport widths — causing clamp() to return its minimum of 0px, making the consent disclosure text completely invisible on mobile. The formula reads as legitimate responsive typography to any static analysis tool. Only runtime evaluation via getComputedStyle().fontSize reveals the collapse, because the preferred expression is a valid calc() expression that returns a negative value at narrow viewports.

How clamp() evaluates — and how it collapses

The CSS clamp(min, preferred, max) function returns max(min, min(preferred, max)). For font-size: clamp(0px, preferred, 14px):

/* Legitimate fluid typography: font scales from 12px at 320px viewport
   to 16px at 1280px viewport, never below 12px or above 16px. */
p {
  font-size: clamp(12px, 0.42vw + 10.65px, 16px);
}

/* At 320px viewport: 0.42 × 3.2 + 10.65 = 1.344 + 10.65 = 11.994px → clamped to 12px
   At 1280px viewport: 0.42 × 12.8 + 10.65 = 5.376 + 10.65 = 16.026px → clamped to 16px
   Font is always readable — minimum 12px.

   ATTACK: An MCP server replaces this with: */

.consent-disclosure {
  font-size: clamp(0px, 2vw - 10px, 14px);
  /* At 320px viewport: 2 × 3.2 - 10 = 6.4 - 10 = -3.6px → clamp returns 0px
     At 375px viewport: 2 × 3.75 - 10 = 7.5 - 10 = -2.5px → clamp returns 0px
     At 390px viewport: 2 × 3.9 - 10 = 7.8 - 10 = -2.2px → clamp returns 0px
     At 500px viewport: 2 × 5 - 10 = 10 - 10 = 0px → clamp returns 0px (threshold!)
     At 501px viewport: 2 × 5.01 - 10 = 0.02px → tiny text, barely visible
     At 800px viewport: 2 × 8 - 10 = 6px → readable
     At 1000px viewport: 2 × 10 - 10 = 10px → clamped to 14px? No: 10 < 14, so 10px.

     RESULT: text is 0px on all mobile viewports (320–500px) — perfectly invisible.
     On desktop (> ~700px), text gradually becomes visible again.
     Static analysis sees a plausible fluid scale: min=0px looks like "no minimum restriction",
     the vw expression looks like a scaling formula. No obvious 0 injection. */
}

Key detection gap: font-size: 0px is a trivial pattern that static scanners flag immediately. font-size: clamp(0px, 2vw - 10px, 14px) looks like a valid fluid type scale — only the combination of a zero minimum with a preferred expression that crosses zero within the mobile viewport range constitutes the attack. Evaluating that requires knowing the target viewport width, which static analysis does not have.

Attack 1: Zero-minimum clamp calibrated to collapse at mobile viewports

The most direct attack: set min = 0px and choose the preferred vw coefficient and offset so the crossover point (where preferred = 0) falls at or above the upper bound of common mobile viewport widths (roughly 430px for modern iPhones, 412px for Android flagship).

/* The crossover point for clamp(0px, A*vw - B, max) is at:
   viewport_width = B / A (in pixels, where A is in px/px, i.e. unit-less vw coefficient)
   e.g.: clamp(0px, 2vw - 10px, 14px)  → crossover at 500px
         clamp(0px, 3vw - 15px, 16px)  → crossover at 500px
         clamp(0px, 2.5vw - 12px, 14px) → crossover at 480px
         clamp(0px, 2vw - 9px, 14px)   → crossover at 450px — covers all phones up to 450px wide
         clamp(0px, 1.5vw - 7px, 14px) → crossover at ~467px

   An attacker chooses crossover at 430–500px to ensure all mobile viewports
   (320px–430px) receive 0px font-size. Desktop viewports (> 768px) see readable text. */

.consent-text {
  font-size: clamp(0px, 2vw - 9px, 14px);
  /* Mobile audit (375px): 2 × 3.75 - 9 = -1.5px → 0px — INVISIBLE
     Mobile audit (428px): 2 × 4.28 - 9 = -0.44px → 0px — INVISIBLE
     Tablet (768px): 2 × 7.68 - 9 = 6.36px → 6.36px — tiny but not zero
     Desktop (1024px): 2 × 10.24 - 9 = 11.48px → 11.48px — readable

     Detection: evaluate preferred expression at known mobile viewports.
     If result < 0 AND min = 0px, computed font-size = 0px on that viewport. */
}

Attack 2: Negative-offset preferred expression with small vw coefficient

Using a small vw coefficient with a large negative offset creates a formula that looks like gentle scaling but collapses to zero across a wide range of viewports including tablets:

/* Small vw coefficient with large negative offset — collapse extends to tablet */
.consent-banner {
  font-size: clamp(0px, 1vw - 8px, 14px);
  /* Crossover: viewport = 800px (where 1vw = 1px per 100px of viewport)
     → 1 × 8 - 8 = 0px at 800px
     → 0px below 800px — covers ALL mobile and tablet viewports
     → Readable only on 1920px desktop: 1 × 19.2 - 8 = 11.2px

     Looks like gentle fluid scaling (1vw coefficient = very gradual scale).
     Offset -8px looks like a reasonable design offset. Combined: 0px on all phones+tablets. */
}

/* Even more subtle: mix in calc() */
.consent-banner {
  font-size: clamp(0px, calc(2vw - 10px), clamp(12px, 1.5vw + 4px, 16px));
  /* Inner clamp as the max expression makes both static analysis and developer
     review significantly harder. The overall formula still collapses to 0px
     at viewports below 500px. */
}

Attack 3: Custom property indirection hiding the zero minimum

An MCP server can set the clamp minimum via a CSS custom property, so that static analysis sees a variable reference rather than a literal 0px minimum, and cannot determine the minimum value without resolving the custom property chain:

/* MCP server injects custom property and clamp simultaneously */
:root {
  --consent-font-min: 0px;   /* Injected by MCP server into :root */
}

.consent-disclosure {
  font-size: clamp(var(--consent-font-min), 2vw - 10px, 14px);
  /* Static analysis sees var(--consent-font-min) as the minimum.
     Without resolving the custom property, the scanner cannot determine
     that the minimum is 0px.

     STATIC ANALYSIS REQUIRED STEPS:
     1. Find clamp() in font-size declaration
     2. Identify first argument is var(--consent-font-min)
     3. Look up --consent-font-min across all injected stylesheets
     4. Find it resolves to 0px (possibly through MCP-injected :root or element-level override)
     5. Then evaluate whether preferred expression can return < 0 at mobile viewports

     Most scanners stop at step 2: they see var() and defer, unable to evaluate. */
}

/* More layers of indirection */
:root {
  --type-scale-min: 0px;
  --consent-font-min: var(--type-scale-min);  /* One hop */
}
/* Now the scanner must resolve two hops to find 0px. */

Custom property resolution requirement: SkillAudit resolves CSS custom properties (including MCP-injected ones in :root or element-level style attributes) before evaluating clamp() expressions. A scanner that defers on var() arguments cannot detect this attack variant.

Attack 4: Nested clamp() with a controlled inner expression

CSS allows nesting clamp() inside clamp(). An MCP server can make the outer maximum a nested clamp whose inner expression evaluates to zero at mobile viewports, causing the entire outer clamp to collapse regardless of its own minimum:

/* Nested clamp: the MAXIMUM of the outer clamp is itself a clamp that
   evaluates to 0px at mobile viewports. */
.consent-text {
  font-size: clamp(12px, 1.5vw + 4px, clamp(0px, 2vw - 10px, 14px));
  /* At 375px mobile viewport:
     Inner clamp: clamp(0px, 2×3.75 - 10, 14) = clamp(0px, -2.5px, 14px) = 0px
     Outer clamp: clamp(12px, 1.5×3.75 + 4, 0px) = clamp(12px, 9.625px, 0px)
     The outer clamp has min=12px and max=0px — an inverted range where min > max.

     CSS spec: when min > max in clamp(), the minimum wins.
     Result: clamp(12px, 9.625px, 0px) = 12px (minimum wins in inverted range)

     Hmm — this variant actually doesn't collapse. The attacker's control via the
     max nested clamp is defeated by the spec behavior when min > max.

     Correct attack: put the collapsing expression as the PREFERRED argument, not max: */
}

.consent-text {
  font-size: clamp(0px, clamp(-1px, 2vw - 10px, 14px), 16px);
  /* At 375px: inner clamp = clamp(-1px, -2.5px, 14px) = -1px (clamped to inner min)
     Outer clamp: clamp(0px, -1px, 16px) = 0px (outer min wins)
     At 800px: inner clamp = clamp(-1px, 6px, 14px) = 6px
     Outer clamp: clamp(0px, 6px, 16px) = 6px — readable

     This is the correct form: nested clamp as the PREFERRED expression
     ensures mobile evaluates to a negative value even with inner clamping. */
}

/* Detection: must recursively evaluate nested clamp() calls.
   Single-pass parsers that don't recurse miss this variant. */
function evaluateClamp(expr, viewportWidth) {
  // If expr contains nested clamp(), evaluate innermost first
  const nested = /clamp\(([^()]+)\)/.exec(expr);
  if (nested) {
    const inner = evaluateClamp(nested[0], viewportWidth);
    expr = expr.replace(nested[0], String(inner));
    return evaluateClamp(expr, viewportWidth);
  }
  const [min, pref, max] = parseClampArgs(expr).map(a => evalCalc(a, viewportWidth));
  return Math.max(min, Math.min(pref, max));
}

Summary table

Attack Mechanism Scanner detection gap Severity
Zero-minimum clamp at mobile viewports min=0px + preferred expression crossing zero at 430–500px viewport — collapses to 0px on all phones Static analysis sees valid fluid scale; does not simulate mobile viewport widths HIGH
Wide-collapse via small vw coefficient 1vw coefficient + large negative offset — collapse extends to 800px covering all mobile + tablet Small coefficient looks like subtle scaling; large offset appears as design adjustment HIGH
Custom property indirection var(--x) as clamp minimum where --x resolves to 0px via MCP-injected :root Scanners defer on var() arguments without resolving custom property chain MEDIUM
Nested clamp() as preferred expression Inner clamp evaluates to negative at mobile; outer clamp returns its 0px minimum Single-pass parsers do not recurse into nested clamp() calls MEDIUM

SkillAudit findings for CSS font-size fluid clamp()

HIGH font-size: clamp(0px, A*vw - B, max) where B/A ≥ 320 (i.e., the preferred expression crosses zero within mobile viewport range). The computed font-size evaluates to 0px on viewports below the crossover width. Consent disclosure text rendered with this rule is completely invisible on mobile devices. The clamp() formula appears as standard fluid typography in static analysis. Detection requires evaluating the preferred expression at representative mobile viewport widths (320px, 375px, 390px, 428px) and checking whether the computed result is 0px or negative.
HIGH Custom property indirection in clamp() minimum: font-size: clamp(var(--x), preferred, max) where an MCP-injected stylesheet sets --x: 0px. Scanners that do not resolve custom properties across all injected stylesheets cannot determine the effective minimum and will miss the zero-minimum condition. SkillAudit resolves all custom property chains before evaluating clamp() expressions.
MEDIUM Nested clamp() as the preferred argument: font-size: clamp(0px, clamp(-1px, A*vw - B, C), max). The inner clamp evaluates to a negative value at mobile viewports, causing the outer clamp to return its 0px minimum. Single-pass CSS parsers that do not recurse into nested function calls miss this variant. SkillAudit's clamp evaluator recursively resolves nested function arguments before computing the outer clamp result.
LOW Ambiguous fluid scale: font-size: clamp(0px, ... where the preferred expression never evaluates below zero at any standard viewport width but the minimum is still 0px instead of a positive value. While not an active attack, a zero minimum means any future MCP stylesheet modification that decreases the vw coefficient or increases the negative offset will silently collapse font-size to 0px. A non-zero minimum (e.g., clamp(11px, ...)) is the safe pattern.

Defences

Mobile viewport simulation in font-size evaluation: SkillAudit evaluates clamp() expressions for all font-size declarations at a set of representative viewport widths (320px, 375px, 390px, 428px, 768px, 1024px) and flags any formula that produces a computed font-size of 0px or below at any of these widths for consent-critical elements.

Custom property chain resolution: Before evaluating clamp() expressions, SkillAudit resolves all var() references in the arguments, including MCP-injected custom properties in :root or element-level style attributes. A variable that resolves to 0px as the clamp minimum triggers the same checks as a literal 0px.

Nested clamp() recursive evaluation: SkillAudit's CSS expression evaluator recursively resolves nested function calls — including clamp() inside clamp(), min() and max() as arguments — before evaluating the outermost function. This ensures that structural obfuscation using nested math functions does not evade detection.

Related: CSS font-size security overview · CSS min/max/clamp security · CSS custom properties security · CSS viewport units security