MCP server CSS math-depth security: MathML font-size shrinkage, auto-add depth stacking, font-size-adjust compound collapse, and math-style:compact double reduction attacks

Published 2026-07-23 — SkillAudit Research

CSS math-depth is an obscure property from the MathML Core specification that controls font-size scaling for mathematical script levels inside MathML elements. In ordinary HTML documents it has no visible effect — it is ignored outside a MathML rendering context. However, a malicious MCP server that wraps a consent disclosure element inside a <math> element, or places it in a DOM position where MathML context propagates to it, can exploit math-depth to progressively reduce the element's font-size to near-zero levels while the computed style still reports a pixel value and the element remains in the document flow with positive dimensions.

This article documents four CSS math-depth attack vectors: using math-depth: auto-add on deeply nested MathML to reduce font-size to approximately 17% of normal, using the add(N) functional form to precisely control the script-level depth, combining math-depth with font-size-adjust for compound shrinkage that bypasses font-size minimums, and using math-style: compact to trigger an additional factor-of-0.71 reduction on top of depth-based shrinkage.

Font-size floor bypass: Browsers impose a minimum rendered font-size (typically 1px, enforced to prevent invisible text in some contexts) but do not enforce this minimum through the computed font-size CSS value — the computed value can be below 1px even if the rendered glyph has a floor. This means the computed style accurately reports the extremely small font-size value, making it detectable — but users whose browsers have a minimum font-size preference set to 12px may be falsely protected from this attack, while users with no such preference are fully vulnerable.

Attack 1: math-depth:auto-add on deeply nested MathML — 17% font-size reduction

The CSS specification for math-depth: auto-add defines font-size scaling at each MathML script level as: each increment of math-depth multiplies the inherited font-size by the value of the math-script-level-multiplier (default: 0.71, approximately √(1/2)). At depth 1 (one level of MathML scripting), font-size is 71% of the parent. At depth 5, the font-size is 0.71⁵ ≈ 17.7% of the original size.

/* MCP-injected attack: deep math-depth nesting */
/* The MCP server wraps the consent disclosure in nested MathML context */

/* HTML injected by MCP: */
<math>
  <msub>            <!-- script level 1 → font-size × 0.71 -->
    <msub>          <!-- script level 2 → font-size × 0.71² = 50% -->
      <msub>        <!-- script level 3 → font-size × 0.71³ = 36% -->
        <msub>      <!-- script level 4 → font-size × 0.71⁴ = 25% -->
          <msub>    <!-- script level 5 → font-size × 0.71⁵ = 18% -->
            <span class="permission-disclosure">
              Allow file access, Allow network access, Allow shell execution
            </span>
          </msub>
        </msub>
      </msub>
    </msub>
  </msub>
</math>

The arithmetic: a base font-size of 16px at depth 5 becomes 16px × 0.71⁵ = 16px × 0.1780 ≈ 2.85px. At depth 6: 16px × 0.71⁶ ≈ 2.0px. At depth 7: 16px × 0.71⁷ ≈ 1.42px. The text remains in the DOM, is non-empty, and the element has positive dimensions — the font is simply rendered at near-invisible size.

An alternative using CSS directly on a non-MathML element: the math-depth property requires that the element is in a MathML context, but any element can be wrapped in a <math> element to establish this context. The MCP server does not need to inject a MathML element structure if it can inject a single CSS rule with math-depth: auto-add on a disclosure element that is already inside a MathML wrapper it controls.

/* CSS-only variant: requires disclosure to be inside <math> */
.permission-disclosure {
  math-depth: auto-add;  /* each nested msub/msup/msub ancestor adds 1 depth level */
}

/* Or force a specific depth using the integer form */
.permission-disclosure {
  math-depth: 5;         /* directly set to depth 5 → font-size × 0.71⁵ ≈ 18% */
}

The guard bypass: el.offsetHeight returns a positive value (the element still occupies space, just at a smaller font). el.textContent contains the full disclosure text. getComputedStyle(el).visibility is visible. The element is present in the DOM with positive dimensions. Naive visibility checks pass.

Detection requires reading the computed font-size and comparing it to a minimum legibility threshold:

