MCP Security Reference

MCP server CSS font-variant-alternates security

CSS Fonts Level 3 font-variant-alternates provides fine-grained control over OpenType alternate glyph forms via named references declared in @font-feature-values rules: character-variant(cv1), styleset(ss1), swash(sw1), annotation(an1). With an attacker-controlled font file, the alternate glyphs behind these feature indices can be anything — including lookalike characters, invisible glyphs, or reversed/mirrored forms of specific consent letters. The textContent of the element is unchanged; the substitution happens at the rendering level. getComputedStyle(el).fontVariantAlternates returns a non-default value, and cross-referencing with @font-feature-values rules reveals which OpenType feature indices are activated.

Attack findings

HIGHSA-CSS-FVALT-001font-variant-alternates: character-variant(1) with @font-feature-values AppFont { @character-variant { cv1: 1; } }; attacker-controlled font maps 'g', 'r', 'a', 'n', 't' characters' alternate glyph (cv01 index 1) to lookalike or invisible glyphs; "grant" becomes visually altered; textContent: "grant"; standard check passes
HIGHSA-CSS-FVALT-002font-variant-alternates: styleset(1) with @font-feature-values { @styleset { ss1: 1; } }; OpenType ss01 feature substitutes a full set of characters with stylistic alternates from attacker font; entire consent vocabulary rendered in alternate forms; each alternate glyph chosen to visually mislead; textContent unchanged
MEDIUMSA-CSS-FVALT-003font-variant-alternates: annotation(1) with @font-feature-values { @annotation { an1: 1; } }; OpenType nalt feature renders characters in circled/enclosed forms; "grant" renders as circled-G circled-R circled-A circled-N circled-T; each glyph is visually a character inside a circle; scan salience altered; textContent: "grant"
MEDIUMSA-CSS-FVALT-004 — JS mousedown dynamically inserts @font-feature-values + sets fontVariantAlternates inline; static page load: fontVariantAlternates is 'normal'; runtime substitution fires at click; MutationObserver on document.head and consent element required

Background: @font-feature-values and font-variant-alternates

The font-variant-alternates property works together with @font-feature-values at-rules. The at-rule assigns human-readable names to OpenType feature index numbers. The property then activates those named features by name. The mechanism has four relevant function-style values:

An attacker controls the font file. Therefore, any character in the font can have any glyph assigned to its cv01/ss01/nalt slot. The @font-feature-values rule and the font-variant-alternates property are the keys that unlock those slots.

Detection gap: textContent is unchanged — the Unicode code points in the DOM are the original characters. The browser's text renderer chooses alternate glyphs at paint time based on the activated OpenType features. A text-extraction based scanner reads the original code points and finds nothing anomalous. The detection path requires: (1) checking getComputedStyle(el).fontVariantAlternates for non-default values, and (2) for each active feature function, cross-referencing the named index against @font-feature-values rules to understand which OpenType features are activated, and (3) checking whether the font at that element is an attacker-controlled URL.

Attack 1 — character-variant targeted letter substitution (SA-CSS-FVALT-001)

The character-variant() value is the highest precision attack. OpenType cvNN features allow per-character alternate glyphs. An attacker who controls the font file can assign any glyph to the cv01 alternate of any character. With character-variant(cv1) activating cv01, the alternate glyph for every character that has one will be used. The attacker specifically designs cv01 alternates for the characters in the key permission verb — for example, the letters in "grant", "install", "allow", "execute" — mapping them to near-invisible or lookalike forms while leaving other characters unmodified.

/* @font-feature-values declares that 'cv1' means cv01 feature index 1 */
@font-feature-values "AppUICons" {
  @character-variant {
    dangerous-chars: 1;  /* cv01 index 1 in the attacker's font */
  }
}

.consent-text {
  font-family: "AppUICons", sans-serif;
  /* cv01 alternates for 'g','r','a','n','t' in the attacker font are lookalikes */
  font-variant-alternates: character-variant(dangerous-chars);
  /* "By installing you grant us shell access" */
  /* renders as: "By installing you ɢrant us shell access" (lookalike g) */
  /* or: "By installing you gr̃nt us shell access" (combining character overlay) */
  /* textContent: "By installing you grant us shell access" — unchanged */
}

/* Detection */
function checkFontVariantAlternates(consentEl) {
  const fva = getComputedStyle(consentEl).fontVariantAlternates;
  if (!fva || fva === 'normal') return null;

  return {
    vuln: 'SA-CSS-FVALT-001',
    detail: `font-variant-alternates: ${fva} — OpenType alternate glyph activation on consent element; requires font audit`
  };
}

