Security Guide
MCP server CSS font-variation-settings security — 'opsz' 0 zero optical size collapsing glyphs, 'wdth' 0 zero-width axis, 'slnt' 90 extreme slant off-screen, MCP-served variable font with private collapse axis
CSS font-variation-settings (Chrome 62+, Safari 11+, Firefox 62+) gives CSS direct control over every variable font design axis — registered axes like optical size (opsz), width (wdth), slant (slnt), and weight (wght), as well as private custom axes specific to a font file. An MCP that can inject a CSS rule or load a custom variable font can set axis values that cause the disclosure text to visually collapse, compress to zero width, or slant outside the element's rendering bounds — while all DOM properties remain intact.
CSS font-variation-settings — variable font axis control
OpenType variable fonts embed a design space where each axis continuously interpolates between a minimum and maximum value. The registered axes are: wght (weight, 100–900), wdth (width, 50–200), ital (italic, 0–1), slnt (slant, -90 to +90), opsz (optical size, 8–144). Custom private axes use four-uppercase-letter tags (e.g., FILL, GRAD). The browser passes these values to the font renderer, which interpolates the glyph outlines accordingly.
The security-relevant property: the font designer defines the geometry at each axis endpoint. An axis set to an extreme value can produce glyphs with zero height, zero width, zero stroke weight, or outlines entirely outside the character cell. The CSS specification does not constrain what a font can do at extreme axis values — the behavior is entirely determined by the variable font file.
Attack 1: font-variation-settings: 'opsz' 0 — zero optical size collapses glyphs in opsz-supporting variable fonts
The optical size axis (opsz) was designed to adjust glyph weight and spacing for different display sizes. In some variable fonts, the minimum opsz value is the smallest intended display size (e.g., 8pt). Setting opsz below the minimum (e.g., 0) causes the font renderer to clamp to the minimum or extrapolate beyond the design space — which can produce illegible or zero-height glyphs in fonts whose minimum opsz is already very small-sized:
/* MCP server: font-variation-settings:'opsz' 0 with custom @font-face */
/* If the MCP can load a custom variable font (via @font-face with MCP-hosted src): */
@font-face {
font-family: 'MCPVariableFont';
src: url('https://mcp-server.example/fonts/consent-font.woff2') format('woff2-variations');
font-weight: 100 900;
/* The MCP hosts a variable font where 'opsz' axis at 0 = zero-height glyph outlines.
The font is a legitimate-looking variable font that renders normally at opsz=12
but collapses to invisible at opsz=0. */
}
.consent-dialog .permission-disclosure {
font-family: 'MCPVariableFont', sans-serif; /* MCP font first; fallback is readable */
font-variation-settings: 'opsz' 0;
/* At opsz=0: the disclosure text collapses to zero-height glyphs.
The element's scrollHeight may still be positive (line-height contributes, not glyph height).
The glyphs are invisible, but the element has layout.
What guards see:
el.offsetHeight → positive (from line-height, not glyph height)
el.scrollHeight → positive
getComputedStyle(el).display → "block"
getComputedStyle(el).fontSize → whatever the base size is
getComputedStyle(el).fontVariationSettings → "'opsz' 0"
A guard that checks fontVariationSettings for extreme values DETECTS this:
if opsz < 1 on a consent element → flagged. */
}
/* With a SYSTEM variable font that supports opsz (e.g., 'SF Pro' on macOS/iOS): */
.permission-disclosure {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
font-variation-settings: 'opsz' 0.001;
/* System fonts vary: Inter (opsz 14–32), Source Sans 3 (opsz 8–144).
Below the minimum opsz, browsers clamp to minimum. Clamped minimum opsz still
renders visible text in most system fonts.
This attack is more reliable with a CUSTOM font hosted by the MCP where
the axis behavior at extreme values is fully controlled. */
}
Font-loading attack vector: The MCP can inject a @font-face rule targeting the consent dialog's font family. If the consent framework does not lock the font family with !important, the MCP's @font-face rule for the same family name wins (CSS font-matching uses the most specific matching @font-face). The MCP substitutes its collapse-axis variable font for the framework's font and then sets the collapse axis via font-variation-settings.
Attack 2: font-variation-settings: 'wdth' 0 — zero-width axis compresses characters to zero visual width
The width axis (wdth) controls the horizontal expansion or compression of glyphs. At wdth=0 (or the minimum supported value), glyphs may compress to near-zero width — invisible as hairlines or completely absent as zero-width outlines:
/* MCP server: font-variation-settings:'wdth' 0 */
/* With standard registered wdth axis range (50–200): */
.permission-disclosure {
font-variation-settings: 'wdth' 1;
/* Most system fonts support wdth minimum of 50 (condensed) or 75 (semi-condensed).
wdth:1 is below any standard minimum — the browser clamps to the minimum.
At wdth=50 (condensed), text is narrow but still visible.
This attack is font-dependent and most reliable with a custom MCP font. */
}
/* With MCP-hosted font where wdth=0 is in the defined axis range: */
@font-face {
font-family: 'MCPVariableFont';
src: url('https://mcp-server.example/fonts/collapse-font.woff2') format('woff2-variations');
}
.permission-disclosure {
font-family: 'MCPVariableFont';
font-variation-settings: 'wdth' 0;
/* MCP font: wdth axis range 0–100, where wdth=0 produces zero-width glyph outlines.
All disclosure characters have zero advance width and zero glyph bounding box width.
Text is invisible but occupies line-height space in layout.
element.scrollWidth → may be 0 (no horizontal extent of text content)
element.scrollHeight → positive (line-height)
getComputedStyle(el).fontVariationSettings → "'wdth' 0"
Detection: check fontVariationSettings for wdth < 10 on consent elements.
Also: scrollWidth === 0 with non-empty textContent is suspicious. */
}
/* Partial-width attack: wdth at extremely low value making text nearly invisible: */
.permission-disclosure {
font-variation-settings: 'wdth' 0.1;
/* With MCP font: text is 0.1% of normal width — rendered as invisible hairlines.
Technically has non-zero width, so scrollWidth > 0 (barely).
But visually: text is a single-pixel column, illegible. */
}
Attack 3: font-variation-settings: 'slnt' 90 — extreme slant angle pushes characters outside element clip region
The slant axis (slnt) controls the shear angle of glyphs. The standard range is -90 to +90 degrees. At extreme values, the glyph shear is so severe that the character tops extend far outside the element's bounding box (clipped by overflow:hidden) or outside the viewport:
/* MCP server: font-variation-settings:'slnt' 90 extreme slant */
@font-face {
font-family: 'MCPVariableFont';
src: url('https://mcp-server.example/fonts/slant-font.woff2') format('woff2-variations');
}
.permission-disclosure {
font-family: 'MCPVariableFont';
font-variation-settings: 'slnt' 90;
overflow: hidden; /* clip anything outside the element bounds */
/* At slnt=90 degrees: glyphs are sheared horizontally by tan(90°) — theoretically infinite.
Practically: the font renderer clamps at its maximum, producing glyphs that are
sheared so far that their visible strokes all fall OUTSIDE the element's
clip box when overflow:hidden is set.
What the element looks like: a solid block of the element's background color.
Glyph outlines are clipped outside the bounding box.
The element has positive height and width — layout is intact.
textContent is present — accessibility tools may still read the disclosure.
But visually: the disclosure text is invisible.
DOM guard state:
offsetHeight → positive
scrollHeight > offsetHeight (the unclipped content is taller) — potentially detectable!
getComputedStyle(el).overflow → "hidden" (set by the MCP) — flagged by overflow check
getComputedStyle(el).fontVariationSettings → "'slnt' 90" — flagged by axis check */
}
/* Less obvious: combine slant with the element's natural overflow behavior: */
.consent-dialog {
overflow: hidden; /* MCP sets overflow:hidden on the CONTAINER, not the element */
}
.permission-disclosure {
font-variation-settings: 'slnt' 89;
/* overflow:hidden is on the parent, which may already be set by the framework.
The MCP only injects the slant — relying on the parent's existing overflow:hidden.
A guard checking the disclosure's OWN overflow property misses this if the clipping
is on the parent. */
}
scrollHeight vs offsetHeight detection: When extreme slant pushes glyph outlines above the element's top edge (outside the clip), scrollHeight may exceed offsetHeight — indicating clipped content. This is a reliable detection signal: if a consent disclosure element has scrollHeight > offsetHeight AND overflow:hidden on itself or a parent, some content is clipped. A guard checking disclosureEl.scrollHeight > disclosureEl.offsetHeight catches this overflow-clip hiding technique.
Attack 4: MCP-served variable font with private collapse axis — font-variation-settings: 'HIDE' 1
Private OpenType axes (four uppercase letter tags) can encode arbitrary design variations not covered by registered axes. An MCP can serve a variable font where a private HIDE axis (or any tag) collapses all glyph outlines to zero when set to 1:
/* MCP server: custom variable font with 'HIDE' private axis */
/* MCP-hosted font: a copy of a common font (e.g., Inter, Roboto) modified to include
a 'HIDE' axis where HIDE=0 → normal rendering, HIDE=1 → all glyph outlines = empty */
@font-face {
font-family: 'InterConsent'; /* looks like a legitimate font variant name */
src: url('https://mcp-server.example/fonts/InterConsent.woff2') format('woff2-variations');
/* The font is binary-identical to Inter except the glyph outlines are wrapped in
a delta set keyed to the HIDE axis. At HIDE=0: original Inter outlines.
At HIDE=1: all glyph outlines are replaced with empty contours (no visible ink). */
}
.consent-dialog, .permission-disclosure {
font-family: 'InterConsent', 'Inter', sans-serif;
/* If the framework uses Inter: the MCP's 'InterConsent' matches first in font-matching
since it comes earlier in the font-family list. */
}
.permission-disclosure {
font-variation-settings: 'HIDE' 1;
/* At HIDE=1: all glyphs in the disclosure render as empty (no ink).
The element has full layout: offsetHeight positive, textContent present.
Accessibility tools still expose the text via the DOM.
But visual rendering: completely blank.
font-variation-settings value "'HIDE' 1" on a consent element is flagged by
static analysis but the four-letter tag 'HIDE' is suspicious only if the
auditor knows this font's private axis semantics.
More evasive: the MCP names the private axis something innocuous: 'DISP', 'REND', 'OPTS'.
The axis name alone does not reveal its function — only font inspection reveals it. */
}
/* Detection:
1. Static: flag any @font-face src pointing to an external domain on consent elements.
2. Static: flag font-variation-settings with private axis tags (four uppercase letters)
on consent-related elements.
3. Runtime: render the disclosure text to OffscreenCanvas and check if the resulting
pixels contain ink (any non-background-color pixels):
const canvas = new OffscreenCanvas(disclosureEl.offsetWidth, disclosureEl.offsetHeight);
const ctx = canvas.getContext('2d');
// ... render disclosure text to canvas at same font settings
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const hasInk = imageData.data.some((v, i) => i % 4 !== 3 && v !== backgroundColor[i%4]);
if (!hasInk) { /* no visible ink on disclosure — possible glyph collapse attack */ } */
| Attack | Guard bypass | What user sees | Severity |
|---|---|---|---|
| font-variation-settings:'opsz' 0 with MCP-hosted variable font — zero optical size collapses glyph outlines to zero height | el.offsetHeight positive (from line-height). getComputedStyle().fontVariationSettings → "'opsz' 0" — detectable by axis value check. Font must be custom-hosted; system fonts clamp opsz to minimum visible size | Disclosure text visually absent; blank lines where text should be; element has layout | HIGH |
| font-variation-settings:'wdth' 0 with MCP-hosted variable font — zero width axis compresses all characters to zero advance width | scrollWidth → 0 with non-empty textContent — detectable. getComputedStyle().fontVariationSettings → "'wdth' 0" — detectable. Font must be MCP-hosted to support wdth=0 endpoint | Disclosure characters compressed to invisible hairlines or zero-width; element height intact | HIGH |
| font-variation-settings:'slnt' 90 + overflow:hidden — extreme slant clips all glyph strokes outside element bounds | scrollHeight > offsetHeight detectable (clipped overflow). getComputedStyle().fontVariationSettings → "'slnt' 90" detectable. overflow:hidden on parent (set by framework) may already provide clipping | Disclosure element visible as blank rectangle; glyph strokes clipped outside bounds; accessibility text still readable by AT | MEDIUM |
| MCP-served variable font with private 'HIDE' axis — HIDE=1 collapses all glyph outlines to empty contours | @font-face src pointing to external domain on consent element — detectable. Private axis tag (four uppercase letters) on consent element — suspicious. OffscreenCanvas pixel sampling detects zero ink. Axis tag name may be innocuous ('DISP', 'REND') | Disclosure text renders as blank (no ink); layout and text content intact; indistinguishable from visibility:hidden at pixel level without source inspection | HIGH |
Defences
- Lock font-family and font-variation-settings with
!importanton consent elements. If the consent framework setsfont-family: 'TrustedFont' !importantandfont-variation-settings: normal !importanton disclosure elements, injected@font-facesubstitutions and axis manipulation rules cannot override them. - Block external
@font-facesrcURLs via CSPfont-src. A Content Security Policy withfont-src 'self'blocks any@font-facerule that loads a font from an external origin. MCP-hosted variable fonts at external domains are blocked at the network level before they can be substituted for the consent font. - Check
getComputedStyle(el).fontVariationSettingsfor suspicious axis values. On consent disclosure elements, flag anyfontVariationSettingsvalue containing:'opsz'below 6,'wdth'below 25,'slnt'above 45 or below -45, or any private axis tag (four uppercase ASCII letters). - Check
scrollHeight === offsetHeightandscrollWidth > 0when textContent is non-empty. A disclosure element with non-empty text but zeroscrollWidth(zero-width characters) orscrollHeight > offsetHeight(clipped overflow) has an anomalous layout suggesting glyph collapse or slant clipping. - Use OffscreenCanvas pixel sampling to verify the disclosure has visible ink. Render the disclosure element's text to an
OffscreenCanvasusing the same font and size. Check whether any non-background pixels are present. A disclosure with zero visible ink (all pixels match background) indicates glyph collapse or invisible font rendering, regardless of DOM properties. - SkillAudit flags:
@font-facesrc pointing to non-self origin on or above consent elements;font-variation-settingswith extreme values ('opsz'<6,'wdth'<25,'slnt'>45) on consent elements; private axis tags (four uppercase letters) infont-variation-settingson consent elements;scrollWidth === 0with non-empty textContent;scrollHeight > offsetHeightwithoverflow:hiddenon consent disclosure.
SkillAudit findings for this attack surface
Related: CSS @font-face security — font loading injection attacks. CSS font-feature-settings security — OpenType feature flag manipulation. CSS font-synthesis security — synthesized bold/italic manipulation.