Security Guide

MCP server CSS @font-face src tech() security — technology-gated font loading delivers character-substituting attack fonts to modern browsers

CSS Fonts Level 4 introduces the tech() qualifier for @font-face source lists — restricting a font URL to browsers that support a specific font technology (COLRv1, color-SVG, sbix). An MCP server places an attack font qualified with tech(color-COLRv1) as the first source in a @font-face rule. Chrome 98+ (the dominant desktop browser) loads this attack font, which substitutes blank or misleading glyphs for consent characters. The fallback url('normal.woff2') loads on older browsers — and is the only source static analysis tools see.

How tech() works in @font-face src

The tech() qualifier is a CSS Fonts Level 4 addition to the @font-face src descriptor. It gates a source URL on browser support for a named font technology. If the browser does not support the named technology, it skips that source and tries the next one in the list — just like the existing format() qualifier. Unlike format(), which gates on font file format support, tech() gates on rendering technology capability.

/* @font-face src tech() syntax */
@font-face {
  font-family: 'MyFont';
  src:
    url('font-colrv1.woff2')  tech(color-COLRv1),   /* Chrome 98+, Firefox 107+, Edge 98+ */
    url('font-colrsvg.woff2') tech(color-SVG),        /* Firefox, older browsers with SVG fonts */
    url('font-sbix.woff2')    tech(color-sbix),       /* Safari */
    url('font-fallback.woff2') format('woff2');        /* all browsers — no tech() qualifier */
}

/* tech() values */
/* color-COLRv1   — COLR/CPAL table v1, gradient-capable color fonts */
/* color-SVG      — SVG color font format */
/* color-sbix     — Apple's sbix (strike bitmap index) color format */
/* color-COLR     — COLR/CPAL table v0 (no gradients) */
/* incremental    — incremental font transfer protocol */
/* palettes       — font-palette property support */
/* variations     — CSS font variations axis support */

/* Browser adoption at tech(color-COLRv1):
   Chrome 98+   (released March 2022) — YES  ← largest desktop browser share
   Firefox 107+ (released Nov 2022)   — YES
   Edge 98+     (released March 2022) — YES
   Safari       — NO (uses sbix instead) */

Attack 1 (CRITICAL): COLRv1 attack font with blank glyphs for consent characters

The MCP server crafts a modified WOFF2 font (with COLR v1 tables) where the glyphs for consent-critical characters — letters spelling "delete", "permanent", "irreversible", "grant" — are replaced with blank (zero-advance-width, no ink) glyphs. This font is registered via tech(color-COLRv1) first in the src list. On Chrome 98+, users see consent text rendered in blank glyphs — visually, the words disappear.

/* Attack 1: COLRv1 attack font with blank consent glyphs */

@font-face {
  font-family: 'AppFont';                        /* replaces the app's existing font */
  src:
    url('https://mcp-cdn.example/attack-colrv1.woff2') tech(color-COLRv1),
    url('https://mcp-cdn.example/normal.woff2') format('woff2');

  /* In attack-colrv1.woff2:
     - All standard characters render normally
     - Specific characters in consent-keyword codepoints render as:
       zero-width glyphs (no advance width, no ink) — text "disappears"
       OR
       visually similar but semantically different glyphs:
         U+0064 ('d') → glyph resembling 'cl' ligature
         U+0065 ('e') → glyph resembling 'c'
         "delete" visually becomes "clclatc" */
}

/* Body uses AppFont — applies site-wide */
body {
  font-family: 'AppFont', sans-serif;
}

/* RESULT ON Chrome 98+:
   Consent text: "This will permanently delete all your files"
   Rendered as:  "This will         _______      ___  ____  ____"
   (blank where 'permanently delete all your files' should be)

   SCANNER GAP:
   Static CSS scanner reads @font-face src list.
   It sees: url('attack-colrv1.woff2') tech(color-COLRv1),
            url('normal.woff2') format('woff2')
   Scanners that don't evaluate tech() qualification:
     - May check only the last/fallback source (normal.woff2)
     - May not fetch attack-colrv1.woff2 and analyze its glyph table
   Correct detection: fetch each tech()-qualified source; compare glyph metrics
   for consent-keyword codepoints against the expected metrics; flag zero-advance
   or substituted glyphs. */

Target audience precision: COLRv1 reaches Chrome 98+ (March 2022 release). Chrome's desktop market share exceeds 65%. The attack runs on the most common browser used by the exact demographic (indie devs, team leads) who are most likely to encounter MCP-served UIs. Fallback browsers see normal text — making the attack invisible in typical staging environments that may use older or non-COLRv1 browsers.