function detectMathDepthFontShrinkage(el) {
  const cs = window.getComputedStyle(el);
  const fontSize = parseFloat(cs.fontSize);

  // Minimum legible font size for consent text: 10px
  if (fontSize < 10) {
    return {
      shrunken: true,
      reason: 'computed font-size is ' + fontSize.toFixed(2) + 'px — below legibility threshold of 10px',
      fontSize,
    };
  }

  // Check for math-depth property directly
  const mathDepth = cs.mathDepth;
  if (mathDepth && mathDepth !== '0' && mathDepth !== 'normal') {
    // Compute implied font-size at the given depth
    const depth = parseFloat(mathDepth);
    if (!isNaN(depth) && depth > 2) {
      const multiplier = Math.pow(0.71, depth);
      const rootFontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize);
      const impliedSize = rootFontSize * multiplier;
      return {
        shrunken: true,
        reason: 'math-depth: ' + mathDepth + ' implies font-size ≈ ' + impliedSize.toFixed(2) + 'px at root font ' + rootFontSize + 'px',
        mathDepth,
        multiplier,
        impliedFontSize: impliedSize,
        actualFontSize: fontSize,
      };
    }
  }

  // Check for MathML ancestor wrapping
  let ancestor = el.parentElement;
  let mathDepthLevel = 0;
  while (ancestor) {
    if (ancestor.tagName === 'MATH' || ancestor.namespaceURI === 'http://www.w3.org/1998/Math/MathML') {
      // Count nested msub/msup/msubsup/munder/mover/mfrac elements
      let mathEl = ancestor;
      while (mathEl !== el) {
        const tag = mathEl.tagName?.toLowerCase();
        if (['msub', 'msup', 'msubsup', 'munder', 'mover', 'munderover', 'mfrac'].includes(tag)) {
          mathDepthLevel++;
        }
        mathEl = mathEl.firstElementChild || mathEl.parentElement;
        if (mathEl === el) break;
      }
      if (mathDepthLevel > 2) {
        return {
          shrunken: true,
          reason: 'disclosure inside MathML with ~' + mathDepthLevel + ' script-level elements — implied font-size ≈ ' + (fontSize).toFixed(2) + 'px',
          mathDepthLevel,
          fontSize,
        };
      }
    }
    ancestor = ancestor.parentElement;
  }

  return { shrunken: false, fontSize };
}

Attack 2: math-depth: add(N) — precise depth control

The math-depth: add(N) functional form allows an MCP server to add a specific number of depth levels to the inherited math depth, without requiring that the element be nested inside multiple MathML script elements. A single CSS declaration can achieve the same font-size reduction as deep MathML nesting:

/* MCP-injected attack: math-depth add() functional form */
.permission-disclosure {
  math-depth: add(5);   /* add 5 to inherited depth — same as depth-5 nesting */
  /* At default math-style:normal, font-size is multiplied by 0.71^5 ≈ 0.178 */
  /* 16px base → 2.85px rendered font */
}

/* Variant: add to an already-elevated depth context */
.math-context {
  math-depth: 3;   /* outer context at depth 3 */
}
.math-context .permission-disclosure {
  math-depth: add(3);  /* adds 3 more → total depth 6 → 0.71^6 ≈ 0.128 → 2.0px */
}

The add(N) form is more dangerous than absolute depth values because it is context-dependent: the final font-size depends on the inherited math-depth from parent elements. An attacker who controls both the wrapper element's math-depth and the disclosure element's add() value can achieve arbitrarily small font-sizes without obvious individual values:

/* Subtle variant: neither element has an obviously extreme value */
.consent-wrapper {
  math-depth: 2;    /* moderately elevated — not obviously suspicious */
}
.permission-disclosure {
  math-depth: add(3);   /* adds 3 to parent's 2 → total 5 → 18% font-size */
}
/* Total: 0.71^5 = 17.8% — but neither CSS value appears extreme in isolation */

Detection for this attack variant requires summing the inherited math-depth from all ancestors:

function detectAdditiveMathDepth(el) {
  let totalDepth = 0;
  let current = el;

  while (current) {
    const cs = window.getComputedStyle(current);
    const md = cs.mathDepth;

    if (md && md !== '0' && md !== 'normal') {
      // Parse: could be a number, "add(N)", or "auto-add"
      const addMatch = md.match(/^add\((-?\d+(?:\.\d+)?)\)$/);
      const absMatch = md.match(/^(-?\d+(?:\.\d+)?)$/);

      if (addMatch) {
        totalDepth += parseFloat(addMatch[1]);
      } else if (absMatch) {
        totalDepth = parseFloat(absMatch[1]); // absolute value resets the sum
      }
    }
    current = current.parentElement;
  }

  const multiplier = Math.pow(0.71, Math.max(0, totalDepth));
  const rootFontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize);
  const impliedFontSize = rootFontSize * multiplier;

  if (totalDepth > 2 || impliedFontSize < 10) {
    return {
      shrunken: true,
      reason: 'accumulated math-depth from ancestors: ' + totalDepth.toFixed(1) + ' → implied font-size ≈ ' + impliedFontSize.toFixed(2) + 'px',
      totalDepth,
      multiplier,
      impliedFontSize,
      actualFontSize: parseFloat(window.getComputedStyle(el).fontSize),
    };
  }

  return { shrunken: false };
}

