MCP server CSS font-optical-sizing and font-variant-numeric security: optical sizing disabled at small size collapses variable font glyphs to unreadable blobs, slashed-zero dot rendering at 1px, display-size glyph shapes at caption size, and JS mousedown optical sizing disable

Published 2026-08-07 — SkillAudit Research

Variable fonts contain multiple glyph design masters across axes — weight, width, optical size, and others. The optical size axis (opsz) is designed so that the same typeface can have different glyph shapes at different sizes: at large display sizes, strokes are thin and elegant; at small caption sizes, strokes are heavier and serifs wider, ensuring legibility. The CSS font-optical-sizing property controls whether the browser automatically adjusts the optical size axis to match the rendered font-size. With font-optical-sizing: auto (the default), a variable font renders at font-size: 6px using its caption-optimized glyph shapes — heavier strokes, wider letterforms — which remain marginally legible. With font-optical-sizing: none, the font renders at font-size: 6px using whatever optical size the font's default or other CSS properties specify — often the display-size master, with thin decorative strokes designed for 72px headings. At 6px render size with display-master glyph shapes, stroke widths collapse to sub-pixel hairlines, serifs become invisible, and the consent text degrades to illegible ink blobs.

This is an attack that evades font-size threshold detectors: at font-size: 6px, a detector checking parseFloat(getComputedStyle(el).fontSize) < 10 will flag it. But combined with font-size: 11px (above a common 10px threshold) and font-optical-sizing: none forcing display-master glyph shapes, the attack passes the font-size check while the glyphs remain barely legible at 11px display-size rendering. This attack is distinct from font-size dynamic viewport attacks (which reduce the computed font-size below threshold) and font-stretch attacks (which compress glyph widths). It requires a variable font with an optical size axis to be effective. See also font-variant attacks for the broader font rendering attack surface.

Detection gap: parseFloat(getComputedStyle(el).fontSize) < 10 does NOT catch the font-optical-sizing: none + near-threshold font-size attack. The correct check is getComputedStyle(el).fontOpticalSizing === 'none' combined with fontSize < 14 — at any size below 14px, disabling optical sizing degrades legibility on variable fonts with wide optical size ranges.

Attack 1: font-optical-sizing:none + font-size:11px on variable font — display-master glyph shapes render at caption size (SA-CSS-FOPT-001)

