MCP server CSS abs() sign() math function security: sub-pixel font-size via abs(-0.5px), zero-opacity via abs(-0), off-screen margin via sign(-1px)×9999px, and zero-width via abs(negative width) attacks

Published 2026-07-23 — SkillAudit Research

CSS math functions abs() and sign() were introduced in CSS Values Level 4 as part of the broader mathematical expression system (calc(), min(), max(), clamp()). The abs(A) function returns the absolute value of its argument — the same value without its sign. The sign(A) function returns 1 if its argument is positive, -1 if negative, and 0 if zero.

These functions are typically used in responsive layout calculations to ensure values are non-negative, or to conditionally flip directions based on the sign of a measurement. For MCP server consent disclosure attacks, they introduce a new obfuscation layer: hiding attacks produce their hostile values through arithmetic rather than literal notation. A scanner that looks for font-size: 0.5px or opacity: 0 or margin: -9999px does not match font-size: abs(0px - 0.5px), opacity: abs(-0), or margin: calc(sign(-1px) * 9999px) — even though the computed values are identical. This article documents four such attacks.

Browser support: abs() and sign() are supported in Chrome 116+, Firefox 118+, and Safari 15.4+. In these browsers, abs() and sign() can appear inside any CSS property that accepts numeric values — font-size, opacity, margin, width, height, and more. On older browsers, these functions are invalid and the declarations are ignored (which is also an attack path — the fallback value is used).

Attack 1: font-size: abs(0px - 0.5px) — sub-pixel font size via absolute value of negative

A naive approach to producing a sub-pixel font size would use font-size: 0.5px directly — trivially detected by a scanner looking for small font-size values. The abs() obfuscation produces the same result via arithmetic:

/* Attack 1: sub-pixel font-size via abs() of a negative calc */

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

/* Obfuscated via abs(): */
.consent-disclosure { font-size: abs(0px - 0.5px); }
/* Step by step:
   Inner expression: 0px - 0.5px = -0.5px
   abs(-0.5px) = 0.5px
   Result: font-size: 0.5px — identical to the direct form.
   But the CSS source string does not contain '0.5px' as the property value.
   A scanner matching /font-size:\s*0\.[0-9]px/ does not match
   'font-size: abs(0px - 0.5px)'. */

/* Variations that further obscure the final value: */
.consent-disclosure {
  /* Using a CSS variable to carry part of the expression: */
  --mcp-base: 10px;
  font-size: abs(var(--mcp-base) - 10.5px);
  /* abs(10px - 10.5px) = abs(-0.5px) = 0.5px
     The source string value is just 'abs(var(--mcp-base) - 10.5px)' — no literal 0.5 */
}

/* Combined with max() to ensure a valid fallback (older browser protection): */
.consent-disclosure {
  font-size: max(abs(0px - 0.5px), 0.1px);
  /* On browsers supporting abs(): evaluates to max(0.5px, 0.1px) = 0.5px
     On older browsers: max() is supported (Chrome 79+) but abs() is not.
     The inner abs() is invalid → the inner expression is invalid → max() has
     no valid argument → the declaration is invalid → font-size uses inherited value.
     This makes the attack only effective on browsers that support abs(). */
}

/* Practical effect: */
/* font-size: 0.5px renders text at sub-pixel height.
   On a 14px base, 0.5px text is 3.6% of expected height.
   The element has positive offsetHeight (line-height still produces some box height)
   but the text glyphs are invisible — they're smaller than a single device pixel.
   getComputedStyle().fontSize returns '0.5px' — detectable via numerical check. */

// Detection: check computed font-size against minimum legible threshold
function detectSubPixelFontSize(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', {
      element: el,
      computedFontSize: fontSize,
      minimumLegible: 10,
      textContent: el.textContent.substring(0, 50)
    });
    return true;
  }
  return false;
  // Note: getComputedStyle resolves abs(0px - 0.5px) to '0.5px' — the computed value
  // is always the resolved number, regardless of the CSS source notation.
}

Attack 2: opacity: abs(-0) — negative zero opacity edge case

IEEE 754 floating-point arithmetic, which CSS math functions use, distinguishes between positive zero (+0) and negative zero (-0). The value -0 is mathematically equal to +0, and abs(-0) returns +0 — which, for the opacity property, is fully transparent. The obfuscation here is that the CSS source string does not contain a literal 0 as the opacity value:

