MCP Security Reference

MCP server CSS font-variant-ligatures security

CSS Fonts Level 3 font-variant-ligatures controls which OpenType ligature features are active. The standard values are common-ligatures (activates liga/clig), discretionary-ligatures (dlig), historical-ligatures (hlig), and contextual-alternates (calt). Ligature features merge sequences of characters into single composite glyphs. The rendered glyph is a single shape covering what was two or more code points — but the DOM retains the original characters. With an attacker-controlled font, the glyph assigned to any ligature pair can be any shape: a visually unrelated character, an ornament, or an ambiguous merged form that reads differently from the individual characters. The textContent is unchanged. Detection requires checking getComputedStyle(el).fontVariantLigatures for dangerous non-default values.

Attack findings

HIGHSA-CSS-FVLIG-001font-variant-ligatures: historical-ligatures on consent element with attacker-controlled font; OpenType hlig feature active; attacker assigns hlig glyph for 'gr' pair to a single ambiguous archaic-looking stroke; "grant" renders as an unrecognizable merged glyph for 'gr' + 'ant'; textContent: "grant"; historical ligatures are disabled by default for a reason
HIGHSA-CSS-FVLIG-002font-variant-ligatures: discretionary-ligatures on consent element; OpenType dlig feature active; attacker font's dlig glyph for 'an' pair is a lookalike ornament; "grant" splits as "gr" + "ant" → "gr" + ligature(a+n) + "t"; the 'an' merge glyph looks similar to 'æ' or 'œ'; consent meaning altered at word boundaries; textContent unchanged
MEDIUMSA-CSS-FVLIG-003font-variant-ligatures: contextual-alternates on consent element; OpenType calt feature substitutes characters based on neighboring characters; attacker font's calt rule changes the glyph of 'n' when followed by 'o' → 'not' reads with an alternate 'n' that curves away from 'o', visually separating them; "not" reads as "ηot"; textContent: "not"
MEDIUMSA-CSS-FVLIG-004 — JS mousedown sets consentEl.style.fontVariantLigatures = 'historical-ligatures discretionary-ligatures'; static audit finds 'normal'; ligature merge rendering fires at click; MutationObserver on consent element style attribute required

Background: OpenType ligature features and font-variant-ligatures

Ligatures are composite glyphs that replace a sequence of two or more characters with a single shape. Common ligatures (fi, fl, ff, ffi, ffl) are activated by default in most browsers via the OpenType liga and clig features — they improve readability for common character pairs where individual glyphs would collide. Three additional ligature categories are off by default:

These features are off by default precisely because they produce unexpected rendering. An MCP server that explicitly enables them on a consent element — especially with an attacker-controlled font — does so for one reason: to produce unexpected rendering on the consent text specifically.

Detection gap: Ligature rendering is purely a glyph-level operation. The Unicode code points in the DOM are unchanged — textContent always returns the original character sequence. The rendered glyph for a ligature pair is a single shape that may look completely different from either individual character. A text-content checker that reads textContent and compares it to expected strings will find the original text. Only getComputedStyle(el).fontVariantLigatures reveals which ligature features are active.

Attack 1 — historical-ligatures archaic merge forms (SA-CSS-FVLIG-001)

Historical ligatures (OpenType hlig) are off by default because they use archaic forms that modern readers do not recognize. Common historical ligatures include combinations like ꝁt, ꝃ, Ꝅ, or the long-s+t merge (ſt). With an attacker-controlled font, the hlig table can contain any pair → glyph mapping. Setting hlig for the "gr" pair in a font to an ambiguous historical-looking single stroke makes "grant" render as an archaic-looking merge glyph for "gr" followed by "ant". Casual readers parse the word as a decorative typographic element rather than the legal term "grant".

/* Attack */
.consent-text {
  font-family: "AppHistoric", serif;  /* attacker-controlled font with custom hlig table */
  font-variant-ligatures: historical-ligatures;
  /* "you grant us shell access" */
  /* 'gr' pair → attacker hlig glyph → unrecognizable archaic merged form */
  /* textContent: "you grant us shell access" — unchanged */
}

/* Detection */
function checkFontVariantLigatures(consentEl) {
  const fvl = getComputedStyle(consentEl).fontVariantLigatures;
  if (!fvl || fvl === 'normal') return null;

  const dangerous = ['historical-ligatures', 'discretionary-ligatures'];
  const active = dangerous.filter(v => fvl.includes(v));
  if (active.length === 0) return null;

  return {
    vuln: 'SA-CSS-FVLIG-001',
    detail: `font-variant-ligatures: ${fvl} — non-default ligature features active on consent element`,
    activeFeatures: active
  };
}

SA-CSS-FVLIG-001 (High). historical-ligatures activates OpenType hlig — archaic merged forms for character pairs. With an attacker font, any consent character pair can have an arbitrary hlig glyph. These ligatures are off by default; any explicit activation on a consent element is suspicious. Flag fontVariantLigatures containing 'historical-ligatures'.

