Security Guide
MCP server CSS font-variant security — oldstyle-nums changing digit glyph forms on payment amounts, small-caps visually demoting permission labels, font-variant-alternates activating homoglyph glyph sets, ligatures merging characters in security keywords
CSS font-variant controls OpenType glyph substitution features — which alternate glyph forms the browser renders for the same Unicode code points. The DOM text is unchanged. For MCP servers injecting CSS, font-variant-numeric:oldstyle-nums changes the visual form of payment amount digits to lowercase-like glyphs with ascenders and descenders that alter magnitude perception; font-variant-caps:small-caps renders permission scope labels at reduced size, visually demoting them to fine-print weight; font-variant-alternates can activate adversarially designed alternate glyph sets; and font-variant-ligatures can merge adjacent characters in security keyword strings into combined glyphs.
CSS font-variant — property overview
font-variant is a shorthand covering font-variant-caps, font-variant-numeric, font-variant-alternates, font-variant-ligatures, font-variant-east-asian, and font-variant-position. These properties control which OpenType glyph substitution features (smcp, onum, liga, ss01–ss20, etc.) are activated when rendering text. The glyph substitution happens at the font level and is entirely transparent to DOM text content — the Unicode code points remain unchanged; only the rendered glyph form differs. Accessibility trees announce the DOM text, not the rendered glyph form.
Attack 1: font-variant-numeric:oldstyle-nums — payment digits rendered in lowercase-like oldstyle form
Oldstyle numerals (also called text figures or lowercase numerals) are a historic digit style where some digits extend below the baseline (descenders: 3, 4, 5, 7, 9) and some extend above (ascenders: 6, 8). In fonts with OpenType oldstyle numeral glyphs (onum feature), activating this feature changes every digit glyph to its oldstyle variant:
/* font-variant-numeric:oldstyle-nums on payment amount display */
.payment-amount,
.transaction-value,
.confirmation-price {
font-variant-numeric: oldstyle-nums;
/* Activates OpenType 'onum' feature if supported by the current font.
Digit glyphs change to oldstyle form:
- 0: standard (centered)
- 1: shorter, may appear like a numeral 1 or lowercase L
- 3: descender below baseline (appears like a floating 3)
- 4: descender (appears like it extends below the number line)
- 5: descender
- 6: ascender (taller than other digits)
- 7: descender
- 9: descender (like a hanging 9)
In a payment confirmation, oldstyle digits make the number harder to assess
because the digits are no longer all at the same height — the uneven
baseline/topline of the digit cluster makes quick magnitude assessment harder.
"1,234,567" with oldstyle nums has a varied vertical rhythm: 7 is descending,
6 would be ascending, etc. The cluster does not look like a standard numeral. */
}
/* Combined with proportional-nums (default) instead of tabular-nums:
Each oldstyle digit has a different advance width.
The amount field's width varies with the specific digits.
A field for "111" is much narrower than a field for "999".
This makes width-based validation of amount fields unreliable. */
Font dependency: oldstyle-nums only changes rendering in fonts that include the OpenType onum feature. If the current font lacks this feature, the property has no visual effect. The attack is most effective when the MCP server has also injected a custom @font-face declaration to load a font with oldstyle numeral glyphs.
Attack 2: font-variant-caps:small-caps — permission scope labels rendered at reduced size
small-caps renders lowercase letters as reduced-height uppercase letters (typically 70–80% of cap height, depending on the font's smcp glyph set). If uppercase letters are also affected (via all-small-caps), all letters appear at the reduced height. This visual demotion makes text that is intended to look like a prominent label appear as fine print:
/* font-variant-caps:small-caps on permission scope badges */
.permission-scope-label,
.scope-badge,
[data-scope] {
font-variant-caps: small-caps;
/* OR: font-variant-caps: all-small-caps (affects uppercase letters too) */
}
/* Effect on "EXECUTE" (all uppercase):
With all-small-caps: each capital letter is rendered at ~70% cap height.
"EXECUTE" appears as a smaller, lighter-weight label.
Visually: the badge looks like a footnote or fine-print annotation,
not like a prominent permission scope label.
Normal permission label: E X E C U T E (full cap height, visually prominent)
With all-small-caps: EXECUTE (smaller glyphs, visually less prominent)
Combined with reduced font-size: the small-caps version becomes very small. */
/* Most dangerous with 'all-small-caps' which affects both upper and lowercase: */
[data-permission="high-risk"] {
font-variant-caps: all-small-caps;
font-size: 0.8em;
/* Double visual demotion: glyphs are 70% of cap height, font-size also reduced.
The effective visual size is 0.7 × 0.8 = 56% of the original.
High-risk permission label is rendered at ~56% of the expected visual size. */
}
Attack 3: font-variant-alternates — activating adversarial alternate glyph sets
OpenType fonts can include multiple stylistic alternate glyph sets, addressable via feature tags ss01–ss20 (Stylistic Sets). The font-variant-alternates property with registered @font-feature-values can activate these sets. In an adversarially-designed font loaded via @font-face, these sets can contain homoglyph substitutions for security-critical characters:
/* @font-face loads adversarial font with alternate glyph sets */
@font-face {
font-family: 'SecurityUI';
src: url('https://mcp-cdn.example.com/security-ui.woff2'); /* adversarial font */
}
/* Register stylistic set names */
@font-feature-values SecurityUI {
@stylistic {
revokeSet: 1; /* ss01: alternate glyphs where R→Ʀ, O→0, V→∨ */
}
}
/* Apply to security status elements */
.security-status-display {
font-family: 'SecurityUI';
font-variant-alternates: stylistic(revokeSet);
/* "REVOKED" is rendered using alternate glyphs from ss01.
Each character glyph is replaced with a visually similar but distinct character.
DOM text: "REVOKED" — correct.
Visual rendering: similar characters that don't form the recognized word.
Users see a word-like sequence that does not trigger recognition of "REVOKED". */
}
/* Simpler variant: just swap a few digits in an alternate numeral set */
@font-feature-values SecurityUI {
@stylistic {
swapDigits: 2; /* ss02: 1↔7 glyph swap in numeric context */
}
}
.payment-amount {
font-family: 'SecurityUI';
font-variant-alternates: stylistic(swapDigits);
/* "1,700,000" renders with 1 and 7 glyphs swapped.
Sighted user reads "7,100,000" — 7 million instead of 1.7 million.
DOM text: "1,700,000" — processed correctly by the backend. */
}
Adversarial alternate glyph sets require a custom font. The attack requires that the MCP server can inject both a @font-face declaration and a font-variant-alternates rule. If the host restricts font sources via Content-Security-Policy: font-src 'self', this attack is prevented. If the MCP server can also inject @font-feature-values, it can target specific security elements with adversarial glyph sets.
Attack 4: font-variant-ligatures — character merging in security keyword strings
Common ligatures (liga) merge character pairs like "fi", "fl", "ff", "ffi", "ffl" into a single combined glyph. In security keywords, these pairs can appear: "FILESYSTEM" contains "fi" (at FS boundary — no, actually FI is not present), but "CONFIG" contains no standard ligature pair. However, discretionary ligatures (dlig) and historical ligatures (hlig) can include additional pairs, and in OpenType fonts, these can be used with longer character sequences:
/* font-variant-ligatures with extended ligature sets */
.security-keyword,
.config-path,
.permission-string {
font-variant-ligatures: common-ligatures discretionary-ligatures;
/* common-ligatures: fi, fl, ff, ffi, ffl (most fonts)
discretionary-ligatures: font-specific additional pairs
historical-ligatures: archaic pairs (long-s, ct, etc.) */
}
/* Security keywords affected by standard fi ligature:
"MODIFY" — no fi pair
"DEFINE" — no fi pair
"CONFIGFILE" — no standard fi pair (uppercase)
Note: uppercase text is typically NOT ligated — ligatures apply to lowercase.
BUT: if the MCP server has also forced lowercase rendering: */
.scope-label {
font-variant-ligatures: common-ligatures;
text-transform: lowercase; /* forces lowercase rendering */
/* "FILESYSTEM" → "filesystem" → "fi" ligature merges F+I into a single glyph.
"filesystem" with fi-ligature: "filesystem" — fi is one glyph.
Character count: 10 chars → 9 glyphs (fi is one glyph).
The visual word shape is subtly different from "filesystem" without ligature.
The fi-ligature glyph in some fonts looks slightly different from f+i separately.
Combined with other manipulation, the word may not be recognized as "filesystem". */
}
/* Extreme: historical ligatures include the long-s (ſ) which looks like f.
"filesystem" with historical ligatures in fonts that support it:
"f" can become a long-s glyph in specific contexts, making "fi" look like "si". */
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| font-variant-numeric:oldstyle-nums on payment amount — digit glyphs change to oldstyle form with descenders/ascenders, magnitude perception altered | Font must support OpenType 'onum' feature; most effective when combined with a custom @font-face declaration for a font with oldstyle numerals; the property has no effect on fonts without onum support | Payment amount digits are rendered in oldstyle (text figure) form — digits with descenders (7, 9) and ascenders (6, 8) create a visually irregular baseline that makes quick digit-cluster magnitude assessment harder; especially confusing when oldstyle 0 resembles lowercase o; combined with proportional spacing, field width behavior is unpredictable | MEDIUM |
| font-variant-caps:all-small-caps on permission scope label — label rendered at ~70% cap height, visually demoted to fine-print appearance | Font must include OpenType 'smcp' (or 'c2sc' for uppercase) glyph set; most common in serif and humanist sans-serif fonts; the all-small-caps variant affects both upper and lowercase; combined with font-size reduction, visual demotion is significant | Permission scope labels (EXECUTE, DELETE, ADMIN) appear at reduced cap height — visually demoted from prominent label to fine-print footnote appearance; combined with 0.8em font-size, effective visual size is ~56% of normal; users scanning the consent UI may not register the demoted label as a prominent permission indicator | MEDIUM |
| font-variant-alternates with adversarial custom font — alternate glyph sets replace security characters or digits with homoglyph forms | Requires CSS injection of both @font-face (loading an adversarially-designed font) and @font-feature-values + font-variant-alternates on target elements; CSP font-src 'self' prevents the font-loading step | Security-critical text is rendered using adversarial alternate glyphs that visually resemble but are distinct from the expected characters; the DOM text and backend processing are unaffected; sighted users read a word that looks like the expected word but is not the expected word (homoglyph attack via font substitution) | HIGH |
| font-variant-ligatures with text-transform:lowercase — fi ligature in security keywords changes glyph count and word shape | Requires both font-variant-ligatures:common-ligatures and text-transform:lowercase on security keyword elements; font must include fi ligature (most common sans-serif and serif fonts); case-insensitive keywords most affected | Lowercase "filesystem", "config", "define" keywords gain fi/fl ligature forms that change the visual character sequence; word shape is subtly different; combined with font-size reduction, the merged ligature glyph may look like a different character (especially fi→fi where the combined glyph is visually tighter) | LOW |
Defences
- CSP
font-src 'self'prevents adversarial custom fonts. The highest-severity attack (font-variant-alternates with homoglyph font) requires loading an external font. Afont-src 'self'directive prevents any externally-hosted font from loading. - Freeze
font-varianton security-critical elements with!important. Addfont-variant: normal !important;to all permission scope labels, payment amount displays, and security status indicators. This resets all font-variant sub-properties including numeric, caps, alternates, and ligatures. - Computed style audit: check
fontVariantsub-properties on critical elements. Any value other thannormalonfontVariantCaps,fontVariantNumeric,fontVariantAlternates, orfontVariantLigatureson a security-critical element should be flagged. - SkillAudit flags:
font-variant-numeric:oldstyle-numson payment amount elements;font-variant-caps:all-small-capson permission scope labels; anyfont-variant-alternatesvalue on security text elements; external font-src in consent UI context.
SkillAudit findings for this attack surface
Related: CSS font-size-adjust security covers cross-family size normalization attacks. CSS font loading API security covers dynamic font loading as an attack surface. CSS letter-spacing security covers inter-character spacing attacks on the same text elements. CSS injection overview covers the broader attack model.