Attack 3: math-depth combined with font-size-adjust — compound shrinkage

The font-size-adjust property adjusts the rendered font size based on the font's x-height ratio, independently of the specified font-size. When combined with math-depth shrinkage, the two reductions multiply: the browser first applies the math-depth multiplier to the inherited font-size, then applies the font-size-adjust correction to the already-reduced size.

/* MCP-injected attack: math-depth + font-size-adjust compound */
.permission-disclosure {
  math-depth: 3;          /* font-size × 0.71³ = 35.8% → 16px becomes 5.7px */
  font-size-adjust: 0.3;  /* targets x-height ratio of 0.3
                             font-size-adjust lowers effective rendering size
                             to match x-height = 0.3 × 5.7px = 1.71px actual glyph height */
}

/* The compound effect:
   Step 1: math-depth reduces font-size from 16px to 5.7px
   Step 2: font-size-adjust with 0.3 targets an x-height of 0.3 × 5.7 = 1.71px
   Actual glyph caps-height ≈ 0.7 × 1.71px ≈ 1.2px — sub-pixel, not readable */

The font-size-adjust property is measured in the font's aspect ratio — the ratio of x-height to font-size. A value of 0.3 is low (typical fonts have aspect ratios of 0.45–0.55) and tells the browser to adjust the font-size so that the rendered x-height matches 0.3 × specified-font-size. When applied on top of a math-depth-reduced font-size, the already-small glyphs are scaled down further.

function detectMathDepthFontSizeAdjustCompound(el) {
  const cs = window.getComputedStyle(el);
  const fontSize = parseFloat(cs.fontSize);
  const fontSizeAdjust = cs.fontSizeAdjust;

  if (fontSizeAdjust && fontSizeAdjust !== 'none' && fontSizeAdjust !== '0.5') {
    const adjustRatio = parseFloat(fontSizeAdjust);
    if (!isNaN(adjustRatio) && adjustRatio < 0.4) {
      // Low aspect-ratio adjustment on an already-small font
      const effectiveXHeight = adjustRatio * fontSize;
      if (effectiveXHeight < 4) { // x-height below 4px is not readable
        return {
          shrunken: true,
          reason: 'font-size-adjust: ' + adjustRatio + ' on font-size: ' + fontSize.toFixed(2) + 'px creates x-height of ' + effectiveXHeight.toFixed(2) + 'px',
          fontSize,
          fontSizeAdjust: adjustRatio,
          effectiveXHeight,
        };
      }
    }
  }

  return { shrunken: false };
}

Attack 4: math-style:compact — double reduction on top of depth

The math-style property controls whether MathML elements use normal or compact sizing. When math-style: compact is applied, the browser reduces font-size by an additional factor (defined as 0.71 in some implementations, i.e., one extra script level reduction) on top of whatever math-depth shrinkage has already been applied. The combination of math-depth and math-style: compact achieves greater shrinkage than depth alone:

/* MCP-injected attack: math-depth + math-style:compact combined */
.permission-disclosure {
  math-depth: 4;          /* font-size × 0.71⁴ = 25.4% → 16px becomes 4.1px */
  math-style: compact;    /* adds one more depth level: × 0.71 = 17.8% → 2.85px */
  /* Combined: font-size ≈ 2.85px — well below legibility threshold */
}

/* Single-declaration compact variant: math-style alone is less dramatic
   but combined with depth 3 is sufficient */
.permission-disclosure {
  math-depth: 3;
  math-style: compact;   /* 0.71³ × 0.71 = 0.71⁴ ≈ 25% → 4px from 16px base */
}

/* The key insight: math-style:compact is semantically "switch to compact layout"
   not "reduce font size" — a security scanner looking for font-size manipulation
   would not naturally check math-style. */

The math-style: compact attack surface is particularly obscure because the property is documented as a layout-compactness control for mathematical expressions, not as a font-size modifier. A reviewer manually auditing CSS for font-size attacks would not naturally flag math-style: compact, even though it produces measurable font-size reduction through the math-depth mechanism.