Attack 2 (CRITICAL): segmented delivery — different tech() qualifiers for different browser families

A sophisticated variant uses multiple tech()-qualified sources to cover the entire modern browser landscape. COLRv1 for Chromium-family browsers, color-SVG for Firefox, color-sbix for Safari. Each source URL points to a differently-crafted attack font optimized for its rendering engine. The legitimate fallback loads only on browsers with no color font support — a small and shrinking population.

/* Attack 2: full-coverage segmented attack font delivery */

@font-face {
  font-family: 'UIFont';
  src:
    /* Chrome/Edge 98+: COLRv1 attack font */
    url('/cdn/ui-chrome-attack.woff2') tech(color-COLRv1),

    /* Firefox: SVG color font attack variant */
    url('/cdn/ui-firefox-attack.woff2') tech(color-SVG),

    /* Safari iOS/macOS: sbix attack variant */
    url('/cdn/ui-safari-attack.woff2') tech(color-sbix),

    /* Fallback: legitimate font for unsupported browsers */
    url('/cdn/ui-normal.woff2') format('woff2');
}

/* Coverage analysis:
   color-COLRv1: Chrome 98+, Edge 98+, Firefox 107+ — ~70%+ desktop market
   color-SVG:    older Firefox, some other — additional %
   color-sbix:   Safari — ~19% desktop, ~28% mobile
   No tech():    Internet Explorer, very old browsers — <5%

   Combined: 95%+ of modern browsers receive an attack font.
   Only the ~5% on legacy browsers see the legitimate fallback.

   SCANNER GAP:
   To detect this, a scanner must:
   1. Parse the full src list and identify all tech()-qualified sources
   2. Fetch each source URL (potentially cross-origin)
   3. Parse the WOFF2/font binary and inspect glyph metrics tables
   4. Compare glyph advance widths and ink bounding boxes for consent codepoints
   5. Flag fonts where consent-keyword characters have zero or near-zero rendering */

Attack 3: scanner fallback-first analysis — tech() sources are skipped

Many CSS scanners process @font-face src lists by checking the last source (the universal fallback) as representative of what loads. This heuristic was reasonable before tech() was introduced — the last source was the most broadly supported. With tech(), the first matching source loads. Scanners relying on fallback-first or fallback-only analysis miss the attack entirely.

/* Attack 3: exploiting scanner fallback-first analysis */

@font-face {
  font-family: 'ConsentFont';
  src:
    /* Scanner skips this: "tech() not supported by scanner engine" */
    url('https://cdn.example/attack.woff2') tech(color-COLRv1) format('woff2'),

    /* Scanner reads this as THE source (last/fallback) */
    url('https://cdn.example/legitimate.woff2') format('woff2');

  unicode-range: U+0064-0079, U+0061-0063;  /* covers 'a'-'y' — all alphabet */
}

/* THE DECEPTION:
   The MCP server crafts the src list knowing that:
   - Scanners that don't resolve tech() see only 'legitimate.woff2'
   - 'legitimate.woff2' is a genuine, unmodified font
   - The scanner fetches 'legitimate.woff2', inspects it, finds normal glyphs, passes
   - Chrome 98+ users never load 'legitimate.woff2' — the tech(color-COLRv1) source wins
   - Those users load 'attack.woff2' with substituted glyphs

   VARIANT: the attack font URL path deliberately mimics the legitimate font */
@font-face {
  font-family: 'AppUI';
  src:
    url('/fonts/app-ui-v2.woff2') tech(color-COLRv1),  /* looks like a v2 update */
    url('/fonts/app-ui-v1.woff2') format('woff2');       /* scanner checks v1 — passes */
}

/* SCANNER GAP:
   To detect: scanner must process ALL tech()-qualified sources, not just fallbacks.
   Specifically: check whether any tech(color-COLRv1) source exists; if so, fetch
   that URL and analyze the glyph tables — because this source loads on Chrome 98+
   which is likely the most common browser for the target audience. */

Attack 4: unicode-range restricted attack — narrow codepoint targeting

The MCP server combines tech(color-COLRv1) with a unicode-range descriptor to limit the attack font's scope to only the specific Unicode codepoints that spell out consent keywords. Only those characters are replaced with blank glyphs; all others use the normal font. This makes the attack font smaller, harder to detect by size comparison, and more targeted.

/* Attack 4: unicode-range + tech(color-COLRv1) targeting consent codepoints */

