MCP server CSS round() mod() rem() math function security: zero font-size via round(0.4px,1px), opacity:0 via mod(1,1), zero-width via rem(-100%,100%), zero margin via round(down,0.9px,1px) attacks

Published 2026-07-23 — SkillAudit Research

CSS Values Level 4 introduced a family of rounding and modular arithmetic math functions: round(strategy, A, B), mod(A, B), and rem(A, B). These functions operate on CSS numeric values and produce results based on modular division — similar to the % operator in JavaScript but with CSS-specific dimension handling. Browser support shipped in Chrome 116, Firefox 118, and Safari 15.4.

For MCP server consent disclosure attacks, these functions introduce a new obfuscation layer. The computed value is always a plain number (zero, for the attacks below), but the CSS source expression does not contain a literal zero. Static scanners that look for font-size: 0, opacity: 0, width: 0, or margin: 0px do not match expressions like round(0.4px, 1px), mod(1, 1), rem(-100%, 100%), or round(down, 0.9px, 1px).

Browser support and fallback behavior: round(), mod(), and rem() are supported in Chrome 116+, Firefox 118+, and Safari 15.4+. On unsupported browsers, these declarations are invalid and the property uses its inherited or initial value. This creates a dual-target scenario: the attack hides the disclosure on modern browsers that support these functions, while on older browsers the fallback value may expose it — but if the MCP server targets only modern browsers (e.g., @supports (round(1px,1px): 0px)), the attack applies precisely where users are most likely to be running.

Attack 1: font-size: round(0.4px, 1px) — zero font size via rounding to nearest step

The round(A, B) function rounds the value A to the nearest multiple of B. When round(0.4px, 1px) is evaluated, the nearest multiple of 1px to 0.4px is 0px (since 0.4 is closer to 0 than to 1). The result is a font-size of exactly 0px — text is rendered at zero size and is invisible:

/* Attack 1: zero font-size via round() rounding down to nearest 1px step */

/* Direct (trivially detected): */
.consent-disclosure { font-size: 0px; }

/* Obfuscated via round(): */
.consent-disclosure {
  font-size: round(0.4px, 1px);
  /* round(0.4px, 1px):
     Nearest multiple of 1px to 0.4px = 0px (since 0.4 < 0.5, rounds to lower multiple).
     Result: font-size: 0px — text is invisible.
     CSS source string: 'round(0.4px, 1px)' — does not contain '0px' or '0'. */
}

/* Variant: use round(nearest, ...) explicitly (nearest is the default strategy): */
.consent-disclosure {
  font-size: round(nearest, 0.3px, 1px);
  /* round(nearest, 0.3px, 1px):
     0.3px is 0.3 of the way from 0px to 1px — below 0.5, rounds to 0px.
     Result: font-size: 0px. */
}

/* Using CSS variables to obscure the values: */
.consent-disclosure {
  --mcp-size: 0.4px;
  --mcp-step: 1px;
  font-size: round(var(--mcp-size), var(--mcp-step));
  /* Same result: 0px. Source: 'round(var(--mcp-size), var(--mcp-step))' — no literal 0. */
}

// Detection: check computed font-size regardless of expression form
function detectRoundZeroFontSize(el) {
  const cs = window.getComputedStyle(el);
  const fontSize = parseFloat(cs.fontSize);
  if (fontSize < 10 && el.textContent.trim().length > 0) {
    console.error('SECURITY: consent disclosure font-size below legibility threshold (possible round() attack)', {
      element: el,
      computedFontSize: fontSize,
      inlineStyle: el.style.cssText
    });
    return true;
  }
  return false;
}

Attack 2: opacity: mod(1, 1) — zero opacity via modular arithmetic

The mod(A, B) function returns the remainder of A divided by B, with the same sign as B (following the floored division convention). When A is any exact multiple of B, the remainder is 0. mod(1, 1) = 0 because 1 divides exactly into 1 with no remainder. Any integer modulo itself is zero — mod(2, 2), mod(5, 5), mod(100, 100) all evaluate to 0:

/* Attack 2: opacity: mod(1, 1) — zero opacity via any-integer mod itself */

/* Direct: */
.consent-disclosure { opacity: 0; }

/* Obfuscated via mod(): */
.consent-disclosure {
  opacity: mod(1, 1);
  /* mod(1, 1):
     1 ÷ 1 = 1 remainder 0.
     Result: opacity: 0 — fully transparent.
     Source: 'opacity: mod(1, 1)' — no literal '0'. */
}

