MCP server CSS font-variant and font-feature-settings consent security
CSS font-variant and font-feature-settings control OpenType glyph substitution and typographic feature selection at the font engine level. Most attacks in these families are low-severity in isolation — they alter how characters look, not whether they render. The exception is font-variant-caps:all-small-caps on a font that lacks small-caps glyphs: the browser may substitute empty or .notdef (□) glyphs for all uppercase characters, making the consent text partially or fully unrenderable. Four SA-CSS-FNV patterns document this attack surface.
Font-variant and font-feature-settings attacks operate at the OpenType font shaping layer. Unlike CSS visibility or geometry attacks, these affect which glyphs the font engine selects for each character codepoint. Most typographic feature changes (kerning, ligatures, numeric variants) produce subtly different letterforms without hiding text. The high-severity case is when a requested font variant has no corresponding glyphs in the active font file: the font engine substitutes a fallback, which may be a zero-width empty glyph or the standard .notdef box.
These attacks are font-specific: the same CSS applied to a system font (which usually provides synthetic small-caps) may show text normally, while the same CSS applied to a custom @font-face web font without small-caps data silently drops characters. This font-dependency makes detection harder: a static CSS analysis cannot determine glyph availability without querying the actual font metrics.
SA-CSS-FNV-001: font-variant-caps:all-small-caps — empty glyph substitution
The CSS specification states that when font-variant-caps:all-small-caps is applied and the font lacks small-caps glyphs, the browser should synthesize them by scaling down the uppercase glyphs. However, synthesis behaviour is not uniform: some browsers only synthesize when the font-synthesis property is not disabled; some custom web fonts explicitly set font-synthesis:none in their @font-face; and some fonts provide small-caps Unicode substitution tables that map to empty glyphs. The attack: load a custom web font with empty small-caps substitution entries, then apply font-variant-caps:all-small-caps. The browser uses the font’s substitution table, producing empty output for all uppercase letters.
/* Attack: custom font with empty small-caps glyphs */
@font-face {
font-family: 'ConsentFont';
src: url('/fonts/consent-font.woff2') format('woff2');
font-style: normal;
font-weight: 400;
/* This font file has empty glyph substitution entries for
the 'smcp' (small-caps) OpenType feature tag.
When smcp feature is activated, uppercase letters map to
empty zero-width glyphs instead of scaled lowercase letterforms. */
}
.consent-text {
font-family: 'ConsentFont', serif;
font-variant-caps: all-small-caps;
/* all-small-caps activates OpenType 'smcp' feature.
Browser requests smcp-substituted glyphs from font engine.
Font returns empty glyph entries → consent text invisible.
getComputedStyle(el).fontVariantCaps → 'all-small-caps'
el.textContent → "By clicking Allow, you consent to..." (DOM intact)
el.getBoundingClientRect().height → may still show line height space
Visual: all uppercase characters render as empty boxes or nothing. */
}
/* Variant: font-variant-caps:unicase — maps some letters to empty glyphs */
.consent-text {
font-family: 'ConsentFont', serif;
font-variant-caps: unicase;
/* 'unic' OpenType feature: single case — some chars may map to empty */
}
/* Detection:
const style = getComputedStyle(el);
if (style.fontVariantCaps !== 'normal') {
// Flag for canvas rendering test
// Render first 3 chars via OffscreenCanvas
// If all pixels are background color → CRITICAL (SA-CSS-FNV-001)
// Higher-confidence: check if font-family is a custom @font-face
// (system fonts usually have synthesis fallback)
} */
Font synthesis may not save you — font-synthesis:none disables it: Browser small-caps synthesis (scaling uppercase glyphs) is the fallback when the font lacks smcp glyphs. A custom @font-face with font-synthesis:none in the font-face declaration or applied as a CSS property to the element prevents synthesis, leaving empty glyph rendering as the only output. Detection requires a canvas pixel-count test or font metric inspection.
font-variant-caps:all-small-caps on a custom font with empty smcp glyphs: uppercase consent text renders as empty glyphs. Detection: fontVariantCaps !== 'normal' on custom @font-face element + canvas pixel test → CRITICAL.SA-CSS-FNV-002: font-feature-settings:'kern' 0 — kerning disabled spacing collapse
Kerning is the per-pair adjustment of spacing between specific character combinations (e.g., ‘AV’, ‘To’, ‘fi’) to improve visual letter-spacing. Disabling kerning with font-feature-settings: 'kern' 0 modifies the advance widths of certain character pairs. In most fonts, this results in slightly wider or narrower spacing for specific pairs. On fonts with aggressive negative kern table entries for consent-text letter combinations, disabling kerning can cause visual overlap of characters — a low-severity rendering degradation. More significantly, when combined with an already-narrow letter-spacing setting, disabling kerning removes the positive kerning compensation, causing some character pairs to collide and become illegible.
/* Attack: kerning disabled on a font with aggressive positive kern values */
.consent-text {
font-family: 'TightConsentFont'; /* font with large positive kern values */
font-feature-settings: 'kern' 0;
letter-spacing: -0.05em; /* slight negative tracking */
/* 'kern' 0: disables OpenType kern feature.
Without kern: character pairs lose their positive spacing compensation.
Combined with -0.05em letter-spacing: pairs that relied on positive
kerning for separation now overlap visually.
Result: "Allow" → characters partially overlap, legibility reduced.
getComputedStyle.letterSpacing → '-0.05em' (visible, can be checked)
getComputedStyle.fontFeatureSettings → '"kern" 0' (checkable)
Neither triggers a standard audit rule alone.
Compound: both together → MEDIUM/HIGH on legibility. */
}
/* Variant: disable multiple features simultaneously */
.consent-text {
font-feature-settings: 'kern' 0, 'liga' 0, 'calt' 0;
/* Also disables ligatures and contextual alternates.
On some fonts this removes visual distinction between similar chars
(e.g., 'l' and 'I', '0' and 'O') and reduces overall readability. */
}
/* Detection:
const ffs = getComputedStyle(el).fontFeatureSettings;
// Parse font-feature-settings value
if (ffs !== 'normal') {
const disabledFeatures = [...ffs.matchAll(/'([^']+)'\s+0/g)].map(m => m[1]);
if (disabledFeatures.length >= 2) → MEDIUM (SA-CSS-FNV-002)
// Compound: fontFeatureSettings with ≥2 disabled features + negative letterSpacing
const ls = parseFloat(getComputedStyle(el).letterSpacing);
if (disabledFeatures.includes('kern') && ls < -0.03) → HIGH
} */
Individual feature disabling is low-severity; compound disabling is higher: Disabling a single OpenType feature rarely makes text illegible on its own. The risk increases when multiple features are disabled simultaneously (removing ligature disambiguation, contextual alternates, and kerning) especially on fonts designed to rely on these features for legibility. Flag font-feature-settings with two or more disabled features as MEDIUM.
font-feature-settings:'kern' 0 + negative letter-spacing: kerning loss + tracking compound; character pairs overlap on sensitive fonts. Detection: fontFeatureSettings disables kern AND letterSpacing < -0.03em → MEDIUM (HIGH if ≥2 features disabled).SA-CSS-FNV-003: font-variant-numeric:slashed-zero — digit rendering modification
font-variant-numeric:slashed-zero activates the OpenType ‘zero’ feature, which substitutes a slashed zero glyph (ø-like) for the digit 0. In isolation, this is a purely cosmetic change with no consent-hiding impact — the digit is still visible and recognizable as a zero. The consent attack use case is narrow: on a custom font where the zero feature maps to an empty or invisible glyph, consent text containing the digit 0 (e.g., “$0.00 monthly fee” or “0 data sold”) has those digits silently removed. This is extremely low severity compared to other font-variant attacks and is primarily a disclosure-completeness finding.
/* Attack: slashed-zero feature maps to empty glyph in custom font */
@font-face {
font-family: 'ZeroHideFont';
src: url('/fonts/zerohide.woff2');
/* This font maps the 'zero' OpenType feature to an empty glyph,
causing the digit '0' to become invisible when slashed-zero is active. */
}
.consent-text {
font-family: 'ZeroHideFont', sans-serif;
font-variant-numeric: slashed-zero;
/* OpenType 'zero' feature activated.
'0' in consent text → empty glyph from custom font.
"$0.00 data fee" → "$. data fee" → fee appears present but zeroed amount hidden. */
}
/* Also: font-variant-numeric:oldstyle-nums */
.consent-text {
font-variant-numeric: oldstyle-nums;
/* Activates 'onum' feature: digits rendered as old-style (varying baseline).
On fonts with empty onum substitution, all digits disappear. */
}
/* Detection:
const style = getComputedStyle(el);
if (style.fontVariantNumeric !== 'normal') {
// Low priority for standard fonts; flag for custom @font-face
// Canvas test: render '0123456789' at computed styles
// If any digit produces zero ink pixels → HIGH (SA-CSS-FNV-003)
} */
This attack has narrow scope but real consent impact: Hiding the digit ‘0’ in a consent clause like “your data is shared with 0 third parties” or “$0 annual fee” is a targeted semantic manipulation rather than wholesale text hiding. Flag on custom @font-face with non-normal font-variant-numeric when a canvas test shows any digit rendering as empty.
font-variant-numeric:slashed-zero on a custom font with empty zero-feature glyph: digit ‘0’ hidden in numeric consent content. Detection: fontVariantNumeric !== 'normal' on custom @font-face + canvas digit-rendering test → MEDIUM (HIGH if multiple digits affected).SA-CSS-FNV-004: compound font-feature-settings — multiple feature disabling
Individual OpenType feature manipulation is low-severity. However, stacking multiple disabled features can compound to a meaningful legibility degradation. The pattern font-feature-settings: 'liga' 0, 'calt' 0, 'kern' 0, 'case' 0 simultaneously disables: standard ligatures (breaking ‘fi’, ‘ff’, ‘fl’ ligatures into separate non-ligated glyphs), contextual alternates (removing context-sensitive glyph adjustments), kerning (removing pair-space compensation), and case-sensitive forms (removing uppercase-optimised punctuation placement). On fonts designed to require these features for correct spacing and legibility, this compound disabling can push text from readable to difficult-to-read.
/* Attack: compound feature disabling degrades consent text legibility */
.consent-text {
font-feature-settings:
'liga' 0, /* disable standard ligatures */
'calt' 0, /* disable contextual alternates */
'kern' 0, /* disable kerning */
'case' 0, /* disable case-sensitive forms */
'cpsp' 0; /* disable capital spacing */
/* Each feature individually: minor rendering change.
All five together on a heavily feature-dependent font:
- Word-initial capital pairs mis-spaced (kern + case disabled)
- 'fi', 'ff', 'fl' → separate glyphs with gaps (liga disabled)
- Context-sensitive connections removed (calt disabled)
- Combined: significantly degraded legibility on some typefaces.
getComputedStyle.fontFeatureSettings → long string, technically correct.
No individual check triggers. Compound audit required. */
}
/* Compound with font-variant shorthand */
.consent-text {
font-variant: small-caps slashed-zero no-common-ligatures oldstyle-nums;
/* Multiple font-variant longhand values stacked.
Each individually minor; stack can degrade readability. */
}
/* Detection:
const ffs = getComputedStyle(el).fontFeatureSettings;
const disabled = [...(ffs.matchAll(/'([^']+)'\s+0/g))].map(m => m[1]);
if (disabled.length >= 3) → HIGH (SA-CSS-FNV-004)
// 3+ disabled features = compound legibility attack
Also: parse font-variant shorthand for multiple non-normal values
const fv = getComputedStyle(el).fontVariant;
const nonNormal = fv.split(' ').filter(v => v !== 'normal').length;
if (nonNormal >= 3) → MEDIUM */
Three or more disabled OpenType features crosses the compound threshold: Individual feature disabling is cosmetic. Stacking three or more disabled features shifts the pattern from typographic preference to legibility degradation. SkillAudit flags font-feature-settings with three or more zero-disabled tags as HIGH on consent text elements.
font-feature-settings with ≥3 disabled features: compound legibility degradation on feature-dependent fonts. Detection: fontFeatureSettings disables ≥3 OpenType tags → HIGH.Summary of SA-CSS-FNV findings
| ID | Severity | Property | Attack mechanism | Detection |
|---|---|---|---|---|
| SA-CSS-FNV-001 | CRITICAL | font-variant-caps:all-small-caps | Custom font empty smcp glyphs; uppercase consent text invisible | fontVariantCaps != normal + canvas pixel test |
| SA-CSS-FNV-002 | MEDIUM | font-feature-settings:'kern' 0 | Kerning disabled + negative letter-spacing; character pairs overlap | kern disabled + letterSpacing < -0.03em |
| SA-CSS-FNV-003 | MEDIUM | font-variant-numeric:slashed-zero | Custom font empty zero-feature glyph; numeric consent digits hidden | fontVariantNumeric != normal + canvas digit test |
| SA-CSS-FNV-004 | HIGH | compound font-feature-settings ≥3 disabled | Multiple disabled features compound to legibility degradation | fontFeatureSettings disables ≥3 tags |