MCP Security Reference

MCP server CSS font-variant-numeric security

CSS Fonts Level 3 font-variant-numeric selects alternate glyph forms for numeric characters: diagonal-fractions converts 1/3 to a single precomposed fraction glyph (⅓); stacked-fractions renders fractions with a horizontal bar; ordinal shrinks ordinal suffixes to superscript. In a consent string like "you grant access to 1/3 of your files", diagonal-fractions renders this as "you grant access to ⅓ of your files" — visually suggesting a smaller, fraction-like quantity rather than 33% of all files. The textContent is unchanged: "1/3" is still there. Standard text-content checks pass. The detection signal is getComputedStyle(el).fontVariantNumeric, which returns the non-default value when an attack is active.

Attack findings

HIGHSA-CSS-FVNUM-001font-variant-numeric: diagonal-fractions on consent element; "1/3 of your files" renders as "⅓ of your files"; the diagonal fraction glyph visually implies a fraction of one item rather than 33% of all; textContent unchanged; getComputedStyle(el).fontVariantNumeric returns 'diagonal-fractions'; standard textContent check passes
HIGHSA-CSS-FVNUM-002font-variant-numeric: stacked-fractions on consent element; "1/2 access level" renders with numerator stacked over denominator with horizontal bar; visual: ½ (superscript 1, bar, subscript 2); adjacent consent words split around the fraction form; meaning alteration for fraction-denominated permissions; textContent: "1/2 access level"
MEDIUMSA-CSS-FVNUM-003font-variant-numeric: ordinal on consent element; "1st party access" renders as "1ˢᵗ party access"; ordinal suffix becomes superscript; visual: number appears larger, suffix is small and easy to miss; "1st" vs "1ˢᵗ" has different scan salience; textContent unchanged; detection: fontVariantNumeric includes 'ordinal'
MEDIUMSA-CSS-FVNUM-004 — JS mousedown sets consentEl.style.fontVariantNumeric = 'diagonal-fractions'; static audit finds 'normal'; fraction rendering fires at click gesture; MutationObserver on consent element style attribute required for runtime detection

Background: CSS font-variant-numeric glyph forms

The font-variant-numeric property is part of the CSS Fonts Level 3 specification. It selects from among alternate numeric glyph forms that many OpenType fonts provide via features like frac (diagonal fractions), afrc (alternative fractions/stacked), and ordn (ordinals). The property accepts space-separated keywords from several groups:

The security risk concentrates in the fraction and ordinal forms, because these change the visual rendering of a numeric expression in ways that alter its apparent meaning — not just its appearance.

Detection gap: textContent always contains the original character sequence — 1/3, not . A consent checker that reads textContent and compares it to expected strings will see the original text and conclude the consent is unmodified. The visible rendering, however, shows the precomposed fraction glyph. The correct detection signal is getComputedStyle(el).fontVariantNumeric — any value other than 'normal' on a consent element warrants inspection.

Attack 1 — diagonal-fractions alters fraction consent quantities (SA-CSS-FVNUM-001)

The most semantically impactful attack uses diagonal-fractions. When consent text contains a fraction like "1/3 of your stored files will be indexed", the frac OpenType feature produces a compact diagonal fraction glyph: ⅓. This glyph visually registers as a small, fraction-like symbol — the human eye parses it as a single token rather than a composed expression. The magnitude implied by ⅓ as a visual unit feels smaller than reading "one slash three" sequentially. More critically, an attacker can use fractions to obscure permission scope: "access to 1/1 of your files" contains "1/1" — rendered as a fraction, this appears as the number 1 with a fraction bar and the number 1 below it, which looks like a fraction (equal to one) rather than the literal string "all of your files".

/* Attack */
.consent-text {
  font-variant-numeric: diagonal-fractions;
  /* "you grant us access to 1/3 of your stored data" */
  /* renders as: "you grant us access to ⅓ of your stored data" */
  /* textContent: "you grant us access to 1/3 of your stored data" — unchanged */
}

/* Detection */
function checkFontVariantNumeric(consentEl) {
  const fvn = getComputedStyle(consentEl).fontVariantNumeric;
  if (!fvn || fvn === 'normal') return null;

  const risky = ['diagonal-fractions', 'stacked-fractions', 'ordinal'];
  const active = risky.filter(v => fvn.includes(v));
  if (active.length === 0) return null;

  return {
    vuln: 'SA-CSS-FVNUM-001',
    detail: `font-variant-numeric: ${fvn} — numeric glyph substitution active on consent element`,
    activeValues: active
  };
}

SA-CSS-FVNUM-001 (High). Detection: getComputedStyle(consentEl).fontVariantNumeric. Flag any value including diagonal-fractions or stacked-fractions on a consent-bearing element. These values activate OpenType frac/afrc features that substitute slash-separated digit pairs with visually different fraction glyphs.