/* Variants — all evaluate to mod(N, N) = 0: */
.consent-disclosure { opacity: mod(2, 2); }   /* 2 mod 2 = 0 */
.consent-disclosure { opacity: mod(5, 5); }   /* 5 mod 5 = 0 */
.consent-disclosure { opacity: mod(0.5, 0.5); } /* 0.5 mod 0.5 = 0 */

/* Using a CSS variable for the divisor: */
.consent-disclosure {
  --mcp-n: 3;
  opacity: mod(var(--mcp-n), var(--mcp-n));
  /* mod(3, 3) = 0. Source: 'mod(var(--mcp-n), var(--mcp-n))' — no literal opacity value. */
}

/* Difference between mod() and rem(): */
/* mod(A, B): result has same sign as B (floored division) */
/* rem(A, B): result has same sign as A (truncated division) */
/* For positive equal values: both return 0. */

// Detection: check computed opacity
function detectModZeroOpacity(el) {
  const cs = window.getComputedStyle(el);
  const opacity = parseFloat(cs.opacity);
  if (opacity === 0 && el.textContent.trim().length > 0) {
    console.error('SECURITY: consent disclosure opacity:0 (possible mod() attack)', {
      element: el,
      computedOpacity: opacity,
      inlineStyle: el.style.cssText
    });
    return true;
  }
  return false;
}

Attack 3: width: rem(-100%, 100%) — zero width via CSS rem() remainder

The rem(A, B) function returns the remainder of A divided by B using truncated (C-style) division — the result has the same sign as A. For rem(-100%, 100%): -100% ÷ 100% = -1 exactly (no fractional part), so the remainder is 0% with the sign of A = −100% → the result is 0% (or −0% in IEEE 754, which CSS normalizes to 0%). Combined with overflow: hidden, a zero-width disclosure clips all text:

/* Attack 3: width: rem(-100%, 100%) — zero width via CSS rem() */

/* Direct: */
.consent-disclosure { width: 0; overflow: hidden; }

/* Obfuscated via rem(): */
.consent-disclosure {
  width: rem(-100%, 100%);
  overflow: hidden;
  /* rem(-100%, 100%):
     -100% / 100% = -1 exactly (remainder 0%).
     rem() uses truncated division: result sign = sign of A = negative.
     remainder is 0% (zero × sign of A, but 0 has no sign in practice).
     Result: width: 0% — element occupies no horizontal space.
     With overflow:hidden, all text is clipped.
     Source: 'rem(-100%, 100%)' — no literal '0'. */
}

/* Variants: */
.consent-disclosure {
  width: rem(200%, 200%);     /* 200/200 = 1 remainder 0 → 0% */
}
.consent-disclosure {
  width: rem(50px, 50px);     /* 50/50 = 1 remainder 0 → 0px */
}
.consent-disclosure {
  width: rem(var(--parent-w), var(--parent-w)); /* any self-divided value = 0 */
}

// Detection: check computed width
function detectRemZeroWidth(el) {
  const cs = window.getComputedStyle(el);
  const width = parseFloat(cs.width);
  const overflow = cs.overflow;
  const rect = el.getBoundingClientRect();
  if ((width < 4 || rect.width < 4) && overflow === 'hidden' && el.textContent.trim().length > 0) {
    console.error('SECURITY: consent disclosure near-zero width with overflow:hidden (possible rem() attack)', {
      element: el,
      computedWidth: width,
      boundingRectWidth: rect.width,
      overflow,
      inlineStyle: el.style.cssText
    });
    return true;
  }
  return false;
}

Attack 4: margin-top: calc(round(down, 0.9px, 1px)) — zero margin via round(down) strategy

The round(strategy, A, B) form accepts an explicit rounding strategy: nearest (default), up, down, or to-zero. The down strategy rounds toward negative infinity — for positive values below the step size, round(down, 0.9px, 1px) rounds 0.9px down to the nearest 1px multiple below 0.9px, which is 0px:

/* Attack 4: margin-top collapse via round(down, 0.9px, 1px) */

/* Context: consent disclosure relies on margin-top for visual separation from a button.
   The user must see the disclosure as separate from the accept button.
   Collapsing margin-top to 0 makes them appear merged. */

