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
font-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 passesfont-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 unchangedfont-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"@font-feature-values + sets fontVariantAlternates inline; static page load: fontVariantAlternates is 'normal'; runtime substitution fires at click; MutationObserver on document.head and consent element requiredBackground: @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:
- character-variant(name) — activates the OpenType
cvNNfeature for the named index. OpenType supports cv01–cv99, each of which can substitute individual characters with up to 65,535 alternate glyphs. - styleset(name) — activates the OpenType
ssNNfeature. Stylesets substitute coordinated sets of characters together (e.g., a stylistic alternate for an entire alphabet). - swash(name) — activates the OpenType
swsh/cswhfeature. Swash glyphs are decorative alternates, often with extended strokes. Can make letters harder to parse. - annotation(name) — activates the OpenType
naltfeature. Annotation alternates place characters inside shapes (circles, squares, diamonds). Renders letters as enclosed symbols.
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 ID | Properties involved | Key detection signal |
|---|---|---|
| SA-CSS-FVALT-001 | font-variant-alternates: character-variant(); @font-feature-values @character-variant; attacker font with alternate glyphs for consent chars | getComputedStyle(el).fontVariantAlternates !== 'normal'; inspect font URL for external/attacker origin |
| SA-CSS-FVALT-002 | font-variant-alternates: styleset(); @font-feature-values @styleset; ss01/ss02 feature with wholesale alphabet substitution | fontVariantAlternates includes 'styleset'; flag all styleset activations on consent elements |
| SA-CSS-FVALT-003 | font-variant-alternates: annotation(); OpenType nalt feature; characters rendered as enclosed icon forms | fontVariantAlternates includes 'annotation'; annotation glyphs render permission verbs as icon sequences |
| SA-CSS-FVALT-004 | JS mousedown inserts @font-feature-values + sets font-variant-alternates; static audit: 'normal'; runtime injection | MutationObserver on document.head (style insertion) + consent element style attribute |