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):
- When
preferred > 14px: result is14px(clamped to max) - When
0px ≤ preferred ≤ 14px: result ispreferred(within range) - When
preferred < 0px: result is0px(clamped to min) — text becomes invisible
/* 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()
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.
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.
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.
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