The MCP server loads a variable font with a wide optical size axis (e.g., the opsz range 8–144, which covers caption to display sizes). With font-optical-sizing: auto (default) at font-size: 11px, the browser selects an opsz value of approximately 11 — using the caption-optimized glyphs with heavier strokes and wider letterforms designed for small sizes. With font-optical-sizing: none, the opsz axis is frozen at its CSS value (typically controlled by font-variation-settings: 'opsz' 72, or the font's default which may be the display size). At 11px font-size with display-size glyph shapes (opsz 72), strokes designed to be 0.5px at 72px screen rendering become 0.5 × (11/72) ≈ 0.08px at 11px — sub-pixel, rendering as grey antialiased fog rather than black ink. A font-size threshold check returns 11px — above 10px — and passes. The consent text is technically there but visually illegible.

/* MCP attack: */
@font-face {
  font-family: 'MCPConsent';
  src: url('/fonts/consent-variable.woff2') format('woff2-variations');
  /* Variable font with wide opsz axis: 8-144 */
}

.consent-disclosure {
  font-family: 'MCPConsent', sans-serif;
  font-size: 11px;                        /* above common 10px threshold */
  font-optical-sizing: none;              /* freeze at display-size master */
  font-variation-settings: 'opsz' 72;    /* explicitly set display opsz */
  /* Result: display-master glyphs at 11px = stroke widths ~0.08px
     Text renders as grey fog — visually unreadable
     getComputedStyle().fontSize:          '11px'  ← passes threshold check
     getComputedStyle().fontOpticalSizing: 'none'  ← reveals the attack */
}

// Detection:
function detectOpticalSizingAttack(el) {
  const cs = window.getComputedStyle(el);
  const fos = cs.fontOpticalSizing;
  const fvs = cs.fontVariationSettings;
  const fontSize = parseFloat(cs.fontSize);

  if (fos === 'none' && fontSize < 14) {
    console.error('SA-CSS-FOPT-001: font-optical-sizing:none at small font size — display glyphs at caption size', {
      el, fontOpticalSizing: fos, fontSize, fontVariationSettings: fvs
    });
  }
  // Check if fontVariationSettings explicitly sets large opsz at small font-size
  if (fvs && fvs !== 'normal' && fontSize < 14) {
    const opszMatch = fvs.match(/'opsz'\s+([\d.]+)/);
    if (opszMatch) {
      const opsz = parseFloat(opszMatch[1]);
      if (opsz > 36 && fontSize < 14) {
        console.error('SA-CSS-FOPT-001: large opsz variation at small font-size forces display glyphs', {
          el, fontSize, opsz, fontVariationSettings: fvs
        });
      }
    }
  }
}

Attack 2: font-optical-sizing:none + font-size:6px — strokes collapse to sub-pixel below any legibility threshold (SA-CSS-FOPT-002)

At font-size: 6px with font-optical-sizing: none and an opsz value frozen at the display master (72+), individual glyph strokes become sub-pixel on all screen densities including 3× retina displays. A standard font-size threshold detector set at < 10px catches this case. However, the MCP server may rely on this pair to evade detectors that only check font-size against a lower threshold of < 8px (some implementations use 8px as the minimum legible size). At 6px with display-size optical sizing, the glyphs are less legible than at 6px with auto sizing — so the combination makes the attack worse and the detection harder: the MCP server may argue that 8px is the "legible threshold" while the glyph design makes even 8px unreadable at display optical size.

/* MCP attack: */
.consent-disclosure {
  font-family: 'VariableDisplayFont', sans-serif;
  font-size: 6px;                         /* below 8px and 10px thresholds */
  font-optical-sizing: none;              /* prevents caption-size adaptation */
  /* Without font-optical-sizing:none: 6px renders with heavy caption glyphs
     (still barely readable by a determined user)
     With font-optical-sizing:none: 6px renders with thin display glyphs
     (strokes ~0.04px — completely invisible on all displays)
     fontOpticalSizing check: 'none' — reveals attack
     fontSize check: 6px — reveals sub-threshold size */
}

// Detection:
function detectOpticalSizingSubThreshold(el) {
  const cs = window.getComputedStyle(el);
  const fontSize = parseFloat(cs.fontSize);
  const fos = cs.fontOpticalSizing;

  // Even at font-sizes 8-13px, disabling optical sizing is suspicious
  if (fontSize < 14 && fos === 'none') {
    const severity = fontSize < 8 ? 'HIGH' : 'MEDIUM';
    console.error(`SA-CSS-FOPT-002: [${severity}] font-optical-sizing:none at small size`, {
      el, fontSize, fontOpticalSizing: fos
    });
  }
}

Attack 3: font-variant-numeric:slashed-zero with font-size:1px — consent permissions rendered as single-pixel dots (SA-CSS-FOPT-003)

font-variant-numeric: slashed-zero requests a zero glyph variant with a diagonal slash through it — a different character shape intended to distinguish 0 from O. On consent text that includes numeric permission identifiers, counts, or permission numbers (e.g., "grant access to 0 files" or "permission level: 0"), the slash variant renders the zero with additional ink — which at font-size: 1px makes each zero character a single-pixel dot with a diagonal brush stroke. The full consent text is present in textContent; the rendered output is a 1px-high string of dots. fontVariantNumeric is a distinct property from fontVariant — a scanner checking only getComputedStyle(el).fontVariant returns the shorthand-combined value and may not parse the numeric subfeature independently.

/* MCP attack: */
.consent-disclosure {
  font-size: 1px;                            /* extreme sub-threshold size */
  font-variant-numeric: slashed-zero;        /* slash variant for zeros */
  /* fontVariantNumeric is distinct from fontVariant shorthand
     Consent text "0 permissions granted" renders as ~1px line of dots
     getComputedStyle().fontSize:           '1px'          ← caught by size check
     getComputedStyle().fontVariantNumeric: 'slashed-zero' ← distinct property */
}

/* More sophisticated variant: near-threshold + numeric variants */
.consent-disclosure {
  font-size: 9px;                            /* near 10px threshold */
  font-variant-numeric: oldstyle-nums;       /* old-style descending numerals */
  line-height: 1;
  overflow: hidden;
  height: 9px;
  /* Old-style numerals have descenders (like 3, 4, 5, 7, 9 dropping below baseline)
     With overflow:hidden + height equal to font-size:
     descending numerals are clipped below baseline
     The numerals in "3 files, 4 network permissions" are partially clipped */
}

// Detection:
function detectFontVariantNumeric(el) {
  const cs = window.getComputedStyle(el);
  const fvn = cs.fontVariantNumeric;
  const fontSize = parseFloat(cs.fontSize);

  // Flag any non-normal fontVariantNumeric at small sizes
  if (fvn && fvn !== 'normal' && fontSize < 14) {
    console.error('SA-CSS-FOPT-003: non-default font-variant-numeric at small font size', {
      el, fontVariantNumeric: fvn, fontSize
    });
  }
  // Also check for oldstyle-nums + overflow:hidden height clipping
  if (fvn?.includes('oldstyle-nums')) {
    const oh = el.offsetHeight;
    const lh = parseFloat(cs.lineHeight);
    if (oh <= lh + 2 && cs.overflow === 'hidden') {
      console.error('SA-CSS-FOPT-003: oldstyle-nums descenders clipped by overflow:hidden height match', {
        el, fontVariantNumeric: fvn, offsetHeight: oh, lineHeight: lh
      });
    }
  }
}

Attack 4: JS mousedown disables font-optical-sizing on consent — glyph collapse at install click (SA-CSS-FOPT-004)

The consent loads with font-optical-sizing: auto (default) and a small-but-readable variable font at font-size: 8px. At load time, the caption-optimized glyphs render with appropriate stroke weights — marginally readable. An auditor evaluating the page at load time sees 8px text (possibly flagged but still readable). At mousedown on the install button, JS sets el.style.fontOpticalSizing = 'none' and el.style.fontVariationSettings = "'opsz' 144". The font immediately switches from caption-master to the display-master at the large end of the optical size range. At 8px font-size with opsz=144 design, strokes designed for 144px text collapse to sub-pixel — the consent text visually disappears during the install click. Because both changes are applied before the browser repaints, the user experiences the text as invisible at the moment they click.

/* Baseline CSS — loads with readable (if small) text: */
.consent-disclosure {
  font-family: 'VariableDisplayFont', sans-serif;
  font-size: 8px;
  /* Default: font-optical-sizing: auto — caption glyphs at 8px */
  /* Readable (barely) at load time */
}

// MCP JS — triggers at install click:
document.querySelector('#install-btn').addEventListener('mousedown', () => {
  const el = document.querySelector('.consent-disclosure');
  if (el) {
    el.style.fontOpticalSizing = 'none';
    el.style.fontVariationSettings = "'opsz' 144";  /* large display opsz */
    /* 8px font with opsz=144 design: strokes ~0.03px — invisible
       Changes applied in same task before repaint
       Consent collapses from barely-readable to invisible
       Looks instantaneous — no visible transition */
  }
}, { capture: true });

// Detection:
function detectDynamicOpticalSizing() {
  document.querySelectorAll('.consent-disclosure, [data-consent]').forEach(el => {
    const observer = new MutationObserver(() => {
      const cs = window.getComputedStyle(el);
      if (cs.fontOpticalSizing === 'none') {
        const fontSize = parseFloat(cs.fontSize);
        const fvs = cs.fontVariationSettings;
        if (fontSize < 14) {
          console.error('SA-CSS-FOPT-004: JS disabled font-optical-sizing at interaction time', {
            el, fontOpticalSizing: cs.fontOpticalSizing, fontSize, fontVariationSettings: fvs
          });
        }
        // Check opsz in fontVariationSettings
        if (fvs) {
          const opszMatch = fvs.match(/'opsz'\s+([\d.]+)/);
          if (opszMatch && parseFloat(opszMatch[1]) > 36 && fontSize < 14) {
            console.error('SA-CSS-FOPT-004: JS set large opsz at small font-size after optical sizing disable', {
              el, opsz: parseFloat(opszMatch[1]), fontSize
            });
          }
        }
      }
    });
    observer.observe(el, { attributes: true, attributeFilter: ['style'] });
    document.querySelector('#install-btn, [data-action="install"]')
      ?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
  });
}

Root detection method for all font-optical-sizing attacks: Check getComputedStyle(el).fontOpticalSizing independently — it is a distinct property from fontVariant, fontStretch, or font-size. Flag fontOpticalSizing === 'none' whenever fontSize < 14px. Additionally check fontVariationSettings for explicit 'opsz' values above 36 at small font sizes — these force display-master glyph shapes at caption render sizes. Check fontVariantNumeric separately from fontVariant. SkillAudit audits all four properties on every consent element and cross-references font-size against optical sizing configuration.

Attack summary

IDCSS / JS techniquefontSizefontOpticalSizingDetection methodSeverity
SA-CSS-FOPT-001font-optical-sizing:none + font-size:11px + opsz:72 — display glyphs at caption size11px (above threshold)'none'fontOpticalSizing + opsz fvsHigh
SA-CSS-FOPT-002font-optical-sizing:none + font-size:6px — stroke sub-pixel collapse6px (below threshold)'none'fontOpticalSizing + fontSizeHigh
SA-CSS-FOPT-003font-variant-numeric:slashed-zero at 1px, or oldstyle-nums + overflow clip1px or 9px'auto'fontVariantNumeric + fontSizeHigh
SA-CSS-FOPT-004JS sets fontOpticalSizing='none' + opsz:144 at mousedown8px'none' (after)MutationObserver + fontOpticalSizingHigh

Consolidated finding blocks

High CSS font-optical-sizing:none at 11px with opsz:72 — display-master glyph shapes render at caption size, strokes collapse to grey fog: Passes fontSize > 10px threshold check. Variable font's display-optimized shapes (thin strokes, delicate serifs designed for 72px) render at 11px — stroke widths ~0.08px on standard displays. Consent text degrades to illegible grey fog. Only fontOpticalSizing === 'none' + small-size cross-check reveals the attack.
High CSS font-optical-sizing:none at 6px — optical size adaptation disabled makes small variable font text worse than default small text: Standard font-size threshold catches the 6px size. But the combination of sub-threshold size AND disabled optical sizing means the text is even less legible than standard 6px text — the attack is doubly effective. Both signals must be reported when co-occurring.
High CSS font-variant-numeric:slashed-zero at 1px renders consent as single-pixel dot string: fontVariantNumeric is distinct from the fontVariant shorthand. Scanners checking only fontVariant may not decompose the numeric subfeature. Combined with 1px font-size, all consent text renders as a single-pixel dot row. Detection: check fontVariantNumeric !== 'normal' when fontSize < 14px.
High JS disables font-optical-sizing + sets display opsz at mousedown — consent glyph collapse during install click: Load-time audit sees 8px text with auto optical sizing — caption glyphs, marginally readable. At mousedown, JS sets fontOpticalSizing = 'none' and fontVariationSettings = "'opsz' 144". Both changes applied before repaint — consent visually disappears at install moment. MutationObserver on style attribute detects the dynamic property changes.

CSS font-size dynamic viewport security  |  CSS font-stretch security  |  CSS font-variant security  |  Security Checklist