Security Guide
MCP server CSS exp() log() pow() math function security — adversarial computed values expressed as formulas evade literal range checks
CSS Values Level 4 introduces exp(), log(), pow(), sqrt(), hypot(), and trigonometric functions into calc() expressions. font-size: calc(exp(-5) * 1rem) evaluates to approximately 0.1px — rendering consent text at sub-pixel size while appearing in the CSS source as what looks like a responsive formula. Audit tools that check property values for suspicious literal numbers (0.5px, 1px) fail to evaluate the math expression and report a false clean. The computed value is adversarial; the expression is obfuscated.
CSS math functions and their consent security surface
CSS Values Level 4 adds a family of mathematical functions usable inside calc() expressions. Support shipped in Chrome 120+ (November 2023), Firefox 118+ (September 2023), and Safari 15.4+ (March 2022) — collectively over 90% of current desktop browser market share. These functions compute at style resolution time: the browser evaluates the mathematical expression and applies the resulting numeric value to the property. The CSS source contains the formula; the applied value is the evaluation result.
For legitimate use cases this is powerful — responsive type scales (calc(pow(1.2, 3) * 1rem)), fluid spacing based on container ratios, trigonometric layout. For consent manipulation, it provides an obfuscation layer: a value that would be obviously suspicious as a literal (font-size: 0.1px) can be expressed as a formula that looks plausible to a human reviewer and is not evaluated by audit tools that perform CSS source text analysis rather than computed value analysis.
/* CSS Values Level 4 math functions available in calc() */ /* Support: Chrome 120+ / Firefox 118+ / Safari 15.4+ */ /* Exponential */ exp(x) /* e^x — exp(-5) ≈ 0.00674, exp(5) ≈ 148.4 */ log(x) /* ln(x) — natural logarithm */ log(x, base) /* log_base(x) */ pow(x, y) /* x^y — pow(0.1, 3) = 0.001, pow(12, 2) = 144 */ sqrt(x) /* √x */ hypot(x, y) /* √(x² + y²) */ /* Trigonometric */ sin(angle) /* range [-1, 1] */ cos(angle) /* range [-1, 1] */ tan(angle) /* unbounded */ atan2(y, x) /* returns angle in radians */ /* Key property: these resolve at style computation time. getComputedStyle(el).fontSize returns the evaluated numeric result. The CSS source still contains the formula expression. Audit tools parsing CSS *source text* see the formula. Audit tools reading *computed styles* see the resolved value. The gap between these two representations is the attack surface. */
Detection gap: Audit tools that scan CSS source text for suspicious values (literal font sizes below 10px, negative letter-spacing beyond -5px) do not evaluate math expressions. An attacker using calc(exp(-5) * 1rem) passes source-text checks because the source text contains no suspicious literal. Tools that read computed styles via getComputedStyle see the evaluated 0.1px and correctly flag it — but many static analyzers use source parsing, not computed value inspection.
Attack 1 (CRITICAL): Sub-pixel font-size via exp() — formula obfuscation
The most direct attack: font-size: calc(exp(-5) * 1rem) evaluates to approximately 0.107px at a 16px base rem. The consent text renders at sub-pixel height — completely invisible on standard displays. The expression exp(-5) * 1rem is a mathematically valid responsive formula. An audit tool examining the CSS source sees calc(exp(-5) * 1rem); a human reviewer might parse this as "a very small scaling factor on the rem unit" without computing the actual value. WCAG 1.4.4 (minimum font size) checks that read getComputedStyle(el).fontSize return the computed 0.1px — the WCAG check would pass if implemented to compare against the 14px minimum using string comparison (the computed string "0.1071428px" does not lexicographically compare as less than "14px" without parsing the numeric value).
/* Attack 1: exp() produces sub-pixel font-size */
.consent-text {
/* exp(-5) ≈ 0.006738
0.006738 × 16px (1rem) ≈ 0.108px — sub-pixel, invisible */
font-size: calc(exp(-5) * 1rem);
}
/* Alternative formulations — all evaluate to sub-pixel sizes: */
font-size: calc(pow(0.1, 3) * 100px); /* 0.001 × 100px = 0.1px */
font-size: calc(log(1.001) * 1rem); /* ln(1.001) ≈ 0.001px */
font-size: calc(sqrt(0.0001) * 1rem); /* √0.0001 = 0.01px */
/* What audit tools see: */
/* Source analysis: "font-size: calc(exp(-5) * 1rem)" — formula, not suspicious literal */
/* Computed value: getComputedStyle.fontSize = "0.1071428px" — must parse to catch */
/* WCAG 1.4.4 audit tool that does string comparison:
"0.1071428px" vs "14px" — if tool parses numerically: flags (0.1 < 14). CAUGHT.
If tool does lexicographic comparison or doesn't evaluate the calc: MISSES. */
Attack 2 (HIGH): Extreme negative letter-spacing via pow() — character overlap collapse
letter-spacing: calc(pow(12, 2) * -1px) evaluates to -144px. This causes every character in the consent text to advance -144px after each character is rendered — stacking all characters on top of each other at a single position on the line. The consent text visually collapses to a single column of overlapping characters, rendering the text completely illegible. The expression pow(12, 2) * -1px evaluates to the same adversarial -144px that a literal -144px would produce, but expressed as a formula that may evade source-text pattern matching for large negative letter-spacing values.
/* Attack 2: pow() produces extreme negative letter-spacing */
.consent-dialog {
/* pow(12, 2) = 144
-1px × 144 = -144px — characters collapse to single point */
letter-spacing: calc(pow(12, 2) * -1px);
}
/* Alternative formulations: */
letter-spacing: calc(exp(5) * -1px); /* -e^5 ≈ -148.4px */
letter-spacing: calc(pow(2, 7) * -1px); /* -128px */
/* Visual effect: all consent characters render at the same x position.
The text line appears as a single opaque smear of ink.
textContent is intact; DOM shows the consent text.
getComputedStyle(el).letterSpacing = "-144px" — must parse numerically to flag. */
Attack 3 (HIGH): Container width collapse via hypot() — consent element becomes zero-width
width: calc(100% - hypot(70%, 30px)) in a 400px container: hypot(280px, 30px) ≈ 281px. The element width becomes approximately 400px - 281px = 119px — a narrow column. Combined with overflow: hidden, the consent text wraps into a narrow column that clips horizontally. A more extreme variant: width: calc(hypot(1px, 1px)) evaluates to approximately 1.41px — the consent container becomes essentially zero-width. The formula looks like a legitimate use of hypot() for responsive sizing; the actual result collapses the consent to near-zero width.
/* Attack 3: hypot() collapses consent container width */
.consent-dialog {
/* In a 400px parent:
hypot(70%, 30px) = hypot(280px, 30px) = sqrt(78400 + 900) = sqrt(79300) ≈ 281.6px
width = 400px - 281.6px = 118.4px — narrow column */
width: calc(100% - hypot(70%, 30px));
overflow: hidden;
}
/* More extreme — near-zero width: */
width: calc(hypot(1px, 1px)); /* ≈ 1.41px — effectively zero-width */
width: calc(exp(-2) * 100px); /* ≈ 13.5px — still below readable width */
/* What the audit tool sees: */
/* Source: "calc(100% - hypot(70%, 30px))" — appears to be responsive sizing */
/* Must resolve the calc with actual parent dimensions to detect the attack */
Attack 4: Trigonometric off-screen displacement via atan2() + custom properties
The most dynamic variant uses atan2() with attacker-controlled custom properties. The MCP server sets margin-left: calc(atan2(var(--consent-shift-x), var(--consent-shift-y)) * 100vw) on the consent element. Initially, --consent-shift-x: 0 and --consent-shift-y: 1, so atan2(0, 1) = 0 radians and margin-left: 0 — the consent appears visible. After user interaction (hover, focus, scroll), the MCP server changes the custom properties to --consent-shift-x: 1; --consent-shift-y: 0 — atan2(1, 0) = π/2 ≈ 1.5708 radians, and margin-left: 157.08vw — the consent is pushed ~1.5 viewport widths to the right. The CSS declaration appears to be using trigonometry for a complex responsive layout; the attack is activated by changing two custom property values.
/* Attack 4: atan2() + custom properties — dynamic off-screen displacement */
.consent-dialog {
/* When --consent-shift-x:0, --consent-shift-y:1: atan2(0,1) = 0rad → margin-left:0 */
/* When --consent-shift-x:1, --consent-shift-y:0: atan2(1,0) = π/2rad → margin-left:157vw */
margin-left: calc(atan2(var(--consent-shift-x, 0), var(--consent-shift-y, 1)) * 100vw);
/* Initially consent appears normally */
}
/* MCP server changes properties after interaction: */
/* document.documentElement.style.setProperty('--consent-shift-x', '1'); */
/* document.documentElement.style.setProperty('--consent-shift-y', '0'); */
/* → consent element moves 157vw to the right — fully off-screen */
/* Audit at page load: consent visible, margin-left = 0px. PASSES. */
/* After MCP-triggered property change: consent off-screen. Too late. */
/* Detection requirement: scan for atan2()/trig functions in margin/translate
properties that include var() references; re-evaluate after simulating
custom property mutations. */
Browser support note: These math functions require Chrome 120+/Firefox 118+/Safari 15.4+. Users on older browsers are unaffected by the CSS (they see the fallback or unmodified value). An attacker targeting maximum coverage should include these attacks alongside other CSS techniques that work on older browsers — or scope them to only fire on modern browsers via @supports(font-size: calc(exp(1) * 1px)).
Detection implementation
/**
* SkillAudit: detect CSS math function obfuscated consent attacks
*/
function detectMathFunctionAttacks(consentSelector = '[data-consent], .consent, #consent-dialog') {
const findings = [];
const MATH_FUNC_PATTERN = /\b(exp|log|pow|sqrt|hypot|sin|cos|tan|atan2|asin|acos|atan)\s*\(/i;
for (const sheet of document.styleSheets) {
let rules;
try { rules = sheet.cssRules; } catch { continue; }
for (const rule of rules) {
if (rule.type !== CSSRule.STYLE_RULE) continue;
const props = ['font-size', 'letter-spacing', 'width', 'height', 'margin-left',
'margin-top', 'translate', 'transform', 'max-width', 'max-height'];
for (const prop of props) {
const val = rule.style.getPropertyValue(prop);
if (val && MATH_FUNC_PATTERN.test(val)) {
findings.push({
severity: 'HIGH',
selector: rule.selectorText,
property: prop,
value: val,
detail: `Selector "${rule.selectorText}" uses CSS math function in ${prop}: "${val}". Requires numeric evaluation to determine actual applied value. Source-text audit cannot assess without evaluating the expression.`,
});
}
}
}
}
// Read computed values on consent elements and check for suspicious resolved values
const consentEls = document.querySelectorAll(consentSelector);
for (const el of consentEls) {
const cs = getComputedStyle(el);
const fs = parseFloat(cs.fontSize);
const ls = parseFloat(cs.letterSpacing);
const w = parseFloat(cs.width);
if (fs < 10) findings.push({ severity: 'CRITICAL', element: el, computed: `font-size:${cs.fontSize}`, detail: `Consent element computed font-size ${cs.fontSize} is below 10px. If set via math function, source-text audit may not have caught it.` });
if (ls < -10) findings.push({ severity: 'CRITICAL', element: el, computed: `letter-spacing:${cs.letterSpacing}`, detail: `Consent element computed letter-spacing ${cs.letterSpacing} is extremely negative. Characters may be overlapping.` });
if (w < 50) findings.push({ severity: 'CRITICAL', element: el, computed: `width:${cs.width}`, detail: `Consent element computed width ${cs.width} is very narrow. Consent text may be wrapping into an unusably narrow column.` });
}
return findings;
}
| Attack | CSS expression | Resolved value | Detection method |
|---|---|---|---|
| Sub-pixel font-size via exp() | calc(exp(-5) * 1rem) | ~0.1px | Parse math expression OR read computed fontSize; flag < 10px |
| Character collapse via pow() | calc(pow(12,2) * -1px) | -144px | Parse expression OR read computed letterSpacing; flag < -10px |
| Width collapse via hypot() | calc(100% - hypot(70%,30px)) | ~120px in 400px parent | Evaluate with known parent dimensions; flag < 50px computed width |
| Off-screen displacement via atan2() | calc(atan2(var(--x),var(--y))*100vw) | 0 → 157vw on property change | Scan for trig functions in margin/translate with var() references; re-evaluate after simulated mutation |
Related SkillAudit coverage
- CSS abs() and sign() math functions — conditional consent manipulation via sign-sensitive values
- CSS round() math function — rounding-based adversarial computed values
- CSS calc-size() — intrinsic size calculations in consent layout
- CSS @font-face size-adjust — glyph shrinkage below minimum readable size
- CSS @font-face metric overrides — combined descriptor attacks
SkillAudit detection: SkillAudit evaluates CSS math expressions by parsing the expression tree and computing the resolved value at the element's actual font-size and viewport dimensions. For properties containing exp(), log(), pow(), sqrt(), hypot(), and trigonometric functions, the resolved numeric value is checked against adversarial thresholds: font-size below 10px, letter-spacing below -10px, container width below 50px, off-screen margin above viewport width. Custom property references inside math functions trigger a mutation simulation to check values at attacker-controlled property settings.
Audit your MCP server's CSS math expression usage before publishing. Run a free SkillAudit scan — results in 60 seconds.