MCP server CSS @font-face unicode-range security
The unicode-range descriptor inside @font-face restricts which Unicode code points a loaded font applies to. MCP servers exploit this by loading a crafted web font whose glyph table maps targeted code points — capital letters, specific consent keywords, or currency symbols — to invisible or ornamental glyphs. The consent element retains correct textContent, normal color, opacity, and visibility values. All standard text-content and style-property checks pass. Only inspection of @font-face rules in document.styleSheets and cross-referencing with the consent element's font-family reveals the substitution.
Attack findings
Background: @font-face unicode-range descriptor
The unicode-range descriptor, defined in CSS Fonts Level 3, tells the browser to use a particular @font-face rule only when rendering characters within the specified Unicode code-point range. This is the standard mechanism behind font subsetting — loading a Devanagari font only for Devanagari code points, or a CJK font only for Han characters, without affecting Latin text. A single font-family stack can include multiple @font-face declarations with different unicode-range values; the browser selects the matching font for each individual character.
The security implication: an attacker can declare a @font-face rule that applies a crafted font to a specific narrow set of code points — for instance, the exact Unicode values spelling "GRANT" or "SHELL" or all capital letters in English — while leaving all other characters rendered by the legitimate system font. The crafted font maps those code points to invisible glyphs (advance-width ≥ 1px so the character still occupies space, but the glyph path has no fill), to zero-advance-width glyphs (collapsing spacing), or to ornamental shapes that carry no semantic meaning.
Detection gap: textContent always reports the underlying Unicode code points regardless of rendering. getComputedStyle(el).fontFamily reports the declared family name, not the actual font file selected for each character. getComputedStyle(el).color reports the element's CSS color (unchanged). Opacity, visibility, and geometric checks all report normal values because the characters are rendered in-flow, just as invisible or ornamental glyphs. Detection requires iterating document.styleSheets to find @font-face rules and cross-referencing their unicode-range values against the code points present in the consent element's text.
Attack 1 — capital-letter invisibility via transparent-glyph font (SA-CSS-UCNRNG-001)
The attacker loads a crafted web font file (a minimal WOFF2 containing only invisible glyphs for code points U+0041–U+005A, the 26 ASCII capital letters) and declares it as a @font-face rule with the same font-family name as the consent element's legitimate font. The unicode-range: U+0041-005A restricts the substitution to capital letters only. Every capital letter in the consent text — typically sentence-initial capitals and the key permission verbs written in caps — is rendered as an invisible glyph. The sentence "By INSTALLING you GRANT us SHELL EXECUTION access" becomes visually "y ou s ". The invisible glyphs still have advance-width (they occupy horizontal space), so word spacing and layout appear nearly normal. textContent returns the full string. Color, opacity, and visibility pass all checks.
/* Attack: transparent-glyph font for all capital letters */
@font-face {
font-family: 'SystemUI'; /* shadows the legitimate font name */
src: url('/fonts/consent-icons.woff2'); /* crafted font: A-Z → invisible glyphs */
unicode-range: U+0041-005A; /* capital A–Z only */
font-weight: 400;
font-style: normal;
}
.consent-text {
font-family: 'SystemUI', system-ui, sans-serif;
/* browser uses crafted font for A–Z; system font for everything else */
/* textContent: "By INSTALLING you GRANT us SHELL EXECUTION" */
/* visual: "y you us E E " */
}
SA-CSS-UCNRNG-001 (High). textContent, color, opacity, and visibility all appear normal. Detection: iterate document.styleSheets; collect all CSSFontFaceRule entries; for each, check style.getPropertyValue('unicode-range') for ranges that cover U+0041–U+005A or U+0061–U+007A (letter ranges); cross-reference the font-family descriptor against the consent element's computed font-family stack; flag any match.
/* Detection: @font-face unicode-range covering letter ranges */
function checkUnicodeRangeAttack(consentEl) {
const consentFamily = getComputedStyle(consentEl).fontFamily.toLowerCase();
const suspiciousRanges = [
/U\+004[1-9A-F]/i, // A-Z range start
/U\+006[1-9A-F]/i, // a-z range start
/U\+0041-005A/i,
/U\+0061-007A/i,
];
const findings = [];
for (const sheet of document.styleSheets) {
let rules;
try { rules = sheet.cssRules; } catch { continue; }
for (const rule of rules) {
if (rule.type !== CSSRule.FONT_FACE_RULE) continue;
const ruleFamily = rule.style.getPropertyValue('font-family')
.replace(/['"]/g, '').toLowerCase();
if (!consentFamily.includes(ruleFamily)) continue;
const range = rule.style.getPropertyValue('unicode-range');
if (!range) continue;
for (const re of suspiciousRanges) {
if (re.test(range)) {
findings.push({ vuln: 'SA-CSS-UCNRNG-001', family: ruleFamily, range });
}
}
}
}
return findings.length ? findings : null;
}
Attack 2 — ornamental glyph substitution for "GRANT" (SA-CSS-UCNRNG-002)
Rather than targeting broad letter ranges (which might be noticed by visual inspection of other page text), a more targeted attack substitutes only the exact code points spelling the key permission verb "GRANT": U+0047 (G), U+0052 (R), U+0041 (A), U+004E (N), U+0054 (T). A crafted font maps each of these to a decorative ornament glyph (❧, ✦, ⁂, ✤, ✿). In the consent sentence "By installing you GRANT us shell execution access", the word "GRANT" visually appears as five decorative symbols with approximately the same total advance-width as the five letters. A casual reader sees decorative punctuation in the middle of a sentence — possibly interpreted as a design flourish. The critical permission verb is destroyed. Because only five specific code points are targeted, lower-case and other capital letters render normally, making the overall text appear readable and legitimate.
/* Attack: ornamental substitution for the word "GRANT" */
@font-face {
font-family: 'AppUI';
src: url('/fonts/ui-ornaments.woff2');
/* G R A N T as ornamental glyphs */
unicode-range: U+0047, U+0052, U+0041, U+004E, U+0054;
}
/* Detection: check for narrow targeted ranges over critical permission words */
function checkNarrowTargetedRange(consentEl) {
const consentText = consentEl.textContent.toUpperCase();
const criticalWords = ['GRANT', 'INSTALL', 'ALLOW', 'ACCEPT', 'EXECUTE', 'ACCESS'];
for (const sheet of document.styleSheets) {
let rules;
try { rules = sheet.cssRules; } catch { continue; }
for (const rule of rules) {
if (rule.type !== CSSRule.FONT_FACE_RULE) continue;
const range = rule.style.getPropertyValue('unicode-range');
if (!range) continue;
const points = range.split(',').map(s => parseInt(s.trim().replace('U+', ''), 16));
const chars = points.map(p => String.fromCodePoint(p)).join('');
for (const word of criticalWords) {
if (consentText.includes(word) && word.split('').every(c => chars.includes(c))) {
return { vuln: 'SA-CSS-UCNRNG-002', targetedWord: word, range };
}
}
}
}
return null;
}
Attack 3 — zero-advance-width space glyph (SA-CSS-UCNRNG-003)
The space character (U+0020) is targeted with a @font-face providing a glyph with zero advance-width. Every space in the consent text collapses. Words that were separated by spaces now run together into a single unbroken string: "Byinstallingyougrantusshellexecutionaccess". The sentence structure is completely destroyed — the text appears as one incomprehensible word. Ironically, textContent still contains all spaces and the full readable text. A content-based scanner that calls textContent sees a well-formed English sentence. The geometric checks (BCR, offsetHeight) return normal values because the single long word is still in the DOM, just overflowing and visually unreadable. A variation uses an extremely wide space glyph (2000 em advance-width) that forces every word onto its own line, making the consent text so tall that the key clauses are below the fold.
/* Attack: zero-width or extreme-width space glyph */
@font-face {
font-family: 'AppSans';
src: url('/fonts/spacetrick.woff2'); /* U+0020 → 0px advance glyph */
unicode-range: U+0020;
}
.consent-text { font-family: 'AppSans', sans-serif; }
/* textContent: "By installing you grant us shell execution access" */
/* visual: "Byinstallingyougrantusshellexecutionaccess" */
/* Detection: check for unicode-range targeting U+0020 (space) */
function checkSpaceGlyphAttack(consentEl) {
const consentFamily = getComputedStyle(consentEl).fontFamily.toLowerCase();
for (const sheet of document.styleSheets) {
let rules;
try { rules = sheet.cssRules; } catch { continue; }
for (const rule of rules) {
if (rule.type !== CSSRule.FONT_FACE_RULE) continue;
const ruleFamily = rule.style.getPropertyValue('font-family')
.replace(/['"]/g, '').toLowerCase();
if (!consentFamily.includes(ruleFamily)) continue;
const range = rule.style.getPropertyValue('unicode-range') || '';
if (range.toUpperCase().includes('U+0020')) {
return { vuln: 'SA-CSS-UCNRNG-003', detail: 'space character (U+0020) in unicode-range for consent font' };
}
}
}
return null;
}
Attack 4 — runtime @font-face injection at mousedown (SA-CSS-UCNRNG-004)
At page load, no suspicious @font-face rules exist. Static stylesheet inspection at audit time returns clean. At mousedown on the install button, a JavaScript handler inserts a <style> element containing a @font-face rule with unicode-range targeting consent key characters. Because the inserted @font-face rule uses the same font-family already applied to the consent element, the browser immediately applies the new rule to matching characters. The glyph substitution fires in the click frame — the moment the user's finger lifts from the button and the click event fires, the substitution has already been active. The install event fires with the user having seen corrupted consent text for the duration of their click gesture.
/* Attack: runtime @font-face injection at mousedown */
installBtn.addEventListener('mousedown', () => {
const style = document.createElement('style');
style.textContent = `
@font-face {
font-family: 'AppUI';
src: url('/fonts/invisible-caps.woff2');
unicode-range: U+0041-005A;
}
`;
document.head.appendChild(style);
/* font loads asynchronously but preloaded via */
/* substitution fires before click event if font is already in cache */
});
/* Detection: MutationObserver on document.head for style insertion */
new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.tagName === 'STYLE' || node.tagName === 'LINK') {
/* re-run @font-face unicode-range audit */
const finding = checkUnicodeRangeAttack(consentEl);
if (finding) {
flagTampering('SA-CSS-UCNRNG-004');
installBtn.disabled = true;
}
}
}
}
}).observe(document.head, { childList: true });
SkillAudit detection: SkillAudit audits all @font-face rules in document.styleSheets at consent-check time, extracting unicode-range descriptors and cross-referencing them against the consent element's computed font-family stack. It flags ranges covering letter code points (U+0041–U+007A), the space character (U+0020), and any narrow ranges whose code points spell critical permission verbs present in the consent text. It also monitors for runtime style injection via MutationObserver on document.head. Run a free audit →
Detection summary
| Attack ID | Properties involved | Key detection signal |
|---|---|---|
| SA-CSS-UCNRNG-001 | @font-face unicode-range U+0041–U+005A (capitals) + transparent-glyph font + consent element font-family matches | CSSFontFaceRule.style.getPropertyValue('unicode-range') covers letter ranges AND font-family matches consent element's computed family |
| SA-CSS-UCNRNG-002 | @font-face unicode-range covering exact code points of critical consent verbs (GRANT, INSTALL, ALLOW) + ornamental glyph font | Decode unicode-range code points to characters; check if they spell any critical permission word present in consent textContent |
| SA-CSS-UCNRNG-003 | @font-face unicode-range U+0020 (space) + zero-advance or extreme-advance-width space glyph | unicode-range string contains 'U+0020' for a font-family used on the consent element |
| SA-CSS-UCNRNG-004 | JS mousedown inserts <style> with @font-face unicode-range rule; preloaded font activates in click frame | MutationObserver on document.head for style/link insertion + re-run unicode-range audit on each insertion |