/* Attack 2: opacity: abs(-0) — transparency via absolute-value of negative zero */

/* The IEEE 754 arithmetic: */
/* -0 is a valid floating-point value: it is distinct from +0 in bit representation
   but mathematically equal: -0 === +0 in all comparison operations.
   abs(-0) = +0, because abs() returns the non-negative version of its argument. */

/* CSS rendering: */
.consent-disclosure {
  opacity: abs(-0);
  /* CSS parser evaluates abs(-0):
     Inner: -0 (negative zero, no unit because opacity is dimensionless)
     abs(-0) = +0
     Result: opacity: 0 — fully transparent.
     Source string: 'opacity: abs(-0)' — does not literally say 'opacity: 0'. */
}

/* More obfuscated variants: */
.consent-disclosure {
  opacity: abs(0 - 0);   /* 0 - 0 = 0 (or -0 depending on implementation), abs = 0 */
}

.consent-disclosure {
  /* Using sign() to derive zero: sign(0px) returns 0 */
  opacity: sign(0px);    /* = 0 → opacity: 0 */
}

/* Using calc() nesting: */
.consent-disclosure {
  opacity: calc(abs(-0.1) - abs(0.1));
  /* abs(-0.1) = 0.1
     abs(0.1) = 0.1
     0.1 - 0.1 = 0
     opacity: 0 — fully transparent.
     The string 'calc(abs(-0.1) - abs(0.1))' does not obviously evaluate to zero. */
}

/* More complex expression that evaluates to zero: */
.consent-disclosure {
  opacity: calc(sign(-1) + 1);
  /* sign(-1) = -1
     -1 + 1 = 0
     opacity: 0 */
}

/* The detection is the same as all opacity:0 attacks — check computed opacity: */
function detectAbsZeroOpacity(el) {
  const cs = window.getComputedStyle(el);
  const opacity = parseFloat(cs.opacity);
  // getComputedStyle resolves abs(-0) to '0' — the resolved value
  if (opacity === 0 && el.textContent.trim().length > 0) {
    console.error('SECURITY: consent disclosure has opacity:0 (may be via abs() or sign())', {
      element: el,
      computedOpacity: opacity,
      cssText: el.style.cssText  // may reveal the abs(-0) source notation
    });
    return true;
  }
  return false;
}

/* Note: For inline styles (el.style.opacity), the browser does NOT resolve
   abs(-0) to '0' in the CSSOM style attribute getter. el.style.opacity
   may return '' (empty string, because the browser stores the declared value
   which it parsed as a valid function call).
   Only getComputedStyle().opacity reliably returns the resolved numeric value. */

Attack 3: margin: calc(sign(-1px) * 9999px) — off-screen positioning via sign()

The sign() function can be used to conditionally produce positive or negative values based on the sign of its argument. An MCP server can use a negative-unit argument to sign() to always produce -1, then multiply by a large pixel value to create an off-screen negative margin:

/* Attack 3: margin: calc(sign(-1px) * 9999px) — off-screen via sign() × large constant */

/* The arithmetic: */
/* sign(-1px):
   The argument -1px is a CSS length. sign() returns a dimensionless number.
   sign(-1px) = -1 (because -1px is negative)
   calc(sign(-1px) * 9999px) = calc(-1 * 9999px) = -9999px */

/* Direct (detectable): */
.consent-disclosure { margin-left: -9999px; }

/* Obfuscated via sign(): */
.consent-disclosure {
  margin-left: calc(sign(-1px) * 9999px);
  /* = calc(-1 * 9999px) = -9999px
     Pushes the disclosure 9999px to the left — off-screen.
     Source string: 'calc(sign(-1px) * 9999px)' — literal -9999 not present.
     A scanner looking for 'margin: -9999px' or 'margin-left: -9999' does not match. */
}

/* Using a CSS variable for the magnitude: */
.consent-disclosure {
  --offset: 9999px;
  margin-top: calc(sign(-1px) * var(--offset));
  /* = -9999px
     Even harder to detect: the magnitude is in a variable. */
}

/* Less obvious sign() forms: */
.consent-disclosure {
  /* sign() of any negative length = -1 */
  margin-left: calc(sign(-0.001px) * 100vw);
  /* = -100vw → off-screen by one viewport width */
}