/* Direct: */
.consent-disclosure { margin-top: 0px; }

/* Obfuscated via round(down): */
.consent-disclosure {
  margin-top: calc(round(down, 0.9px, 1px));
  /* round(down, 0.9px, 1px):
     Round 0.9px down to the nearest multiple of 1px toward negative infinity.
     Multiples of 1px: ..., -1px, 0px, 1px, ...
     The largest multiple of 1px that is ≤ 0.9px is 0px.
     Result: 0px.
     The calc() wrapper is technically redundant here but further obscures the expression. */
}

/* More aggressive: round(down) on a sub-pixel combined with negative offset */
.consent-disclosure {
  margin-top: calc(round(down, 0.49px, 1px) - 20px);
  /* round(down, 0.49px, 1px) = 0px
     0px - 20px = -20px
     margin-top: -20px — pulls content 20px up, overlapping the button above.
     Source has no literal '-20px' on the margin-top line. */
}

/* round(to-zero) is equivalent to truncation — also produces 0 for values between 0 and 1: */
.consent-disclosure {
  margin-top: round(to-zero, 0.7px, 1px);
  /* round toward zero: 0.7px is between 0px and 1px, truncate toward zero = 0px. */
}

/* Combined: font-size via round() and margin via round(down) */
.consent-disclosure {
  font-size: round(0.4px, 1px);     /* = 0px */
  margin-top: round(down, 0.9px, 1px); /* = 0px */
  /* Both properties collapse via rounding. Source contains no literal zeros. */
}

// Detection: check computed margins regardless of expression
function detectRoundZeroMargin(el) {
  const cs = window.getComputedStyle(el);
  const marginTop = parseFloat(cs.marginTop);
  if (marginTop < 4 && el.textContent.trim().length > 0) {
    console.error('SECURITY: consent disclosure has near-zero margin-top (possible round(down) attack)', {
      element: el,
      computedMarginTop: marginTop,
      inlineStyle: el.style.cssText
    });
    return true;
  }
  return false;
}

/* Key detection insight: all four attacks are invisible to source-level scanning.
   getComputedStyle() always returns the resolved value:
   - round(0.4px, 1px) → '0px'
   - mod(1, 1) → '0'
   - rem(-100%, 100%) → '0%'
   - round(down, 0.9px, 1px) → '0px'
   Check resolved values, not CSS source expressions. */

Why round()/mod()/rem() bypass most static scanners: These functions are relatively new (Chrome 116+, 2023) and most CSS linters, security scanners, and static analysis tools do not evaluate CSS Level 4 math expressions. A scanner that parses CSS tokens and checks property values against a deny-list will see mod(1, 1) and flag it as a function call — not as zero. Only a scanner that actually evaluates CSS math expressions (treating the result as a numeric value and comparing against thresholds) will catch these. Runtime inspection via getComputedStyle is the reliable detection mechanism.

Attack summary

Attack CSS expression Computed value Property Severity
Zero font-size via round() round(0.4px, 1px) 0px font-size High
Zero opacity via mod() mod(1, 1) 0 opacity High
Zero width via rem() rem(-100%, 100%) 0% width + overflow:hidden High
Zero margin via round(down) round(down, 0.9px, 1px) 0px margin-top Medium

Consolidated finding blocks

High CSS round() zero font-size attack: MCP server injects font-size: round(0.4px, 1px) or round(nearest, 0.3px, 1px) — any value below 0.5px in a 1px step rounds to 0px. Text is invisible. Source string does not contain '0px'. Detection: parseFloat(getComputedStyle(el).fontSize) < 10.
High CSS mod() zero opacity attack: MCP server injects opacity: mod(N, N) for any positive N. Any integer modulo itself is zero. Fully transparent element. Detection: parseFloat(getComputedStyle(el).opacity) === 0 on an element with non-empty text content.
High CSS rem() zero-width attack: MCP server injects width: rem(-100%, 100%) with overflow: hidden. rem(-100%,100%) = 0%. Element clips all text. Detection: getBoundingClientRect().width < 4 on element with non-empty text content.
Medium CSS round(down) zero margin attack: MCP server injects margin-top: round(down, 0.9px, 1px) = 0px, collapsing the visual separator between consent disclosure and accept button. Detection: parseFloat(getComputedStyle(el).marginTop) < 4 on consent elements that should have visual separation.

← Blog  |  Security Checklist