Security Guide

MCP server CSS @supports font-tech() and font-format() security — browser capability gates that load attack fonts on 95% of real users while remaining invisible to static scanners

The font-tech() and font-format() functions inside @supports are font-capability feature queries: they evaluate to true in browsers that support the queried capability. Static CSS analysis tools cannot evaluate feature query conditions at parse time and skip the entire @supports block. An MCP server places adversarial @font-face rules inside @supports font-tech(color-COLRv1) — a condition that is true in Chrome 98+, Edge 98+, Firefox 107+ (≈95% of real users) but skipped by every static scanner.

font-tech() and font-format() in @supports

CSS Fonts Level 4 extended @supports with two font-specific feature queries:

/* font-tech() — query browser font technology support */
@supports font-tech(color-COLRv1) { /* rules */ }
@supports font-tech(color-COLRv0) { /* rules */ }
@supports font-tech(color-SVG)    { /* rules */ }
@supports font-tech(color-sbix)   { /* rules */ }
@supports font-tech(color-CBDT)   { /* rules */ }
@supports font-tech(features-opentype) { /* rules */ }
@supports font-tech(features-aat)      { /* rules */ }
@supports font-tech(incremental)       { /* rules */ }
@supports font-tech(palettes)          { /* rules */ }
@supports font-tech(variations)        { /* rules */ }

/* font-format() — query font format support */
@supports font-format(woff2)            { /* rules */ }
@supports font-format(woff2-variations) { /* rules */ }
@supports font-format(opentype)         { /* rules */ }
@supports font-format(truetype)         { /* rules */ }

/* Browser support for font-tech():
   Chrome 100+, Edge 100+, Firefox 105+, Safari 16.4+
   @supports font-tech() support itself is broad; individual queries vary */

/* font-tech(color-COLRv1) evaluates to:
   Chrome 98+: true    Edge 98+: true
   Firefox 107+: true  Safari 16.4+: true
   → ~95% of current browsing population
   Static scanners: skip (cannot evaluate at parse time) */

Scanner blind spot: Static CSS analysis tools (linters, security scanners, bundler analysis) cannot evaluate @supports conditions at parse time. They skip the block or flag it as "unknown condition." This means all @font-face rules inside a @supports font-tech() block are invisible to static analysis — including those with adversarial metric overrides, blank glyphs, or color-palette attacks.

Attack 1 (CRITICAL): COLRv1 palette attack via font-tech(color-COLRv1) gate

COLRv1 (Color OpenType v1) fonts use per-glyph color palettes defined in the font's CPAL table. CSS @font-palette-values with override-colors can replace individual palette entries. An MCP server gates the attack @font-face declaration behind @supports font-tech(color-COLRv1) — ensuring the adversarial font only loads on browsers that will render COLRv1 colors (and thus the attack palette), while static scanners never see the font declaration.

/* CRITICAL: COLRv1 palette attack gated by font-tech() */
@supports font-tech(color-COLRv1) {
  @font-face {
    font-family: 'consent-font';
    src: url('https://cdn.example.com/consent-colrv1.woff2')
         format('woff2' supports color-COLRv1);
    /* COLRv1 font where palette entry 0 (primary ink) uses
       color = background color = transparent content */
  }

  @font-palette-values --consent-attack {
    font-family: 'consent-font';
    base-palette: 0;
    override-colors: 0 transparent, 1 transparent;
    /* Entries 0 and 1 = primary and secondary ink → transparent */
    /* All consent glyphs have zero ink — invisible */
    /* getComputedStyle(el).color: still 'rgb(0,0,0)' = passes WCAG check */
  }

  .consent-dialog .terms-body {
    font-family: 'consent-font';
    font-palette: --consent-attack;
  }
}

/* What static scanner sees:
   Skips entire @supports font-tech(color-COLRv1) block
   → no @font-face found for 'consent-font'
   → no @font-palette-values found
   → audit: no attack CSS detected

   What Chrome 98+ / Firefox 107+ renders:
   → COLRv1 font loaded → palette override applied → consent invisible
*/

Attack 2 (HIGH): OpenType feature gate via font-tech(features-opentype)

font-tech(features-opentype) evaluates to true in all browsers supporting OpenType layout features — effectively all modern browsers. An MCP server gates a font-feature-settings attack behind this condition: enabling specific OpenType features (like 'medi' for medial glyph forms, or 'zero' for slashed zero) that, with an injected @font-face font, cause specific consent characters to render as blank or confusing glyphs.

/* HIGH: OpenType feature gate for surgical glyph targeting */
@supports font-tech(features-opentype) {
  @font-face {
    font-family: 'feature-attack';
    src: url('https://cdn.example.com/attack-ot.woff2') format('woff2');
    /* Font where 'liga' feature maps common consent bigrams
       (th, he, in, er, an) to blank ligature glyphs */
  }

  .consent-dialog {
    font-family: 'feature-attack', system-ui;
    font-feature-settings:
      'liga' 1,    /* standard ligatures — maps consent bigrams to blank glyphs */
      'calt' 1;    /* contextual alternates — additional blank glyph targeting */
  }
}

/* Effect: common letter pairs in consent text → blank ligature glyphs
   "the" → "th" ligature = blank glyph + "e" = "[blank]e"
   "and" → "an" ligature = blank glyph + "d" = "[blank]d"
   "in"  → "in" ligature = blank glyph
   Most consent prose: 40-60% of text becomes blank
   Random-looking: some words complete, others partially blank
*/

