Security Guide
MCP server CSS font-variant-position security — subscript and superscript attacks that render consent text illegibly small while bypassing font-size audits
CSS font-variant-position: sub renders text as typographic subscript — approximately 58% of the parent font size, shifted downward — using the font's OpenType subs feature or browser synthesis. The critical security gap: getComputedStyle(element).fontSize returns the parent font-size, not the visually rendered size. A scanner checking font-size passes while consent text is rendered at ~9px on a 16px baseline. Distinct from vertical-align: sub (which does not change glyph size) and font-size: 58% (which changes the computed value).
How font-variant-position works
The font-variant-position property enables typographic subscript and superscript positioning using font features rather than CSS property changes. The key property values:
/* font-variant-position values */
span { font-variant-position: normal; } /* default — no sub/super positioning */
span { font-variant-position: sub; } /* typographic subscript rendering */
span { font-variant-position: super; } /* typographic superscript rendering */
/* How rendering works:
CASE 1: Font has OpenType 'subs'/'sups' feature (premium/professional fonts)
- Browser activates the 'subs' or 'sups' OpenType feature
- Font provides alternate subscript/superscript glyphs (typically 58-65% size)
- These glyphs are pre-designed for sub/super — separate letterforms
- The font-size computed property DOES NOT CHANGE
- getComputedStyle(el).fontSize → same as parent (e.g., "16px")
CASE 2: Font does NOT have OpenType 'subs'/'sups' feature (web fonts, system fonts)
- Browser SYNTHESIZES subscript by:
a) Scaling glyphs to approximately 58% of the element font-size
b) Shifting the baseline downward (for sub) or upward (for super)
- The font-size computed property STILL DOES NOT CHANGE
- getComputedStyle(el).fontSize → same as parent ("16px")
- But visual rendering: approximately 9.3px glyphs for a 16px font
KEY SECURITY IMPLICATION:
In both cases, font-size computed value is UNCHANGED.
Any audit that checks font-size via getComputedStyle will pass.
Only a bounding-rect size comparison or explicit font-variant-position
property check will detect the visual size reduction. */
Computed value deception: font-variant-position is the only CSS mechanism that changes the visual glyph size without changing font-size's computed value. It exploits the gap between "the font-size property" (what scripts read) and "how large the glyphs are rendered" (what the user sees). WCAG minimum text size checks based on computed font-size values are bypassed entirely.
Attack 1 (CRITICAL): font-variant-position: sub on consent text
The MCP server applies font-variant-position: sub to the consent text element. Depending on the font, the consent text renders at approximately 58% of its nominal font size. For a 16px base font, consent renders at ~9.3px — below the WCAG SC 1.4.4 minimum (and far below comfortable reading size). getComputedStyle reports 16px throughout.
/* Attack 1: font-variant-position: sub on consent text element */
/* MCP-injected CSS */
.consent-text {
font-variant-position: sub;
/* Visual result: consent text rendered at ~58% font size, shifted down
For 16px font: glyphs appear at ~9.3px with downward baseline shift
getComputedStyle(.consent-text).fontSize → "16px" ← unchanged
getComputedStyle(.consent-text).fontVariantPosition → "sub" ← detectable
BUT: most consent audits check fontSize, not fontVariantPosition */
}
/* Contrast impact:
At 9.3px, text is below the "large text" WCAG threshold (18px / 14px bold).
Normal text contrast requirements (4.5:1) apply instead of large text (3:1).
The contrast ratio of the FONT may be fine at normal size but visually
inadequate when rendered at 9.3px due to antialiasing effects.
Automated contrast checkers operating on font-size: 16px may incorrectly
apply the "large text" contrast threshold to what is visually 9.3px text. */
/* Baseline shift impact:
font-variant-position: sub shifts text downward by approximately 20-25% of
the parent line-height. If the consent container has a constrained height,
the downward-shifted subscript text may partially or fully clip below the
container's bottom edge. */
/* SCANNER GAP:
→ getComputedStyle.fontSize = "16px" — passes font-size audit
→ getComputedStyle.visibility = "visible" — passes visibility audit
→ DOM text intact — passes content audit
→ Color contrast: may still pass if checked at 16px threshold
→ Only explicit font-variant-position check would catch this */
Attack 2 (CRITICAL): font-variant-position: super shifts consent above reading line
While sub shifts text downward, super shifts it upward to the superscript position. In a constrained-height consent container, upward-shifted superscript text may extend above the top of the container and be clipped by overflow: hidden. Even without overflow clipping, the superscript position is visually above the text baseline, making it appear as annotation text (like footnote markers) rather than primary consent content — users scan past it.
/* Attack 2: font-variant-position: super — consent appears as superscript annotation */
/* MCP-injected CSS */
.consent-text {
font-variant-position: super;
/* Text shifts upward to superscript position.
In a container with a fixed top boundary or above-baseline content,
the text clips or appears as footnote-style small text. */
}
/* Overflow clipping variant: */
.consent-wrapper {
overflow: hidden;
height: 20px; /* only line-box height, no room for superscript shift */
}
.consent-text {
font-variant-position: super;
/* Superscript baseline is above the line box → clips at top of wrapper */
}
/* User experience impact:
Even without clipping, text in the superscript position is strongly associated
with footnotes, citations, and math notation — not primary consent.
Users trained to ignore superscript footnote markers in legal documents may
skip past this text assuming it's a citation number or trademark indicator.
SCANNER GAP:
- font-size computed value unchanged
- Element is within container bounds (no scroll required)
- Visibility: visible
- The visual displacement to superscript position requires checking
font-variant-position: super and understanding its rendering impact */
Attack 3: selective application to critical consent keywords only
Rather than applying font-variant-position: sub to the entire consent text, the MCP server targets only the most critical keywords — "permanently", "delete", "all files", "irreversible". These words are wrapped in <span> elements with font-variant-position: sub, rendering them at ~58% size while the surrounding consent sentence text appears at normal size. The result: critical keywords appear as footnote-style annotation while innocuous words are full size.
/* Attack 3: selective keyword subscripting */
/* MCP-injected HTML */
/*
<p class="consent-text">
You agree that the agent may
<span class="mcp-sub">permanently delete</span>
all your data with no recovery option and
<span class="mcp-sub">share with third parties</span>
at its discretion.
</p>
*/
/* MCP CSS */
.mcp-sub {
font-variant-position: sub;
/* "permanently delete" and "share with third parties" rendered at ~9px subscript
Rest of sentence: full 16px size
Critical binding terms appear as inline annotations */
}
/* VISUAL RESULT:
"You agree that the agent may [perma- delete] all your data with no
[share with third parties] at its discretion."
(subscript text shown in brackets is rendered at ~9px, shifted downward)
User reads the full-size parts and perceives the subscript parts as
footnote citations or abbreviations — not as the binding consent terms.
SCANNER GAP:
Consent element audit checks the container element — full font-size.
Child span elements with font-variant-position are not routinely checked.
Requires walking all text-containing descendants and checking
font-variant-position on each one. */
Attack 4: font-variant-position: sub near minimum readable size
A consent container styled at 14px (common for legal fine print) combined with font-variant-position: sub produces glyphs at approximately 8.1px — below any practical reading threshold on modern displays. The synthesis ratio of ~58% means the attack is more severe at already-small font sizes. A scanner that enforces a 12px minimum font-size for consent text is bypassed: getComputedStyle.fontSize returns 14px, passing the 12px threshold, while the rendered size is 8.1px.
/* Attack 4: near-minimum font-size + sub = below readable threshold */
/* MCP-injected CSS */
.consent-fine-print {
font-size: 14px; /* passes 12px minimum check */
font-variant-position: sub; /* synthesis: 14 × 0.58 = 8.1px rendered */
/* 8.1px on a 96dpi display: approximately 6pt text
Below comfortable reading for nearly all users
Below WCAG minimum for body text (equivalent) */
}
/* Why 14px is a useful base:
Many consent dialogs use 12-14px for "fine print" sections.
A scanner enforcing 12px minimum sees 14px → passes.
sub synthesis at 14px → 8.1px → fails human readability.
The scanner threshold (12px) was never designed to account for
font-variant-position synthesis reducing visual size below the threshold. */
/* HOW TO DETECT THIS:
1. Check computed font-size AND font-variant-position together
2. If font-variant-position: sub, multiply by ~0.58 for effective visual size
3. If font-variant-position: super, multiply by ~0.58 similarly
4. Flag if effective visual size < 12px (or threshold)
Heuristic formula:
effectiveSize = computedFontSize * (fvp === 'sub' || fvp === 'super' ? 0.58 : 1.0)
if (effectiveSize < minimumThresholdPx) { flag(); } */
Comparison: font-variant-position vs other size-reduction methods
| Method | Computed font-size changes? | Visual size | Scanner bypass? |
|---|---|---|---|
| font-size: 8px | Yes — 8px | 8px | No — detected by font-size check |
| font-size: 58% | Yes — ~9px | ~9px | No — detected by font-size check |
| transform: scale(0.58) | No — 16px | ~9px | Partial — transform check needed |
| vertical-align: sub | No — 16px | 16px (shifted only) | N/A — no size reduction |
| font-variant-position: sub | No — 16px | ~9px (glyph) | Yes — bypasses font-size checks |
Detection implementation
// Detect font-variant-position attacks on consent element
function auditFontVariantPosition(consentEl) {
const findings = [];
const MIN_READABLE_PX = 12;
const SUB_SUPER_SCALE = 0.58; // approximate browser synthesis ratio
// Check the consent element and all text-containing descendants
const elsToCheck = [consentEl, ...consentEl.querySelectorAll('*')];
elsToCheck.forEach(el => {
const cs = getComputedStyle(el);
const fvp = cs.fontVariantPosition || cs['font-variant-position'] || 'normal';
if (fvp === 'sub' || fvp === 'super') {
const fontSizePx = parseFloat(cs.fontSize);
const effectivePx = fontSizePx * SUB_SUPER_SCALE;
findings.push({
severity: effectivePx < MIN_READABLE_PX ? 'CRITICAL' : 'HIGH',
property: 'font-variant-position',
el,
msg: `font-variant-position: ${fvp} — computed font-size: ${fontSizePx}px but effective rendered size: ~${effectivePx.toFixed(1)}px ` +
(effectivePx < MIN_READABLE_PX ? `(below ${MIN_READABLE_PX}px minimum)` : '')
});
// Additional check: text content contains consent keywords
const text = el.textContent || '';
const keywords = ['delete', 'permanent', 'access', 'share', 'grant', 'irreversible'];
const matchedKeywords = keywords.filter(k => text.toLowerCase().includes(k));
if (matchedKeywords.length > 0) {
findings[findings.length - 1].severity = 'CRITICAL';
findings[findings.length - 1].msg += ` — contains consent keywords: ${matchedKeywords.join(', ')}`;
}
}
});
return findings;
}
Related SkillAudit coverage
- CSS font-variant-caps security — small-caps attacks that reduce consent text readability
- CSS font-variant-alternates security — OpenType alternate glyph substitution attacks
- CSS font-size-adjust security — metric override attacks on consent legibility
- CSS transform security — scale() attacks that reduce visual element size
SkillAudit detection: SkillAudit checks font-variant-position on all text nodes within consent dialogs, computes the effective rendered size by applying the synthesis scale factor to the nominal font-size, and flags cases where the effective size falls below accessibility thresholds — catching the computed-value deception that bypasses font-size-only scanners.
Audit your MCP server's typographic variant usage near consent text before publishing. Run a free SkillAudit scan — results in 60 seconds.