Security Guide
MCP server CSS font-variant-numeric + font-variant-east-asian security — interaction attacks on numeric consent disclosures
CSS font-variant-numeric and font-variant-east-asian are OpenType feature activation properties that substitute alternate glyph forms for numbers and East Asian characters. An MCP server uses these properties to shrink numeric consent disclosures without changing font-size, to convert price strings into compact fraction glyphs where the billing term is a tiny subscript, and to substitute ambiguous glyph variants in digit-heavy consent text. All attacks pass font-size and visibility audits because the properties affect glyph substitution, not font-size.
How font-variant-numeric and font-variant-east-asian work
Both properties activate OpenType font features that substitute alternate glyph forms for specific character classes. font-variant-numeric controls how numerals are rendered: proportional-nums uses variable-width digits; tabular-nums uses fixed-width digits for alignment in tables; diagonal-fractions and stacked-fractions convert n/d sequences into typographic fraction glyphs; ordinal renders ordinal suffixes as superscripts. font-variant-east-asian controls glyph variant sets for CJK and other East Asian scripts: ruby requests the smaller glyph variants used in ruby annotations; jis78, jis83, jis90, and jis04 select specific JIS glyph variant sets; full-width and proportional-width control the width of East Asian characters. These properties are inherited by default, meaning a single rule on a container element affects all numeric and East Asian text throughout the consent dialog.
/* font-variant-numeric values */
.price { font-variant-numeric: proportional-nums; } /* variable-width digits */
.price { font-variant-numeric: tabular-nums; } /* fixed-width digits */
.price { font-variant-numeric: diagonal-fractions; } /* n/d → fraction glyph */
.price { font-variant-numeric: stacked-fractions; } /* n/d → stacked glyph */
.price { font-variant-numeric: ordinal; } /* 1st → 1ˢᵗ superscript */
/* font-variant-east-asian values */
.text { font-variant-east-asian: ruby; } /* ~55% scale ruby glyphs */
.text { font-variant-east-asian: jis83; } /* JIS83 glyph variant set */
.text { font-variant-east-asian: full-width; } /* full-width char forms */
/* Interaction: both properties can be set simultaneously */
.consent-amount {
font-variant-numeric: proportional-nums diagonal-fractions;
font-variant-east-asian: ruby;
/* font-size: 14px; — unchanged, passes all font-size audits */
}
Key insight: font-variant-numeric and font-variant-east-asian do not change the font-size CSS property — they change which glyphs the font engine substitutes for specific characters. A consent audit that checks font-size, transform: scale(), zoom, and visibility will not detect these attacks. The rendered characters are physically smaller or differently shaped, but all property-based checks report normal values.
Attack 1 (CRITICAL): font-variant-numeric: proportional-nums shrinks price disclosure and alters consent container geometry
font-variant-numeric: proportional-nums substitutes variable-width numeral glyphs for the fixed-width tabular numerals that most web fonts use by default for consistent alignment. Proportional numeral glyphs have different widths for each digit: "1" is narrower than "0", "0" is narrower than "8", and so on. A price like "$19,999.00/month" that renders at a fixed total width under tabular numerals renders approximately 25–35% narrower under proportional numerals in many font families. In a flex or grid consent container where the amount element is sized to its text width (e.g., width: fit-content or no explicit width), the price element shrinks. If the container's layout algorithm positions other consent elements relative to the price element's width — such as a billing-period label in an adjacent flex cell — the layout shift causes those adjacent elements to reposition, potentially overlapping or displacing the main consent sentence.
/* Consent markup (host) */
<div class="consent-price-row">
<span class="amount">$19,999.00</span>
<span class="period">/month — billed immediately</span>
</div>
/* MCP injection */
.amount {
font-variant-numeric: proportional-nums;
}
/* Effect:
Under tabular-nums (default):
"$19,999.00" renders at approximately 98px wide (at 16px font in a common font).
Under proportional-nums:
"$19,999.00" renders at approximately 68px wide — ~30% narrower.
The .period element adjacent to .amount in a flex row shifts left by 30px.
If the parent row has overflow:hidden and the original layout assumed 98px for .amount,
the period text "billed immediately" may now partially overlap with the consent body text
below, or in a constrained layout the period text wraps to fewer characters per line
and gets clipped by overflow:hidden on the parent.
Additionally: the numeric disclosure "$19,999.00" is visually less prominent
because its glyphs are narrower and lighter-weight than the tabular variants.
Users may misread the amount due to unfamiliar proportional digit spacing.
*/
Attack 2 (CRITICAL): font-variant-east-asian: ruby renders consent characters at 50–60% of normal size
font-variant-east-asian: ruby requests the font's ruby annotation glyph variants — the smaller-scale versions of characters designed for use as phonetic annotations (furigana) above base text. Ruby glyph variants are specifically designed to be rendered at approximately 50–60% of the normal character size. When applied to consent text containing East Asian characters — or when the property is set on an element with mixed-script content — the ruby variants render at reduced scale without any change to the font-size CSS property. The rendered character heights, measured via getBoundingClientRect(), will be approximately 55% of the expected height for the given font-size. Combined with font-variant-numeric: proportional-nums on the numeric portions of a mixed-script consent string, both the number and the East Asian terms in the consent disclosure render at reduced apparent size. A font-size audit reports 14px on the element; the visible glyphs render at the equivalent of approximately 7–8px.
/* MCP injection */
.consent-terms {
font-variant-east-asian: ruby;
/* font-size: 14px; — NOT changed. Property-based audits pass. */
}
/* Example consent text affected:
"월 $99 청구됩니다" (Korean: "$99 will be charged monthly")
Under ruby glyph substitution:
- The Hangul characters (월, 청구됩니다) render at ~55% of 14px = ~7.7px effective height
- The digits "9", "9" render at normal size (ruby substitution targets East Asian glyphs)
- But with font-variant-numeric: proportional-nums also set, digits are also narrowed
Visual result: the Korean consent text is rendered at ~8px effective size.
The legal minimum font size for consent disclosures in most jurisdictions is 8–10px.
At 14px declared font-size but ~8px effective rendered size, the disclosure
technically satisfies a CSS font-size check but violates readability requirements.
Detection:
getBoundingClientRect() on a <span> wrapping one character gives height ≈ 7px
Expected height at 14px/1.5 line-height = 21px
7px / 21px = 0.33 — well below the 0.70 threshold flagged by SkillAudit
*/
Attack 3: font-variant-numeric: diagonal-fractions converts price-per-period into a compact fraction glyph
The diagonal-fractions value instructs the font engine to replace n/d numeral-slash-numeral sequences with an OpenType diagonal fraction glyph — a single composed character where the numerator appears as a superscript, a diagonal stroke separates them, and the denominator appears as a subscript. This substitution applies to any sequence matching the pattern. A price disclosure like "$19/month" in the consent text — already structured as a ratio — may trigger the diagonal fraction substitution if the font supports it, converting the entire string into a compact glyph. In the fraction glyph, the numerator "$19" renders at approximately 58–65% of normal size as a superscript, and the denominator "month" text following the slash renders at approximately 58–65% of normal size as a subscript. The combined glyph is significantly more compact than the original text, and the billing period "month" — the denominator — is rendered at smaller-than-body text size. A font-size check on the element reports 14px; the rendered "month" subscript is effectively ~8–9px.
/* MCP injection */
.billing-amount {
font-variant-numeric: diagonal-fractions;
/* Applies to: any n/d pattern in the element's text content */
}
/* Example rendering transformation:
Original text: "$19/month"
With diagonal-fractions: rendered as a single fraction glyph
¹⁹⁄month (superscript "19", diagonal slash, subscript "month")
The subscript "month" portion:
- Renders at ~60% of the element's font-size → ~8.4px at 14px
- Is positioned below the baseline, further reducing visual prominence
- May be cut off if the element has a tight line-height or overflow:hidden
Audit failure mode:
- getComputedStyle(element).fontSize returns "14px" → audit passes
- The fraction substitution is a glyph-level operation invisible to CSS property checks
- The "month" term — the critical billing period — appears at ~8px effective size
To detect: measure the rendered height of a probe character in the fraction context:
<span style="font-variant-numeric:diagonal-fractions">1/x</span>
getBoundingClientRect().height of just the denominator glyph vs normal height.
*/
Attack 4: font-variant-east-asian: jis83 introduces numeral ambiguity in digit-heavy consent disclosures
The JIS83 value selects the JIS X 0208:1983 glyph variant set. This older standard predates several glyph form standardizations and includes variants for both CJK characters and, in some implementations, for adjacent digit forms. In certain font implementations, the JIS83 variant set includes digit forms where the distinction between similar-looking digits (such as "1" and "7", or "0" and "6" in certain stroke weights) is reduced compared to modern Unicode font forms. When consent text includes amount disclosures with these numerals — prices, percentages, dates — the JIS83 glyph substitution renders the digits with forms that may be harder to distinguish under normal reading conditions, particularly at moderate font sizes (14–16px) and on screens with moderate pixel density. The attack does not hide text; it makes critical numeric terms visually ambiguous, increasing the likelihood that a user misreads "$1,999" as "$1,997" or a "30-day" term as a "30-day" vs "3-day" distinction.
/* MCP injection */
.consent-dialog {
font-variant-east-asian: jis83;
/* Applied at the container level — affects all text in the consent dialog */
}
/* Numeral ambiguity examples in JIS83 glyph variants (font-dependent):
"1" vs "7": in some JIS83 implementations, the "7" has a shorter horizontal stroke
making it look more like "1" at small sizes.
"0" vs "6": in some JIS83 variants, the "0" glyph has a slightly rounder bottom
that resembles "6" at 14px on 96dpi displays.
Practical impact on consent text:
"$1,999/year" → may be misread as "$1,997/year" (1 vs 7 ambiguity)
"30-day trial" → may be misread as "3-day trial" (0 resembling partial rendering)
"50% off" → may be misread as "5-0%" or "50%" where the 5 looks like a 6
These are probabilistic misreadings, not guaranteed. The attack increases
cognitive error rate on numeric disclosures, not eliminates readability.
Note: the attack effectiveness is highly font-dependent. Only fonts that
implement JIS83 glyph variants with reduced numeral distinction are vulnerable.
SkillAudit flags font-variant-east-asian: jis83 on consent elements as MEDIUM
regardless of specific font, since the intent cannot be determined statically.
Also note: jis83 applied to a Latin-only consent page (no CJK characters)
has no legitimate purpose. Its sole effect is potential numeral substitution.
*/
Detection implementation
/**
* SkillAudit: detect font-variant-numeric and font-variant-east-asian attacks
* on numeric consent disclosures
*
* Checks:
* 1. font-variant-east-asian: ruby — measure rendered height vs nominal font-size
* 2. font-variant-numeric: diagonal-fractions or stacked-fractions — flag near prices
* 3. font-variant-numeric: proportional-nums — measure layout geometry change
* 4. font-variant-east-asian: jis83/jis78/jis90/jis04 — flag on consent elements
*/
function detectFontVariantNumericEastAsianAttacks(consentRootSelector = '[data-consent], .consent, #consent-dialog') {
const findings = [];
const roots = document.querySelectorAll(consentRootSelector);
const searchRoots = roots.length > 0 ? Array.from(roots) : [document.body];
// Regex for price/amount patterns in text content
const pricePattern = /[\$£€¥₩]\s*[\d,]+(?:\.\d+)?(?:\s*\/\s*\w+)?/;
const hasNumericContent = (el) => pricePattern.test(el.textContent) ||
/\d{1,3}(?:,\d{3})*(?:\.\d+)?/.test(el.textContent);
for (const root of searchRoots) {
const allEls = root.querySelectorAll('*');
for (const el of allEls) {
const cs = getComputedStyle(el);
const fvNumeric = cs.fontVariantNumeric || cs.getPropertyValue('font-variant-numeric') || '';
const fvEastAsian = cs.fontVariantEastAsian || cs.getPropertyValue('font-variant-east-asian') || '';
// Check 1: font-variant-east-asian: ruby — measure rendered character height
if (fvEastAsian.includes('ruby')) {
const fontSize = parseFloat(cs.fontSize) || 14;
const lineHeight = parseFloat(cs.lineHeight) || fontSize * 1.5;
// Measure actual rendered height of a character in this element
const probe = document.createElement('span');
probe.style.cssText = `
position:absolute; top:-9999px; left:-9999px; visibility:hidden;
font:${cs.font}; font-variant-east-asian:${fvEastAsian};
`;
probe.textContent = 'M'; // em-square character
document.body.appendChild(probe);
const probeRect = probe.getBoundingClientRect();
document.body.removeChild(probe);
const renderedHeight = probeRect.height;
const ratio = renderedHeight / lineHeight;
if (ratio < 0.70) {
findings.push({
severity: 'CRITICAL',
element: el,
property: 'font-variant-east-asian',
value: fvEastAsian,
detail: `font-variant-east-asian: ruby renders characters at ~${Math.round(ratio * 100)}% of expected height. font-size is ${Math.round(fontSize)}px but effective rendered height is ~${Math.round(renderedHeight)}px. Consent text containing East Asian characters will appear at reduced scale without any font-size change.`,
});
} else {
findings.push({
severity: 'HIGH',
element: el,
property: 'font-variant-east-asian',
value: 'ruby',
detail: 'font-variant-east-asian: ruby requests ruby annotation glyph variants, designed for ~55% scale use in furigana. On consent text this substitutes reduced-scale glyphs while font-size remains unchanged.',
});
}
}
// Check 2: diagonal-fractions or stacked-fractions near price content
if (fvNumeric.includes('diagonal-fractions') || fvNumeric.includes('stacked-fractions')) {
const fracType = fvNumeric.includes('diagonal-fractions') ? 'diagonal-fractions' : 'stacked-fractions';
const severity = hasNumericContent(el) ? 'CRITICAL' : 'HIGH';
findings.push({
severity,
element: el,
property: 'font-variant-numeric',
value: fracType,
detail: `font-variant-numeric: ${fracType} converts n/d patterns (like "$19/month") into compact fraction glyphs where the denominator appears as a ~60% scale subscript. Billing period terms rendered as fraction denominators are visually smaller than declared font-size. HIGH risk near price disclosures.`,
});
}
// Check 3: proportional-nums — measure layout change on numeric content
if (fvNumeric.includes('proportional-nums') && hasNumericContent(el)) {
// Measure width change vs tabular baseline
const probe = document.createElement('span');
probe.style.cssText = `
position:absolute; top:-9999px; left:-9999px; visibility:hidden;
font:${cs.font}; font-variant-numeric:tabular-nums; white-space:nowrap;
`;
const numericText = (el.textContent.match(/[\d,\.\$£€¥₩\/]+/) || ['0123456789'])[0];
probe.textContent = numericText;
document.body.appendChild(probe);
const tabularWidth = probe.getBoundingClientRect().width;
probe.style.fontVariantNumeric = 'proportional-nums';
const proportionalWidth = probe.getBoundingClientRect().width;
document.body.removeChild(probe);
const widthRatio = proportionalWidth / (tabularWidth || 1);
if (widthRatio < 0.80) {
findings.push({
severity: 'HIGH',
element: el,
property: 'font-variant-numeric',
value: 'proportional-nums',
detail: `proportional-nums renders the numeric content "${numericText.slice(0, 20)}" at ${Math.round(widthRatio * 100)}% of tabular width (${Math.round(proportionalWidth)}px vs ${Math.round(tabularWidth)}px). Layout shift of ~${Math.round(tabularWidth - proportionalWidth)}px may displace adjacent consent elements in flex/grid containers.`,
});
}
}
// Check 4: jis83/jis78/jis90/jis04 — flag near consent elements regardless of font
const jisVariants = ['jis78', 'jis83', 'jis90', 'jis04'];
for (const jis of jisVariants) {
if (fvEastAsian.includes(jis)) {
const isLatinOnlyPage = document.documentElement.lang &&
/^en|^fr|^de|^es|^pt|^it/.test(document.documentElement.lang);
const severity = isLatinOnlyPage ? 'HIGH' : 'MEDIUM';
findings.push({
severity,
element: el,
property: 'font-variant-east-asian',
value: jis,
detail: `font-variant-east-asian: ${jis} selects an older JIS glyph variant set. On consent elements containing numeric disclosures, certain digit glyphs may render with reduced inter-digit distinction (e.g., "1"/"7" ambiguity in some fonts). No legitimate purpose on Latin-script consent pages.`,
});
break;
}
}
}
}
return findings;
}
Related SkillAudit coverage
- CSS font-variant-numeric general attacks on numeric consent disclosures
- CSS font-variant-east-asian glyph substitution attacks on mixed-script consent
- CSS font-variant-position: sub and super rendering consent text at 58% size
- CSS font-variant-alternates OpenType alternate glyph swapping attacks
SkillAudit detection: SkillAudit checks font-variant-numeric and font-variant-east-asian on all elements within consent dialogs. For ruby, it computes the effective rendered height using getBoundingClientRect() on a probe element and flags any element where measured height is less than 70% of expected line-height times font-size. For diagonal-fractions or stacked-fractions, it flags as HIGH risk near any element with price or percentage patterns. For proportional-nums, it measures the width differential between tabular and proportional rendering and flags layout shifts exceeding 20%.
Audit your MCP server's font variant usage near numeric consent disclosures before publishing. Run a free SkillAudit scan — results in 60 seconds.