Security reference · CSS injection · Font variants · Small caps · Consent hiding
MCP server CSS font-variant consent security
CSS font-variant properties change which glyph forms are rendered — small capitals, oldstyle numerals, unicase mixtures, and alternate stylistic sets — without changing the nominal font-size. WCAG and most accessibility checks examine font-size as the legibility proxy; font-variant-caps: all-small-caps renders glyphs at approximately 70% of the nominal font-size while leaving that property unchanged. Four attack patterns: all-small-caps glyph size collapse below threshold; oldstyle-nums descenders clipped by fixed-height containers; unicase mixed letterform confusion; and small-caps combined with already-small font-size for compounded sub-threshold rendering.
font-variant properties and rendered glyph height
| font-variant value | Affects | Rendered glyph height vs. font-size | Attack vector |
|---|---|---|---|
font-variant-caps: small-caps | Lowercase letters → small capitals | ~70% of font-size for lowercase glyphs | Sub-threshold rendering for lowercase-heavy consent |
font-variant-caps: all-small-caps | All letters → small capitals | ~70% of font-size for all glyphs | All consent text rendered at sub-threshold glyph size |
font-variant-caps: unicase | Mixed upper/lower forms | Variable — font-dependent | Unpredictable letterform mixtures reduce keyword recognition |
font-variant-numeric: oldstyle-nums | Digit glyphs → oldstyle forms | Descenders extend 20-30% below baseline | Descenders clipped in fixed-height line boxes |
font-variant affects rendered glyph dimensions, not font-size: getComputedStyle(el).fontSize returns the nominal font-size — e.g., 16px. With font-variant-caps: all-small-caps, the rendered capital glyph height is approximately 11px (70% of 16px). WCAG 1.4.4 Resize Text and minimum-size guidelines reference the CSS font-size value, not the rendered glyph height. A consent element styled with all-small-caps at 12px nominal size would report 12px font-size (passing a 9px minimum threshold) while rendering at approximately 8.4px glyph height — below threshold.
Attack 1: font-variant-caps: all-small-caps — rendered glyph size below threshold
all-small-caps renders uppercase and lowercase letters using the font's small-capitals glyphs. These glyphs have cap-heights of approximately 70% of the full capital-letter height. For 16px text, the consent glyphs render at approximately 11px cap-height. For 12px text (a common secondary-text size), small-caps glyphs render at approximately 8px — below the practical legibility threshold. Standard audits check font-size: 12px and pass; the rendered glyph is at 8px:
/* Malicious CSS — SA-CSS-FV-001 */
.mcp-consent-text {
font-size: 12px; /* Reported font-size — passes a 9px minimum check */
font-variant-caps: all-small-caps;
/* Actual rendered cap-height: ~12px × 0.70 = 8.4px — below threshold */
/* All text — including uppercase consent keywords like "GRANT", "ALLOW",
"INSTALL" — is rendered in small-capitals glyphs at ~8px cap-height */
}
/* Why 70%? The CSS spec for font-variant-caps does not define a size ratio.
The font designer specifies the small-cap glyph size. For most system fonts:
- Helvetica Neue: small-caps at ~68-72% of full caps
- Georgia: small-caps at ~65-70% of full caps
- System-ui: varies by OS, typically 68-75%
All produce rendered text smaller than what font-size reports. */
/* Detection: */
function detectSmallCapsConsent() {
const findings = [];
const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
for (const el of document.querySelectorAll('*')) {
if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
const fvc = getComputedStyle(el).fontVariantCaps;
if (fvc && !['normal', 'initial', 'inherit', 'unset'].includes(fvc)) {
const fs = parseFloat(getComputedStyle(el).fontSize);
const effectiveSize = fs * 0.70; /* conservative estimate */
findings.push({ id: 'SA-CSS-FV-001', severity: 'high',
message: `Consent element has font-variant-caps: "${fvc}". Nominal font-size: ${fs}px. Estimated rendered glyph height: ~${effectiveSize.toFixed(1)}px (70% of nominal). WCAG checks font-size, not rendered glyph height. Check if effective size falls below minimum legibility threshold.` });
}
}
return findings;
}
Attack 2: font-variant-numeric: oldstyle-nums — descender clipping in fixed-height containers
Oldstyle numerals (also called text figures) extend above and below the baseline — digits 0, 1, 2 have ascenders or regular height, while 3, 4, 5, 6, 7, 8, 9 have descenders that fall 20-30% below the baseline. In a consent container with overflow: hidden and a height calculated from line-height × number-of-lines, the descenders of oldstyle numerals are clipped. Permission scope numbers, version codes, and rate limits embedded in consent text are partially hidden:
/* Malicious CSS — SA-CSS-FV-002 */
.mcp-permission-list {
font-variant-numeric: oldstyle-nums;
/* Oldstyle numeral descenders: 3,4,5,6,7,8,9 descend below baseline */
/* Descender depth: typically 15-30% of font-size below baseline */
/* Container height precisely matches line-height × lines */
overflow: hidden;
height: calc(1.4em * 3); /* exactly 3 lines at 1.4 line-height */
/* Descenders of the bottom line's numerals are clipped */
}
/* Example consent text:
"This MCP server will read up to 50,000 messages per day"
"and can write to 3 folders in your file system"
"at a rate of 9 operations per second"
↑ The "9" in "9 operations" is the last line — its descender clips.
The numeral is still readable but partially obscured.
More damaging: "read 50,000 messages" — the "9" descender clips too. */
/* A more aggressive variant: small font-size + oldstyle-nums + overflow:hidden */
.mcp-rate-limit-note {
font-size: 11px;
font-variant-numeric: oldstyle-nums;
overflow: hidden;
/* At 11px, descenders may be clipped to the point where 5,6,9 are
ambiguous — "read 56 files per second" vs "read 58 files per second" */
}
Attack 3: font-variant-caps: unicase — mixed letterform confusion
unicase produces a mixture of uppercase and lowercase glyph forms in an unpredictable, font-dependent pattern. For consent keywords that users rely on pattern-recognition to identify ("GRANT", "ALLOW", "INSTALL"), unicase rendering produces an unfamiliar mixed-case presentation that slows reading and recognition. Unlike all-caps or lowercase which are predictable, unicase output varies by font:
/* Malicious CSS — SA-CSS-FV-003 */
.mcp-consent-header {
font-variant-caps: unicase;
/* Unicase: produces mixed uppercase and lowercase forms from the font's
unicase feature table. The exact output is font-defined.
For many web fonts, unicase produces a visually noisy mixture
that impairs rapid keyword recognition. */
}
/* Example: "GRANT READ ACCESS TO YOUR FILES"
With unicase in some fonts renders similar to:
"GRaNt ReaD aCCeSs TO YoUr FiLeS"
The brain's pattern-recognition for known consent keywords is disrupted.
The text is technically readable but requires more cognitive effort. */
/* Why this matters for security:
Users of MCP install dialogs often skim consent text quickly.
Unicase rendering forces slow serial letter-by-letter reading
instead of word-shape recognition, increasing the chance of
misreading scope keywords like "ALL", "FULL", "WRITE". */
/* Detection: */
function detectUnicaseConsent() {
const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
const findings = [];
for (const el of document.querySelectorAll('*')) {
if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
const fvc = getComputedStyle(el).fontVariantCaps;
if (fvc === 'unicase' || fvc === 'petite-caps' || fvc === 'all-petite-caps') {
findings.push({ id: 'SA-CSS-FV-003', severity: 'medium',
message: `Consent element has font-variant-caps: "${fvc}". This produces non-standard mixed or reduced glyph forms that may impair recognition of consent keywords. font-size reports the nominal value — rendered glyph forms differ.` });
}
}
return findings;
}
Attack 4: font-variant: small-caps combined with small font-size — compounded sub-threshold rendering
When font-variant: small-caps (shorthand, affecting lowercase letters) is combined with a font-size that is already near the minimum legibility threshold, the compound effect pushes lowercase glyph rendering well below threshold. An attacker sets font-size: 10px (above a 9px minimum) and font-variant: small-caps. The uppercase letters render at 10px; the lowercase letters render at approximately 7px — clearly below threshold but reported as 10px font-size:
/* Malicious CSS — SA-CSS-FV-004 */
.mcp-consent-fine-print {
font-size: 10px; /* passes a 9px minimum threshold check */
font-variant: small-caps; /* shorthand → font-variant-caps: small-caps */
/* Lowercase letter glyph height: ~10px × 0.70 = 7px
WCAG: checks font-size (10px — passes)
Rendered lowercase: 7px — fails WCAG 1.4.4 readability in practice */
}
/* Compound attack chain:
- font-size: 10px → passes ≥9px minimum check
- small-caps → lowercase at ~7px
- color: #555 → 5.5:1 contrast on white (barely passes AA for large text,
does NOT meet AA 4.5:1 for normal text at 7px rendered)
- line-height: 1.1 → lines overlap at this glyph size
Each factor individually might be flagged; combined they create a borderline
that defeats automated checks while remaining technically "valid". */
/* Detection: check getComputedStyle().fontVariant + fontSize together */
function detectCompoundSmallCaps() {
const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
const findings = [];
for (const el of document.querySelectorAll('*')) {
if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
const s = getComputedStyle(el);
const fvc = s.fontVariantCaps;
if (!fvc || fvc === 'normal') continue;
const fs = parseFloat(s.fontSize);
/* small-caps affects lowercase — effective size for lowercase glyphs */
if (fvc === 'small-caps' && fs < 14) {
/* 14px × 0.70 = 9.8px — effective lowercase near threshold */
findings.push({ id: 'SA-CSS-FV-004', severity: 'medium',
message: `Consent element has font-variant-caps: "small-caps" with font-size: ${fs}px. Estimated lowercase rendered glyph height: ~${(fs * 0.70).toFixed(1)}px. Combined effect may push rendered size below minimum legibility threshold even though font-size passes.` });
}
}
return findings;
}
font-variant attacks evade font-size minimum checks: WCAG success criterion 1.4.4 defines text resize requirements in terms of CSS font-size. Browser accessibility audits and most automated tools check getComputedStyle(el).fontSize. font-variant-caps renders glyphs at a fraction of the nominal font-size without changing that property. A consent element at font-size: 12px; font-variant-caps: all-small-caps passes a 10px minimum check while rendering at ~8.4px glyph height. Accurate detection requires checking both fontVariantCaps and applying the approximately 0.70 multiplier to estimate effective rendered glyph size.
SkillAudit findings for CSS font-variant consent attacks
font-variant-caps: all-small-caps. All glyphs (uppercase and lowercase) render at approximately 70% of the nominal font-size. If the effective glyph height falls below the minimum legibility threshold after applying the 70% factor, this constitutes a font-size evasion attack. getComputedStyle(el).fontSize reports the nominal value and will not trigger font-size minimum checks.font-variant-numeric: oldstyle-nums with overflow: hidden on a fixed-height ancestor. Oldstyle numerals have descenders extending 15-30% below the baseline. In a container sized to the line-height without accounting for descenders, numeral descenders are clipped — numbers in consent text (scope counts, rate limits, version identifiers) may be partially illegible.font-variant-caps: unicase or petite-caps. These values produce non-standard, font-dependent mixed glyph forms that impair rapid pattern-recognition of consent keywords. Unicase output is unpredictable across fonts — the exact letterform mixture is not specified by CSS and varies per typeface.font-variant: small-caps combined with a small font-size (below 14px). The compound effect renders lowercase glyphs at approximately 70% of an already-small nominal size, falling below the practical legibility threshold. The nominal font-size passes minimum threshold checks; the effective rendered size does not.Related MCP consent attack research
- CSS font-size clamp() attacks — clamped sub-threshold font sizes
- CSS text-overflow attacks — ellipsis and clip truncation of consent text
- CSS line-height attacks — zero and extreme positive line-height consent collapse
- CSS writing-mode attacks — vertical text rendering that defeats horizontal scanners
- CSS filter effects as a consent bypass vector
SkillAudit's consent audit checks fontVariantCaps, fontVariantNumeric, and fontVariantAlternates on consent elements and applies the appropriate glyph-size multiplier to estimate effective rendered glyph height — catching font-variant attacks that evade standard font-size minimum checks. Paste your MCP server URL at skillaudit.dev to scan for SA-CSS-FV findings.