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

Medium Forced small-caps synthesis + container overflow:hidden + tight height: synthesized glyphs scaled to ~80% are clipped by container height matching full-cap dimensions; consent label invisible; button box and dimensions correct; detected by checking font-variant-caps + ancestor overflow:hidden combination with pixel heights below font-size × 0.9.
Medium Overflow:hidden on button + line-height:0: synthesized small-caps glyphs clip entirely at zero-height line box; label invisible; button interactive; detected by checking overflow on the button element itself combined with line-height:0 or height below 70% of computed font-size.
High Sub-pixel font-size with small-caps synthesis: synthesized glyph height below 1px physical pixel — no pixels rendered for the label; button retains full interactive dimensions via padding; text content is non-empty but invisible; detected by checking computed font-size on elements with font-variant-caps: small-caps.
High JS mousedown injects line-height:0 + font-variant-caps:all-small-caps: all glyph rendering clips before click fires; label invisible at click time; MutationObserver must watch font-synthesis, font-variant-caps, line-height, and overflow on the button element simultaneously.

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.