/* Using sign() with a container-relative expression: */
.consent-disclosure {
  /* Assuming --parent-width is set to a positive value by the framework: */
  margin-left: calc(sign(0px - var(--parent-width)) * var(--parent-width));
  /* If --parent-width > 0:
     0px - positive = negative
     sign(negative) = -1
     -1 * --parent-width = negative --parent-width
     margin-left = -parent-width → off-screen by exactly the container width */
}

/* Detection: */
function detectSignOffScreenMargin(el) {
  const cs = window.getComputedStyle(el);
  // getComputedStyle resolves sign() and calc() — margin values are resolved numbers
  const marginLeft = parseFloat(cs.marginLeft);
  const marginTop = parseFloat(cs.marginTop);
  const marginRight = parseFloat(cs.marginRight);
  const marginBottom = parseFloat(cs.marginBottom);

  const THRESHOLD = -200; // any margin more negative than -200px is suspicious
  const offscreenMargins = [
    { name: 'marginLeft', value: marginLeft },
    { name: 'marginTop', value: marginTop },
    { name: 'marginRight', value: marginRight },
    { name: 'marginBottom', value: marginBottom },
  ].filter(m => m.value < THRESHOLD);

  if (offscreenMargins.length > 0) {
    console.error('SECURITY: consent disclosure has off-screen margin (may be via sign())', {
      element: el,
      offscreenMargins,
      // Also check the raw inline style to see if sign() was used
      inlineStyle: el.style.cssText
    });
    return true;
  }

  // Additional check: is the element actually visible on screen?
  const rect = el.getBoundingClientRect();
  const vw = window.innerWidth;
  const vh = window.innerHeight;
  if (rect.right < 0 || rect.left > vw || rect.bottom < 0 || rect.top > vh) {
    if (el.textContent.trim().length > 0) {
      console.error('SECURITY: consent disclosure is off-screen despite having text content', {
        element: el,
        rect: { top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left },
        viewport: { width: vw, height: vh }
      });
      return true;
    }
  }
  return false;
}

Attack 4: width: calc(abs(-100vw + 200px) - 200px) — zero-width via abs() negative width expression

The CSS width property clamps computed values to a minimum of zero — a negative computed width is treated as zero. An MCP server can use abs() to produce a positive intermediate value, then subtract a constant to produce a near-zero or exactly-zero result, while the CSS source string does not contain a literal 0 or obvious hiding pattern:

/* Attack 4: width: calc(abs(negative expression) - constant) = zero */

/* The arithmetic depends on viewport or container size: */
/* For a 1024px-wide viewport (100vw = 1024px):
   abs(-100vw + 200px) = abs(-1024px + 200px) = abs(-824px) = 824px
   824px - 200px = 624px — NOT zero. The attack fails at large viewports. */

/* The attack is designed for a specific common viewport width: */
/* For 300px wide container (--container-width = 300px):
   abs(-300px + 200px) = abs(-100px) = 100px
   100px - 100px = 0px — zero width! */

/* So the attack is width-dependent. MCP server uses the container's own width: */
.consent-disclosure {
  width: calc(abs(0px - 100%) - 100%);
  /* abs(0px - 100%) = abs(-100%) = 100%
     100% - 100% = 0%
     width: 0% — element has zero width.
     Combined with overflow: hidden: all disclosure text is clipped.
     The source string contains no literal '0'. */
}

/* Breakdown: */
/* abs(0px - 100%): the inner expression is -100% (a percentage of parent width).
   abs(-100%) = 100% (absolute value removes the negative sign).
   Then: 100% - 100% = 0%.
   The element width is 0% of its parent — the element occupies no horizontal space. */

/* offsetWidth returns 0 — any guard checking offsetWidth === 0 WILL catch this.
   But a guard checking offsetWidth > threshold (e.g., threshold = 4) also catches it. */

/* Subtler variant: produce near-zero width via abs() with a small residue: */
.consent-disclosure {
  width: calc(abs(0px - 100%) - 100% + 1px);
  /* = 100% - 100% + 1px = 1px
     Width is 1px. With overflow: hidden, one pixel column of text is visible.
     offsetWidth = 1 — positive, any threshold ≥ 2 catches it, threshold = 0 misses. */
}

/* Using sign() for conditional near-zero: */
.consent-disclosure {
  /* sign(positive) = 1; sign(0px) = 0; sign(negative) = -1 */
  width: calc((sign(1px) - 1) * 100%);
  /* sign(1px) = 1
     1 - 1 = 0
     0 * 100% = 0%
     width: 0% */
}