Attack 2 — stacked-fractions (SA-CSS-FVNUM-002)

The stacked-fractions value activates the OpenType afrc feature. Unlike diagonal fractions (which produce a compact diagonal glyph), stacked fractions render the numerator above a horizontal bar with the denominator below — the classic "vulgar fraction" typographic form. This rendering is more obviously a fraction, but it is still semantically manipulative: "1/2 data access" rendered with stacked fractions produces a visually prominent ½ symbol at the start of the phrase. The remaining words "data access" now appear as a qualification of the fraction rather than as the subject of the permission grant. The cognitive effect is that the reader parses "½ data-access grant" (half of a data access permission) rather than "half of all data will be accessed".

/* Attack */
.consent-dialog .permission-scope {
  font-variant-numeric: stacked-fractions;
  /* "1/2 data access permissions granted" */
  /* renders as: ½ data access permissions granted */
  /* textContent: "1/2 data access permissions granted" */
}

SA-CSS-FVNUM-002 (High). stacked-fractions activates OpenType afrc feature. The horizontal-bar fraction form is visually prominent and changes how the human brain parses consent scope. Any consent element with stacked-fractions in fontVariantNumeric should be flagged.

Attack 3 — ordinal superscript suffix shrinkage (SA-CSS-FVNUM-003)

The ordinal value activates the OpenType ordn feature. Ordinal suffixes — st, nd, rd, th — are rendered as small superscripts adjacent to the preceding digit. "1st party data" becomes "1ˢᵗ party data". The effect is subtle: the ordinal suffix becomes small enough that casual readers may skip it and parse the string as "1 party data", which scans differently than "1st party data" (1st-party being a specific data access category with specific legal meaning). Combined with other font manipulation, the ordinal superscript can be made even smaller — approaching the size of punctuation.

/* Attack */
.consent-text {
  font-variant-numeric: ordinal;
  /* "1st party cookies" → "1ˢᵗ party cookies" */
  /* "3rd party data sharing" → "3ʳᵈ party data sharing" */
  /* textContent: "1st party cookies" (unchanged) */
  /* detection: getComputedStyle(el).fontVariantNumeric includes 'ordinal' */
}

Attack 4 — JS mousedown injection (SA-CSS-FVNUM-004)

At page load, the consent element has font-variant-numeric: normal. A static audit finds nothing suspicious. At mousedown, JS sets consentEl.style.fontVariantNumeric = 'diagonal-fractions'. The browser immediately applies the frac OpenType feature to any slash-delimited digit pairs in the consent text. The consent appears to change its numeric rendering at the moment of interaction. This is a runtime-only attack; only a MutationObserver watching the consent element's inline style catches it.

/* Attack */
installBtn.addEventListener('mousedown', () => {
  consentEl.style.fontVariantNumeric = 'diagonal-fractions';
  /* "1/3 of your files" immediately renders as "⅓ of your files" */
});

/* Detection */
new MutationObserver((mutations) => {
  for (const m of mutations) {
    if (m.type === 'attributes' && m.attributeName === 'style') {
      const fvn = getComputedStyle(consentEl).fontVariantNumeric;
      if (fvn && fvn !== 'normal') {
        flagTampering('SA-CSS-FVNUM-004', fvn);
        installBtn.disabled = true;
      }
    }
  }
}).observe(consentEl, { attributes: true, attributeFilter: ['style', 'class'] });

SkillAudit detection: SkillAudit checks getComputedStyle(el).fontVariantNumeric on all consent-bearing elements. It flags any value other than 'normal', with diagonal-fractions and stacked-fractions at High severity (meaning alteration) and ordinal at Medium severity (visual de-emphasis). Runtime injection is caught via MutationObserver. Run a free audit →

Detection summary

Attack IDProperties involvedKey detection signal
SA-CSS-FVNUM-001font-variant-numeric: diagonal-fractions; slash-digit pairs rendered as precomposed fraction glyphs; textContent unchangedgetComputedStyle(el).fontVariantNumeric includes 'diagonal-fractions'
SA-CSS-FVNUM-002font-variant-numeric: stacked-fractions; fractions with horizontal bar; alters visual parsing of permission scopegetComputedStyle(el).fontVariantNumeric includes 'stacked-fractions'
SA-CSS-FVNUM-003font-variant-numeric: ordinal; ordinal suffixes (st, nd, rd, th) shrunk to superscript; "1st party" → "1ˢᵗ party"getComputedStyle(el).fontVariantNumeric includes 'ordinal'
SA-CSS-FVNUM-004JS mousedown sets fontVariantNumeric inline; static audit clean; runtime glyph substitution fires at clickMutationObserver on consent element style attribute; re-check fontVariantNumeric on mutation