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
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.
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.
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.
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.