Attack 2 — discretionary-ligatures designer-chosen merges (SA-CSS-FVLIG-002)

Discretionary ligatures (dlig) are typographer-chosen merges for aesthetic reasons — they are not required for readability and are explicitly off by default. An attacker font can include dlig entries for character pairs found in consent permission verbs. The "an" pair (appearing in "grant", "scan", "manage") with a dlig glyph designed to look like the Latin digraph "æ" changes "grant" to "grœt" visually. The "st" pair (appearing in "install", "access") with a dlig glyph that uses an extended swash stroke can make the "st" ligature look like an "ŝ" — a single character rather than two.

/* Attack */
.consent-text {
  font-family: "AppDesign", sans-serif;
  font-variant-ligatures: discretionary-ligatures;
  /* attacker's dlig table maps 'an' → æ-lookalike glyph */
  /* "grant" → "gr" + æ + "t" */
  /* "manage" → "m" + æ + "ge" */
  /* textContent: "grant", "manage" — unchanged */
}

/* Detection note: fontVariantLigatures === 'normal' is the safe baseline */
/* 'common-ligatures' is the browser default and acceptable */
/* flag: 'discretionary-ligatures', 'historical-ligatures', combinations thereof */

Attack 3 — contextual-alternates neighbor-triggered substitution (SA-CSS-FVLIG-003)

Contextual alternates (calt) is enabled by default in some browsers. When explicitly set alongside historical-ligatures or discretionary-ligatures, it compounds the attack: not only are pair merges active, but individual characters also change shape based on context. An attacker font's calt rule can change the glyph of "n" when it follows a space and precedes "o" — making "not" render with an alternate "n" that visually separates from the "o". The word "not" becomes harder to read as a negation; "do not install" might parse as "do ηot install" with a Greek-letter-like "n". The attack is context-sensitive: the substitute glyph only appears in that specific context, making it harder to catch with string-matching scans.

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

At page load, font-variant-ligatures is normal. At mousedown, JS sets the property inline to activate both historical-ligatures and discretionary-ligatures. Because the font file is preloaded, the hlig and dlig tables are already in the browser's font cache and apply immediately. The consent text changes appearance in the click frame. MutationObserver on the consent element's style attribute catches the injection; re-checking fontVariantLigatures on each mutation detects the attack.

/* Detection — complete */
function auditFontVariantLigatures(consentEl) {
  const cs = getComputedStyle(consentEl);
  const fvl = cs.fontVariantLigatures;

  // 'normal' and 'common-ligatures' are safe baselines
  if (!fvl || fvl === 'normal' || fvl === 'common-ligatures') return null;

  // Flag any non-default ligature activation
  const dangerousValues = [
    'historical-ligatures',
    'discretionary-ligatures',
    'no-common-ligatures',  // disabling common ligatures also suspicious
  ];
  const active = dangerousValues.filter(v => fvl.includes(v));

  if (active.length === 0) return null;

  return {
    vuln: 'SA-CSS-FVLIG-001',
    severity: 'high',
    detail: `font-variant-ligatures: ${fvl} — non-default ligature features: ${active.join(', ')}`,
    fontFamily: cs.fontFamily
  };
}

// Runtime monitor
new MutationObserver(() => {
  const result = auditFontVariantLigatures(consentEl);
  if (result) {
    flagTampering(result.vuln, result.detail);
    installBtn.disabled = true;
  }
}).observe(consentEl, { attributes: true, attributeFilter: ['style', 'class'] });

SkillAudit detection: SkillAudit checks getComputedStyle(el).fontVariantLigatures on all consent elements. It flags historical-ligatures and discretionary-ligatures at High severity (arbitrary glyph merge), no-common-ligatures at Medium (disabling standard ligatures can also mislead), and any combination at High. Runtime injection via inline style is caught by MutationObserver. Run a free audit →

Detection summary

Attack IDProperties involvedKey detection signal
SA-CSS-FVLIG-001font-variant-ligatures: historical-ligatures; OpenType hlig; archaic merged forms for character pairs; attacker font with custom hlig tablefontVariantLigatures includes 'historical-ligatures'; these are off by default for a reason
SA-CSS-FVLIG-002font-variant-ligatures: discretionary-ligatures; OpenType dlig; designer-chosen merges for character pairs in consent text; textContent unchangedfontVariantLigatures includes 'discretionary-ligatures'; flag all dlig activation on consent elements
SA-CSS-FVLIG-003font-variant-ligatures: contextual-alternates; OpenType calt; neighbor-context-triggered glyph substitution; 'not' visually alteredfontVariantLigatures explicitly includes 'contextual-alternates' combined with other dangerous values
SA-CSS-FVLIG-004JS mousedown sets font-variant-ligatures inline; static: 'normal'; runtime merge fires at click; preloaded font caches hlig/dlig tablesMutationObserver on consent element style attribute; re-check fontVariantLigatures on each change