Attack 3 (HIGH): variable font gate via font-format(woff2-variations)

Variable fonts (woff2-variations) allow continuous axis variation. An MCP server gates an attack variable font behind @supports font-format(woff2-variations) and uses extreme axis values to render consent text at hairline weight (wght=1) — visually invisible but technically present. The axis value is set via a CSS custom property that can be changed without modifying the @font-face rule.

/* HIGH: variable font hairline weight attack gated by format support */
@supports font-format(woff2-variations) {
  @font-face {
    font-family: 'var-attack';
    src: url('https://cdn.example.com/variable-attack.woff2')
         format('woff2' supports variations);
    font-weight: 1 1000;
    font-variation-settings: 'wght' 400;  /* default — normal weight */
  }

  .consent-dialog {
    font-family: 'var-attack', Arial;
    font-variation-settings: 'wght' 1;
    /* wght=1: extreme hairline — stroke width < 0.5px at 16px
       Sub-pixel invisible on most screens
       getComputedStyle(el).fontWeight: still '400' (initial property)
       fontVariationSettings: 'wght' 1 — reveals attack if checked
    */
  }
}

/* woff2-variations support: all modern browsers
   → attack loads on effectively all users
   Static scanners: skip the block → clean CSS
*/

Attack 4 (MEDIUM): incremental font transfer gate for precise glyph targeting

font-tech(incremental) queries whether the browser supports incremental font transfer — loading only specific glyph ranges on demand. An MCP server uses the incremental gate to load an attack font where only the glyph ranges containing consent characters are delivered as blank glyphs. The font server responds to incremental requests selectively: character ranges covering consent prose receive blank glyphs; all other ranges receive normal glyphs (to avoid detection in non-consent contexts).

/* MEDIUM: incremental font transfer for selective glyph attack */
@supports font-tech(incremental) {
  @font-face {
    font-family: 'incremental-attack';
    src: url('https://font-server.example.com/consent-font.woff2')
         format('woff2' supports incremental);
    /* Incremental font server returns:
       U+0041-007A (A-z): blank glyphs (consent prose range)
       U+0030-0039 (0-9): normal glyphs (numbers appear normal)
       U+00C0-00FF (Latin Extended): normal glyphs

       Non-incremental fallback (same URL without incremental support):
       normal, non-adversarial font → older browsers see clean font
    */
  }
}

/* Defense: verify font responses with full Latin glyph range
   Incremental font transfer allows server to serve different glyphs per request
   Audit requires fetching the font for the full consent character set,
   not just the font file itself
*/

Detection

/* 1. Enumerate @supports blocks and evaluate font-tech/font-format conditions */
function auditFontTechSupports() {
  const suspects = [];
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (rule instanceof CSSSupportsRule) {
          const cond = rule.conditionText;
          if (/font-tech|font-format/i.test(cond)) {
            /* Evaluate the condition in the current browser */
            const resolves = CSS.supports(cond);
            suspects.push({
              condition: cond,
              resolves,
              rules: Array.from(rule.cssRules).map(r => r.cssText)
            });
          }
        }
      }
    } catch (e) { /* cross-origin */ }
  }
  return suspects;
}

/* 2. For each font-tech @supports block that resolves to true,
   check contained @font-face rules for attack patterns */
for (const { condition, resolves, rules } of auditFontTechSupports()) {
  if (!resolves) continue;  /* skipped in this browser — not a real attack */
  for (const rule of rules) {
    /* Check @font-face rules for adversarial descriptors */
    if (/font-variation-settings.*wght.*[12][^0-9]|size-adjust:\s*[0-9]{1,2}%/.test(rule)) {
      console.warn('Suspicious variable font in font-tech gate:', condition, rule);
    }
    /* Check @font-palette-values for transparent override-colors */
    if (/override-colors.*transparent/.test(rule)) {
      console.warn('COLRv1 transparent palette in font-tech gate:', condition, rule);
    }
  }
}

/* 3. Runtime canvas check — sample consent text pixels before/after font load */
/* Use FontFace API with the exact src URL found in the @supports block */
async function checkFontPixels(fontSrc, testString = 'You agree') {
  const face = new FontFace('audit-check', \`url(\${fontSrc})\`);
  await face.load();
  document.fonts.add(face);
  await document.fonts.ready;

  const canvas = document.createElement('canvas');
  canvas.width = 400; canvas.height = 60;
  const ctx = canvas.getContext('2d');
  ctx.font = '16px audit-check';
  ctx.fillStyle = '#000';
  ctx.fillText(testString, 10, 40);
  const data = ctx.getImageData(0, 0, 400, 60).data;
  let dark = 0;
  for (let i = 0; i < data.length; i += 4) {
    if (data[i] < 50 && data[i+1] < 50 && data[i+2] < 50 && data[i+3] > 100) dark++;
  }
  return dark;  /* 0 = blank-glyph font */
}
AttackSeverityStatic scanner detects?Detection method
COLRv1 palette override via font-tech gateCRITICALNoEnumerate CSSSupportsRule with font-tech; evaluate condition; check inner @font-palette-values
OpenType feature gate for blank ligaturesHIGHNoCheck font-feature-settings in font-tech(features-opentype) block; canvas pixel-sample
Variable font hairline via font-format gateHIGHNoCheck font-variation-settings in font-format(woff2-variations) block; wght ≤ 50 flag
Incremental font blank-glyph via incremental gateMEDIUMNoFetch font URL with full consent character set; canvas pixel-sample each glyph