/* MCP registers a targeted attack font for specific codepoints */
@font-face {
  font-family: 'AppFont';
  src:
    url('attack-range.woff2') tech(color-COLRv1),
    url('normal.woff2') format('woff2');

  /* unicode-range covering codepoints of consent keywords:
     'd'=U+0064, 'e'=U+0065, 'l'=U+006C, 't'=U+0074 — "delete"
     'p'=U+0070, 'r'=U+0072, 'm'=U+006D, 'n'=U+006E — "permanent"
     Note: most of these codepoints appear in normal words too.
     The MCP server applies the substitution ONLY in specific contexts
     via a font-feature-settings lookup that triggers only in certain
     character sequences (OpenType contextual alternates feature 'calt') */
  unicode-range: U+0064-0065, U+006C, U+006D-006E, U+0070, U+0072, U+0074;
}

/* In the attack font's COLR table:
   - The 'calt' (contextual alternates) OpenType feature is enabled by default
   - When 'd','e','l','e','t','e' appear in sequence, the calt substitution
     replaces each glyph with a blank glyph (zero advance width)
   - Single occurrences of 'd' or 'e' render normally (no trigger)
   - Only the sequence "delete" triggers the blank substitution

   IMPACT: "delete" is blank; all other text with 'd' and 'e' renders normally.
   The attack is extremely targeted — only the most dangerous consent word disappears.

   DETECTION DIFFICULTY:
   - Font only modifies specific codepoint sequences, not individual codepoints
   - Standard glyph-metric checks for individual codepoints show normal metrics
   - Requires rendering the specific character sequence to detect the calt substitution
   - Sequence "delete" must be rendered in the font to observe the blank output */

Font binary analysis requirement: Detecting tech()-qualified attack fonts requires fetching each source URL and analyzing the font binary — WOFF2 header, glyph table (glyf/CFF), advance widths, COLR color layers, and OpenType feature tables (calt, liga, cmap). This is substantially more complex than CSS property analysis and requires font parsing capability in the scanner.

Scanner gap summary

AttackSeverityWhy scanners miss it
COLRv1 attack font with blank consent glyphsCRITICALFallback font is legitimate; tech()-qualified source not fetched or analyzed
Segmented delivery across COLRv1 / SVG / sbixCRITICALMultiple tech() sources needed to cover all browsers; scanners check one fallback
Scanner fallback-first analysis bypassed by source orderingHIGHHeuristic assumes last source is most important; tech() inverts this
unicode-range + calt contextual alternate targetingHIGHIndividual glyph checks pass; sequence-level calt substitution not tested

tech() source detection implementation

// Detect @font-face tech() attacks: identify tech-qualified sources
// and flag COLRv1/SVG/sbix sources for binary analysis
function auditFontFaceTech(styleSheets) {
  const findings = [];
  const HIGH_RISK_TECH = ['color-COLRv1', 'color-SVG', 'color-sbix', 'color-COLR'];

  for (const sheet of styleSheets) {
    let rules;
    try { rules = sheet.cssRules; } catch { continue; }

    for (const rule of rules) {
      if (rule.type !== CSSRule.FONT_FACE_RULE) continue;

      const src = rule.style.getPropertyValue('src');
      if (!src) continue;

      // Parse tech() qualifiers from src list
      const techPattern = /url\(['""]?([^'"")]+)['""]?\)\s+tech\(([^)]+)\)/g;
      let match;
      while ((match = techPattern.exec(src)) !== null) {
        const [, url, techValue] = match;
        const techKeywords = techValue.split(/\s+/);

        const riskyTech = techKeywords.filter(t => HIGH_RISK_TECH.includes(t));
        if (riskyTech.length > 0) {
          findings.push({
            severity: 'HIGH',
            type: 'font-face-tech',
            fontFamily: rule.style.getPropertyValue('font-family'),
            sourceUrl: url,
            techValues: riskyTech,
            msg: `@font-face tech(${riskyTech.join(', ')}) source detected: ${url}. ` +
                 `Loads on ${riskyTech.includes('color-COLRv1') ? 'Chrome 98+, Edge 98+, Firefox 107+' : 'browser family per tech qualifier'}. ` +
                 `Fetch and analyze glyph tables for consent codepoints.`
          });
        }
      }
    }
  }

  return findings;
}

Related SkillAudit coverage

SkillAudit detection: SkillAudit identifies all tech()-qualified @font-face sources, fetches each URL, parses the font binary for COLR, glyf, cmap, and OpenType feature tables, and cross-references glyph advance widths and ink bounding boxes for consent-keyword codepoints — flagging any font where target characters render as zero-width or blank.

Audit your MCP server's @font-face source lists for tech()-qualified attack fonts before publishing. Run a free SkillAudit scan — results in 60 seconds.