Security Guide
MCP server CSS font-synthesis-small-caps security — forced synthesis shrinks label text height causing overflow clip, synthesis + overflow:hidden clips synthetic glyphs, sub-1px synthesis makes label invisible, JS toggles synthesis to collapse text at mousedown
CSS font-synthesis-small-caps controls whether the browser synthesizes small-capitals glyphs when the font lacks a native small-caps variant. Synthesis scales uppercase letters to ~80% of normal cap height. Attacks exploit this shrinkage to clip consent labels via container overflow, render text below 1px height, or collapse synthesized glyphs at the moment of user interaction.
CSS font-synthesis-small-caps — property overview
font-synthesis-small-caps is a sub-property of font-synthesis. Values: auto (default — browser may synthesize if font lacks small-caps) and none (disable synthesis). It takes effect when font-variant-caps: small-caps (or all-small-caps) is applied and the font does not include a dedicated small-caps axis or variant. Synthesis scales existing uppercase glyphs to approximately 75–85% of normal cap height. Related: font-variant-caps, font-synthesis, font-size.
Attack 1: forced synthesis shrinks text height — label clipped by container
When font-variant-caps: small-caps triggers synthesis (because the font lacks native small-caps), uppercase glyph heights shrink to ~80% of the line box height. If the containing element has a height set to match the full-size cap height and overflow: hidden, the synthesized glyphs are clipped. The consent label text is invisible — the button box remains visible with correct dimensions.
/* Attack: container height clips synthesized small-caps */
.consent-label-wrapper {
height: 0.8em; /* matches full-cap em height at 1.0 scale */
overflow: hidden; /* any glyph shrunk below 0.8em cap height will clip */
line-height: 1;
}
.consent-btn {
font-variant-caps: small-caps;
/* font-synthesis-small-caps: auto (default) — browser synthesizes
Synthesis scales capitals to ~0.65–0.75em cap height
Container height 0.8em clips the top of synthesized glyphs
Label: "ACCEPT" becomes clipped fragments — unreadable or invisible
The button box (border, background) remains at full visible size */
}
/* More targeted variant */
.consent-label {
font-variant-caps: all-small-caps; /* all letters rendered as small-caps */
font-synthesis-small-caps: auto; /* explicit — ensure synthesis enabled */
line-height: 0; /* collapse line box height to 0 */
/* line-height:0 makes the rendered line box zero height.
Glyphs paint but overflow:hidden on parent clips everything.
Text is invisible. Button is interactive but label-less. */
}
// Detection: check font-variant-caps + synthesis + container overflow
function auditSmallCapsSynthesis(btn) {
const cs = getComputedStyle(btn);
const variantCaps = cs.getPropertyValue('font-variant-caps');
const synthesis = cs.getPropertyValue('font-synthesis-small-caps') ||
cs.getPropertyValue('font-synthesis');
const usesSmallCaps = ['small-caps', 'all-small-caps', 'petite-caps', 'all-petite-caps']
.some(v => variantCaps.includes(v));
if (!usesSmallCaps) return;
// Check container for overflow clipping
let parent = btn.parentElement;
while (parent && parent !== document.body) {
const parentCs = getComputedStyle(parent);
const overflow = parentCs.getPropertyValue('overflow');
const overflowY = parentCs.getPropertyValue('overflow-y');
const height = parentCs.getPropertyValue('height');
const heightPx = parseFloat(height) || 0;
const clips = overflow === 'hidden' || overflow === 'clip' ||
overflowY === 'hidden' || overflowY === 'clip';
if (clips && heightPx > 0 && heightPx < 20) {
console.warn('[SkillAudit] font-synthesis-small-caps: small-caps enabled +',
'ancestor container clips with height:', heightPx, 'px;',
'synthesized glyphs may be clipped; check label visibility;',
'font-variant-caps:', variantCaps,
'| container:', parent, '| button:', btn);
}
parent = parent.parentElement;
}
}
Font-dependent attack: This attack requires the loaded font to lack a native small-caps variant, triggering synthesis. Whether a given font synthesizes small-caps depends on its OpenType feature set — auditors must check not just the CSS properties but whether synthesis actually activates for the specific font in use.
Attack 2: synthesis + overflow:hidden on the button itself — clipped synthetic variant
If overflow: hidden is set directly on the consent button and the font-size is set to match the expected text height, synthesized glyphs that are shorter than expected may render partially clipped. Combined with padding: 0 and line-height set tightly, the synthesized small-caps label is trimmed to near-invisible height.
/* Attack: overflow:hidden + tight dimensions on button itself */
.consent-btn {
font-variant-caps: small-caps;
font-synthesis-small-caps: auto;
font-size: 16px;
line-height: 1;
padding: 0;
height: 10px; /* shorter than normal cap height at 16px */
overflow: hidden; /* clip the bottom of synthesized glyphs */
/* Normal caps at 16px: ~11px cap height
Synthesized small-caps: ~0.75 × 11px = ~8.25px cap height
Container height 10px: clips bottom ~1.75px of synthesized glyphs
For "A": normal cap height ~11px — clips 1px
For synthesized small-caps: cap height ~8px — visible inside 10px
Exact clipping varies by font and browser rendering engine.
Adjust height to 8px to clip most synthesized glyph height */
}
/* Even simpler: line-height:0 collapses rendering entirely */
.consent-btn {
font-variant-caps: small-caps;
line-height: 0;
overflow: hidden;
/* All glyph painting occurs outside the 0-height line box.
overflow:hidden clips everything outside.
Label is invisible. Button box (background, border) remains. */
}
// Detection: check overflow on button + line-height
function auditSmallCapsButtonOverflow(btn) {
const cs = getComputedStyle(btn);
const variantCaps = cs.getPropertyValue('font-variant-caps');
const usesSmallCaps = ['small-caps', 'all-small-caps'].some(v => variantCaps.includes(v));
if (!usesSmallCaps) return;
const overflow = cs.getPropertyValue('overflow');
const overflowY = cs.getPropertyValue('overflow-y');
const height = parseFloat(cs.getPropertyValue('height')) || 0;
const lineHeight = cs.getPropertyValue('line-height');
const fontSize = parseFloat(cs.getPropertyValue('font-size')) || 16;
const clips = overflow === 'hidden' || overflow === 'clip' ||
overflowY === 'hidden' || overflowY === 'clip';
if (clips) {
if (lineHeight === '0' || lineHeight === '0px') {
console.warn('[SkillAudit] font-synthesis-small-caps: button has line-height:0 + overflow:hidden;',
'synthesized small-caps glyphs fully clipped; label invisible;',
'font-variant-caps:', variantCaps, '| button:', btn);
} else if (height < fontSize * 0.7) {
console.warn('[SkillAudit] font-synthesis-small-caps: button height', height, 'px < 70% of font-size', fontSize, 'px;',
'may clip synthesized small-caps glyphs; check label visibility;',
'font-variant-caps:', variantCaps, '| button:', btn);
}
}
}
Attack 3: synthesis at sub-readable font-size — label glyphs below 1px height
When font-size is extremely small (e.g., 1px) and font-variant-caps: small-caps triggers synthesis, the synthesized glyph height is approximately 0.75px — below the minimum rasterization threshold of 1 physical pixel. The label renders as a sub-pixel artifact that is physically invisible. The button element has correct dimensions; only the text rendering is affected.
/* Attack: tiny font-size + small-caps synthesis */
.consent-btn {
font-variant-caps: small-caps;
font-synthesis-small-caps: auto;
font-size: 1px; /* sub-readable — synthesized cap height ≈ 0.75px */
/* At 1px font-size:
Normal cap height: ~0.7px (below 1px physical pixel)
Synthesized small-caps: ~0.75 × 0.7 ≈ 0.5px (far below rasterization threshold)
Browser may not render any pixels for the glyphs.
Label is invisible even if contrast, opacity, and visibility are all correct.
Button box uses a different size value or padding to maintain dimensions. */
padding: 12px 24px; /* button box looks normal size */
width: auto; /* button sized by padding, not font-size */
}
/* Variant: font-size:0 collapses text unconditionally */
.consent-btn {
font-variant-caps: small-caps;
font-size: 0;
padding: 10px 20px;
/* font-size:0: no glyph pixels rendered regardless of font-variant-caps.
Synthesis also produces 0-size glyphs.
Button box retained via padding. Auditors checking btn.textContent see non-empty text. */
}
// Detection: check effective font-size for small-caps elements
function auditSmallCapsFontSize(btn) {
const cs = getComputedStyle(btn);
const variantCaps = cs.getPropertyValue('font-variant-caps');
const usesSmallCaps = ['small-caps', 'all-small-caps'].some(v => variantCaps.includes(v));
if (!usesSmallCaps) return;
const fontSize = parseFloat(cs.getPropertyValue('font-size')) || 0;
if (fontSize < 8) {
console.warn('[SkillAudit] font-synthesis-small-caps: font-size', fontSize, 'px on small-caps element;',
'synthesized glyph height will be ~', (fontSize * 0.75).toFixed(2), 'px;',
'below readable threshold; consent label invisible;',
'font-variant-caps:', variantCaps, '| button:', btn);
}
// Also check child text nodes' computed font-size
const walker = document.createTreeWalker(btn, NodeFilter.SHOW_ELEMENT);
let node;
while ((node = walker.nextNode())) {
const childCs = getComputedStyle(node);
const childVarCaps = childCs.getPropertyValue('font-variant-caps');
const childFontSize = parseFloat(childCs.getPropertyValue('font-size')) || 0;
if (['small-caps', 'all-small-caps'].some(v => childVarCaps.includes(v)) && childFontSize < 8) {
console.warn('[SkillAudit] font-synthesis-small-caps: descendant', node,
'has font-size', childFontSize, 'px with small-caps — synthesized label invisible');
}
}
}
Attack 4: JS toggles font-synthesis-small-caps to collapse synthesized glyphs at mousedown
JavaScript can inject font-synthesis-small-caps: none at mousedown. If font-variant-caps: small-caps is in use but the font lacks native small-caps, switching synthesis off causes the browser to render native uppercase letters at the current element dimensions — which may have been sized for the synthesized (smaller) variant. Alternatively, the attack can inject font-variant-caps: small-caps combined with a tight height constraint at mousedown, making the label overflow-clip precisely when the user clicks.
/* JS attack: inject synthesis override + height constraint at mousedown */
document.addEventListener('mousedown', (e) => {
const btn = document.querySelector('.consent-btn');
if (!btn) return;
// Trigger synthesis + collapse container
btn.style.setProperty('font-variant-caps', 'all-small-caps');
btn.style.setProperty('font-synthesis-small-caps', 'auto');
btn.style.setProperty('line-height', '0');
btn.style.setProperty('overflow', 'hidden');
/* Font switches to synthesized all-small-caps at line-height:0.
All glyph rendering clips to 0-height line box.
Label text invisible before click event fires.
Button element retained — click fires on element.
But the user cannot see what they are clicking. */
}, { capture: true });
// Or: set font-synthesis-small-caps:none when font lacks native small-caps
// This forces browser to not render any small-caps at all
document.addEventListener('mousedown', (e) => {
const btn = document.querySelector('.consent-btn');
if (!btn) return;
btn.style.setProperty('font-synthesis', 'none');
/* With font-synthesis:none, font-variant-caps:small-caps on a font
without native small-caps: browser renders normal uppercase glyphs
at the specified font-size — but if the button height was sized for
synthesized (smaller) glyphs, normal-size glyphs may overflow the
container and be clipped. */
}, { capture: true });
// Detection: MutationObserver for synthesis property changes at mousedown
function monitorFontSynthesisMutation(btn) {
const obs = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.type === 'attributes' && m.attributeName === 'style') {
const cs = getComputedStyle(btn);
const lineHeight = cs.getPropertyValue('line-height');
const overflow = cs.getPropertyValue('overflow');
const synthesis = cs.getPropertyValue('font-synthesis-small-caps') ||
cs.getPropertyValue('font-synthesis');
const variantCaps = cs.getPropertyValue('font-variant-caps');
if (lineHeight === '0' || lineHeight === '0px') {
console.warn('[SkillAudit] font-synthesis-small-caps: line-height set to 0 via inline style;',
'glyphs clipped; font-variant-caps:', variantCaps,
'| synthesis:', synthesis, '| at:', new Date().toISOString());
}
if (synthesis === 'none' && ['small-caps','all-small-caps'].some(v => variantCaps.includes(v))) {
console.warn('[SkillAudit] font-synthesis-small-caps: synthesis disabled at',
new Date().toISOString(), '— small-caps may not render on font lacking native variant;',
'| button:', btn);
}
}
}
});
obs.observe(btn, { attributes: true, attributeFilter: ['style'] });
}
Findings summary
SkillAudit checks computed font-variant-caps on consent buttons, verifies synthesis conditions against the loaded font's OpenType features, audits container overflow and line-height, and monitors mousedown mutations for synthesis-related property changes. Run a free audit on your MCP server.