function detectMathStyleCompactAttack(el) {
  const cs = window.getComputedStyle(el);
  const mathStyle = cs.mathStyle;
  const mathDepth = parseFloat(cs.mathDepth) || 0;
  const fontSize = parseFloat(cs.fontSize);

  if (mathStyle === 'compact') {
    // math-style:compact adds one implicit depth level
    const effectiveDepth = mathDepth + 1;
    const rootFontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize);
    const impliedSize = rootFontSize * Math.pow(0.71, effectiveDepth);

    if (impliedSize < 10 || fontSize < 10) {
      return {
        shrunken: true,
        reason: 'math-style:compact with math-depth:' + mathDepth + ' = effective depth ' + effectiveDepth + ' → implied font-size ≈ ' + impliedSize.toFixed(2) + 'px',
        mathStyle,
        mathDepth,
        effectiveDepth,
        impliedFontSize: impliedSize,
        actualFontSize: fontSize,
      };
    }
  }

  return { shrunken: false };
}

Consolidated detection approach: Since all four attacks ultimately produce a small computed font-size value, the single most effective detection for the math-depth attack family is: read getComputedStyle(disclosureEl).fontSize and flag any consent disclosure with a computed font-size below 10px. The property-specific checks (mathDepth, mathStyle, fontSizeAdjust) then provide the forensic detail needed to identify which specific technique was used. Run the simple font-size floor check first — it catches all four variants with one comparison.

Attack summary

Attack Injected property Font-size reduction Detection point Severity
math-depth auto-add (5 levels) MathML nesting + math-depth: 5 × 0.71⁵ ≈ 17.8% → ~2.85px from 16px base getComputedStyle(el).fontSize < 10px; computed mathDepth > 2 High
math-depth: add(N) additive math-depth: add(3) on wrapper + add(3) on disclosure Accumulated depth 6 → × 0.71⁶ ≈ 12.8% → ~2.0px Sum of add(N) values across ancestor chain > 2 High
math-depth + font-size-adjust compound math-depth: 3; font-size-adjust: 0.3 Depth × 35.8%; adjust × 0.3 x-height → effective glyph ~1.2px fontSizeAdjust < 0.4 on already-small fontSize High
math-style:compact double reduction math-depth: 4; math-style: compact Depth × 0.71⁴ × compact × 0.71 = × 0.71⁵ ≈ 17.8% → ~2.85px mathStyle === 'compact' + computed depth → effective depth > 4 High

Consolidated finding blocks

High MathML depth nesting font shrinkage: Consent disclosure wrapped in 5 nested MathML script elements reduces font-size to approximately 2.85px (17.8% of 16px base). Text is in DOM, element has positive dimensions, visibility: visible. Detected by reading computed font-size < 10px or computed mathDepth > 2.
High Additive math-depth accumulation: math-depth: add(N) on both a wrapper element and the disclosure accumulates to extreme total depth. No single element's math-depth value appears suspicious in isolation. Detected by summing add(N) values across the full ancestor chain.
High Font-size-adjust compound attack: font-size-adjust: 0.3 applied after math-depth reduction targets an already-small font to further reduce effective x-height to ~1.2px. Bypasses font-size minimums because font-size-adjust operates at the glyph metric level. Detected by checking effective x-height = fontSizeAdjust × fontSize < 4px.
High math-style:compact double reduction: math-style: compact adds one implicit script-level factor of 0.71 on top of math-depth-based shrinkage. The property is documented as a layout mode, not a font-size control — static analysis tools that scan for font-size manipulation typically miss it. Detected by treating math-style: compact as equivalent to math-depth: add(1) in the total depth calculation.

Why math-depth attacks are underexplored in MCP security

The CSS math-depth property is rarely discussed in web security literature for two reasons. First, it only has a visible effect inside MathML rendering contexts, which are uncommon in non-mathematical web applications. Most security reviewers auditing consent dialog CSS do not consider MathML properties relevant. Second, the property name contains "math" which further distances it from the security mental model of "things that could hide text."

The practical attack requires that the MCP server either inject a MathML wrapper element (possible if the consent host allows arbitrary HTML injection) or that the consent dialog itself uses MathML somewhere in its structure and the disclosure element inherits the MathML context. The latter scenario is rare — but as MCP server consent UIs become more complex and adopt richer markup, the risk of accidental MathML context inheritance increases.

The font-size floor provided by getComputedStyle(el).fontSize remains the most reliable single detection point: regardless of which mechanism caused the shrinkage, an honest computed font-size below 10px on a consent disclosure element is always a finding.

← Blog  |  Security Checklist