SA-CSS-FVALT-001 (High). font-variant-alternates: character-variant() activates per-character OpenType alternate glyphs. With an attacker-controlled font, these glyphs can be anything. Detection: flag any non-normal fontVariantAlternates on a consent element and inspect the font source URL for external/attacker-controlled origins.

Attack 2 — styleset wholesale alphabet substitution (SA-CSS-FVALT-002)

Stylesets (ssNN features) substitute coordinated character sets — entire alphabets or subsets. Where character-variant targets individual characters, a styleset can replace every letter in a consent string simultaneously with a designed alternate. A font with an ss01 styleset that maps each letter to a Unicode lookalike (Latin small letter A → Cyrillic small letter a, where both look identical but are different code points in the original Unicode, while the glyph displayed is a visually modified form) produces a consent text that reads normally to a human but cannot be machine-decoded to its original meaning from rendered pixels alone.

@font-feature-values "AppUI" {
  @styleset {
    mislead-set: 1;  /* ss01 in the attacker's font */
  }
}

.consent-text {
  font-family: "AppUI";
  font-variant-alternates: styleset(mislead-set);
  /* entire consent rendered in ss01 alternate glyphs */
  /* attacker designed ss01 to modify visual appearance of key permission words */
}

Attack 3 — annotation enclosed character forms (SA-CSS-FVALT-003)

The annotation() function activates the OpenType nalt (Alternate Annotation Forms) feature. Many fonts provide nalt glyphs that render characters inside circles, squares, or other enclosures. When applied to consent text, letters like "g", "r", "a", "n", "t" are each individually enclosed in a circle glyph: ⓖⓡⓐⓝⓣ. The word "grant" visually becomes a series of circled letters — which human readers typically associate with menu icons, list markers, or decorative elements, not with legal permission language. The consent's legal weight is reduced by its visual presentation as a series of icons rather than a sentence.

Attack 4 — runtime feature value + alternates injection (SA-CSS-FVALT-004)

At page load, no @font-feature-values rules exist and font-variant-alternates is normal. At mousedown, JS inserts a <style> element containing both the @font-feature-values declaration and a rule setting font-variant-alternates on the consent element's class. Because the attacker-controlled font file is already loaded (via preload), the alternate glyphs activate immediately. MutationObserver on both document.head and the consent element's style attribute is required for detection.

/* Detection for all attacks */
function auditFontVariantAlternates(consentEl) {
  const cs = getComputedStyle(consentEl);
  const fva = cs.fontVariantAlternates;

  if (!fva || fva === 'normal') return null;

  // Identify which feature functions are active
  const activeFns = [];
  for (const fn of ['character-variant', 'styleset', 'swash', 'annotation', 'ornaments', 'stylistic']) {
    if (fva.includes(fn)) activeFns.push(fn);
  }

  // Check font source for external/suspicious URL
  const fontFamily = cs.fontFamily;
  const findings = [{
    vuln: 'SA-CSS-FVALT-001',
    severity: 'high',
    detail: `font-variant-alternates: ${fva} — active functions: ${activeFns.join(', ')}; font-family: ${fontFamily}`,
    activeFunctions: activeFns
  }];

  return findings;
}

SkillAudit detection: SkillAudit checks getComputedStyle(el).fontVariantAlternates on all consent elements. Any value other than 'normal' triggers a High finding, followed by inspection of the font source URL. Attacker-controlled fonts (non-system, non-CDN origins) combined with alternate glyph activation are flagged as critical. Runtime injection via @font-feature-values insertion is caught by MutationObserver on document.head. Run a free audit →

Detection summary

Attack IDProperties involvedKey detection signal
SA-CSS-FVALT-001font-variant-alternates: character-variant(); @font-feature-values @character-variant; attacker font with alternate glyphs for consent charsgetComputedStyle(el).fontVariantAlternates !== 'normal'; inspect font URL for external/attacker origin
SA-CSS-FVALT-002font-variant-alternates: styleset(); @font-feature-values @styleset; ss01/ss02 feature with wholesale alphabet substitutionfontVariantAlternates includes 'styleset'; flag all styleset activations on consent elements
SA-CSS-FVALT-003font-variant-alternates: annotation(); OpenType nalt feature; characters rendered as enclosed icon formsfontVariantAlternates includes 'annotation'; annotation glyphs render permission verbs as icon sequences
SA-CSS-FVALT-004JS mousedown inserts @font-feature-values + sets font-variant-alternates; static audit: 'normal'; runtime injectionMutationObserver on document.head (style insertion) + consent element style attribute