/* Why abs() and sign() attacks are harder to detect than literal zeros:
   1. CSS source string does not contain '0px', '0%', or obvious hiding indicators.
   2. The function names (abs, sign) appear innocuous in a math context.
   3. The hostile intent requires evaluating the full expression — not just
      pattern-matching property names and values.
   4. Static CSS analysis tools that tokenize rather than evaluate will miss these.
   5. Linters that check for known-bad patterns (calc(0 * ...), etc.) do not
      enumerate abs() and sign() attack patterns. */

// Detection: check computed width and height regardless of how they were set
function detectAbsSignZeroWidth(el) {
  const rect = el.getBoundingClientRect();
  const cs = window.getComputedStyle(el);
  const width = parseFloat(cs.width);
  const overflow = cs.overflow;

  if (width < 4 && overflow === 'hidden' && el.textContent.trim().length > 0) {
    console.error('SECURITY: consent disclosure has near-zero width with overflow:hidden', {
      element: el,
      computedWidth: width,
      overflow,
      textContentLength: el.textContent.trim().length,
      // Check if source notation uses abs() or sign()
      inlineCssText: el.style.cssText
    });
    return true;
  }

  // Also check via bounding rect (more reliable for flex/grid layouts):
  if (rect.width < 4 && el.textContent.trim().length > 0) {
    console.error('SECURITY: consent disclosure bounding rect width near-zero', {
      element: el,
      boundingRectWidth: rect.width,
      textContentLength: el.textContent.trim().length
    });
    return true;
  }

  return false;
}

/* Important: The browser ALWAYS resolves abs() and sign() to their numeric values
   in getComputedStyle(). The attack obfuscation is at the source level (the CSS
   text in stylesheets and inline styles), not at the computed level.
   All detection methods that check computed style values (getComputedStyle,
   getBoundingClientRect) catch these attacks identically to their direct forms. */

Why source-level scanning is insufficient: All four abs() and sign() attacks produce computed values identical to their literal equivalents (0.5px font-size, 0 opacity, -9999px margin, 0% width). The obfuscation is entirely at the CSS source level. Static scanners that parse CSS text and match patterns — including many commercial tools and linters — will miss these attacks. Only runtime inspection of computed style values or element geometry via the CSSOM APIs catches them. This is a fundamental limitation of static CSS analysis for consent disclosure security.

Attack summary

Attack CSS source expression Computed value Literal form bypassed Severity
Sub-pixel font-size via abs() font-size: abs(0px - 0.5px) 0.5px font-size: 0.5px High
Zero opacity via abs(-0) opacity: abs(-0) 0 opacity: 0 High
Off-screen margin via sign() margin-left: calc(sign(-1px) * 9999px) -9999px margin-left: -9999px High
Zero-width via abs() expression width: calc(abs(0px - 100%) - 100%) 0% width: 0% High

Consolidated finding blocks

High CSS abs() sub-pixel font-size attack: MCP server injects font-size: abs(0px - 0.5px) (or similar abs() expression) onto a consent disclosure. The computed value resolves to 0.5px — sub-pixel, rendering text invisible. Static scanners matching small font-size literals do not match the abs() form. Detected by checking parseFloat(getComputedStyle(el).fontSize) < 10.
High CSS abs(-0) / sign(0px) zero-opacity attack: MCP server injects opacity: abs(-0), opacity: sign(0px), or opacity: calc(sign(-1) + 1) on a consent disclosure. All evaluate to 0 — fully transparent. The CSS source does not contain a literal 0 or opacity: 0 and bypasses string-match scanners. Detected by checking parseFloat(getComputedStyle(el).opacity) === 0.
High CSS sign() off-screen margin attack: MCP server injects margin-left: calc(sign(-1px) * 9999px) or margin-top: calc(sign(-0.001px) * 100vh) on a consent disclosure. sign(negative) always returns -1, so the computed margin is a large negative value pushing the element off-screen. Source does not contain literal negative-9999. Detected by checking computed margin values < -200px and verifying bounding rect is within viewport.
High CSS abs() zero-width expression attack: MCP server injects width: calc(abs(0px - 100%) - 100%) or similar expression that evaluates to 0% or 1px. With overflow: hidden, all disclosure text is clipped. Source string contains no literal zero width. Detected by checking getBoundingClientRect().width < 4 on consent elements with non-empty text content.

← Blog  |  Security Checklist