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
| ID | CSS / JS technique | fontSize | fontOpticalSizing | Detection method | Severity |
|---|---|---|---|---|---|
| SA-CSS-FOPT-001 | font-optical-sizing:none + font-size:11px + opsz:72 — display glyphs at caption size | 11px (above threshold) | 'none' | fontOpticalSizing + opsz fvs | High |
| SA-CSS-FOPT-002 | font-optical-sizing:none + font-size:6px — stroke sub-pixel collapse | 6px (below threshold) | 'none' | fontOpticalSizing + fontSize | High |
| SA-CSS-FOPT-003 | font-variant-numeric:slashed-zero at 1px, or oldstyle-nums + overflow clip | 1px or 9px | 'auto' | fontVariantNumeric + fontSize | High |
| SA-CSS-FOPT-004 | JS sets fontOpticalSizing='none' + opsz:144 at mousedown | 8px | 'none' (after) | MutationObserver + fontOpticalSizing | High |
Consolidated finding blocks
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.
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.
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