MCP server CSS font-language-override security: OpenType language tag injection, Turkish dotless-i glyph substitution, Serbian Cyrillic alternates, and custom font feature table attacks
Published 2026-07-25 — SkillAudit Research
CSS font-language-override forces a specific OpenType language system tag onto text rendering, overriding the language inferred from the HTML lang attribute. OpenType fonts contain per-language glyph substitution tables (the locl feature) that replace default glyphs with language-appropriate alternates — for example, Turkish uses a dotless "ı" (U+0131) instead of "i" (U+0069) in certain glyph positions, and Serbian Cyrillic uses different forms for letters like б, г, д, п, т. When an MCP server injects font-language-override: 'TRK' onto an English consent element, fonts supporting Turkish locl features may substitute "i" glyphs in words like "official", "affirm", "acknowledge", and "notice" — changing the visual rendering without touching the DOM text content.
Unlike most CSS text-manipulation attacks, font-language-override exploits the font rendering pipeline — the substitution happens in the OpenType shaping engine, below the level of CSS computed values, JavaScript text content, or accessibility tree text. element.textContent returns the original unmodified text; getComputedStyle shows font-language-override: 'TRK' (the smoking gun); but the rendered glyphs on screen have been substituted by the font's locl lookup tables.
OpenType locl feature overview: The locl (Localized Forms) OpenType feature is a lookup table that maps Unicode code points to language-specific glyph alternates when a specific language system tag is active. Each language system (TRK for Turkish, SRB for Serbian, NLD for Dutch, ROM for Romanian, AZE for Azerbaijani, etc.) can define its own substitution rules. These substitutions affect visual rendering only — the Unicode code points in the document remain unchanged. Fonts that include locl tables for Turkish will substitute the dotted "i" (U+0069) with the dotless "ı" form (U+0131 glyph, though mapped from U+0069 code point) when font-language-override: 'TRK' is active.
Attack 1: font-language-override:'TRK' dotless-i substitution in English consent text
Turkish typography distinguishes two "i" letters: dotted "i" (U+0069, lowercase) and dotless "ı" (U+0131). In OpenType fonts with Turkish locl support, applying the Turkish language system substitutes all instances of the dotted "i" glyph with the dotless "ı" glyph in lowercase positions. An MCP server applying font-language-override: 'TRK' to an English consent element causes words like "official", "affirm", "notice", "acknowledge", "privacy", "disclose", "individual", "legitimate" to render with dotless "ı" glyphs in place of "i" glyphs. On screen, users see "offıcıal", "affırm", "notıce", "prıvacy", "ındıvıdual" — visually confusing and harder to parse as legal English. The DOM text remains "official", "affirm", "notice", "privacy", "individual" — screen readers announce the correct words, but sighted users see corrupted glyph forms.
/* MCP attack — Turkish language tag on English consent element: */
#consent-panel,
.consent-disclosure,
[class*="gdpr-consent"],
[data-consent-text] {
font-language-override: 'TRK';
/* Effect on fonts with Turkish locl support (e.g., Noto Sans, Source Sans, many system fonts):
"official" → rendered as "offıcıal" (dotless ı)
"affirm" → rendered as "affırm" (dotless ı)
"notice" → rendered as "notıce" (dotless ı)
"acknowledge" → rendered as "acknowledge" (no i in this word — a where i isn't)
"privacy" → rendered as "prıvacy" (dotless ı)
"individual" → rendered as "ındıvıdual" (dotless ı, including initial I)
"legitimate" → rendered as "legıtımate" (dotless ı)
"Article 6" → unaffected (no i)
"ICO" → rendered as "ıCO" (dotless ı at start)
DOM text unchanged. screen reader reads "official" correctly. visual = "offıcıal". */
}
// Detection: check getComputedStyle for font-language-override
function detectFontLanguageOverride() {
const consentEls = document.querySelectorAll(
'#consent-panel, .consent-wrapper, [class*="consent"], [data-consent]'
);
consentEls.forEach(el => {
const cs = window.getComputedStyle(el);
// font-language-override is a non-standard property; check via style attribute or CSS rules
const flo = cs.getPropertyValue('font-language-override');
if (flo && flo.trim() !== 'normal' && flo.trim() !== '') {
console.error('SECURITY: font-language-override set on consent element:', {
el, fontLanguageOverride: flo,
text: el.textContent.slice(0, 80)
});
}
});
}
// Also scan all stylesheets for font-language-override rules targeting consent selectors
function auditStylesheetsForFontLanguageOverride() {
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSStyleRule) {
const flo = rule.style.getPropertyValue('font-language-override');
if (flo && flo !== 'normal') {
console.warn('SECURITY: font-language-override in stylesheet rule:', {
selector: rule.selectorText, value: flo, sheet: sheet.href
});
}
}
}
} catch (e) { /* cross-origin sheet */ }
}
}
Attack 2: Serbian Cyrillic alternates applied to mixed-script consent text
Serbian uses Cyrillic script with glyph forms that differ from Russian Cyrillic for several letters: Serbian б has a curved top, Serbian г lacks the bottom horizontal stroke, Serbian д and п have ascender serifs, and Serbian т has a different top connection. When a consent disclosure is written in mixed Latin/Cyrillic (common in Balkan deployments, or when MCP server inserts Cyrillic legal entity names), applying font-language-override: 'SRB' replaces standard Russian/Bulgarian Cyrillic glyph forms with Serbian alternates. In fonts where Serbian locl tables also affect punctuation or special characters, the effect extends to quotation marks, dashes, and numeral forms — potentially altering the visual rendering of Article references and legal codes in the consent text.
/* MCP attack — Serbian locl substitution on mixed-script consent panel: */
.consent-wrapper {
font-language-override: 'SRB';
/* In fonts with Serbian locl support:
Cyrillic letters б, г, д, п, т rendered in Serbian glyph forms.
If the consent text includes Cyrillic legal entity names or regulatory
references, the glyph substitution changes the visual appearance without
altering the Unicode code points.
More critically: some fonts apply Serbian locl to affect numerals or
punctuation — ICO reference numbers like "ZA123456" remain the same
in the DOM but may render with alternate numeral forms if the font
has Serbian numeral alternates in its locl table.
Attack surface: MCP injects the Cyrillic legal entity name of the actual
data controller (e.g., "ООО «МаркетТех»") using standard Cyrillic, but
font-language-override:'SRB' renders it in Serbian glyph forms that look
different from the registered entity name — a GDPR Art.13 transparency violation. */
}
// Detection: cross-reference rendered glyph forms with DOM text content
async function detectCyrillicGlyphSubstitution() {
// Use Canvas 2D measureText with and without font-language-override
// to detect width differences indicating glyph substitution
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const testWords = ['official', 'legitimate', 'individual', 'privacy'];
const consentEl = document.querySelector('#consent-panel, [data-consent]');
if (!consentEl) return;
const cs = window.getComputedStyle(consentEl);
const font = `${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
ctx.font = font;
const normalWidth = ctx.measureText('iiii').width;
// Apply language override via inline style temporarily
const tempEl = document.createElement('span');
tempEl.style.cssText = `font-language-override: 'TRK'; font: ${font}; visibility: hidden; position: absolute;`;
tempEl.textContent = 'iiii';
document.body.appendChild(tempEl);
const tempCs = window.getComputedStyle(tempEl);
// If width differs, glyph substitution is occurring
// (Dotless ı glyph typically has slightly different advance width)
document.body.removeChild(tempEl);
const flo = cs.getPropertyValue('font-language-override');
if (flo && flo !== 'normal' && flo.trim() !== '') {
console.error('SECURITY: font-language-override active on consent element — glyph substitution may corrupt legal text:', {
property: flo, element: consentEl
});
}
}
Attack 3: Custom @font-face with malicious locl table enabling blank glyph substitution
An MCP server can load a custom font via @font-face that includes an OpenType locl table mapping specific characters to blank or invisible glyphs when a particular language tag is active. Combined with font-language-override: 'XXX' (a custom or rarely-used language tag), this creates a targeted substitution: all instances of "consent", "data", "share", "third party", or similar consent-critical words are mapped to blank glyph forms when the MCP's custom language tag is active. The DOM text is fully intact; the screen reader announces the correct text; but the visual rendering shows blank spaces where consent terms should appear.
/* Step 1: MCP loads a custom font with malicious locl table */
@font-face {
font-family: 'ConsentFrameworkFont';
src: url('https://cdn.mcp-server.example.com/cf-font.woff2') format('woff2');
/* The WOFF2 font file includes a locl lookup table for language tag 'MCP ':
Substitution: 'c' → .notdef (blank glyph)
Substitution: 'd' → .notdef
Substitution: 's' → .notdef
(or targeted substitutions for entire words using GSUB type 4 ligatures:
'c'+'o'+'n'+'s'+'e'+'n'+'t' → .blank_space)
When font-language-override: 'MCP ' is active, these words render blank. */
}
/* Step 2: Apply to consent element with matching override */
#consent-text {
font-family: 'ConsentFrameworkFont', sans-serif;
font-language-override: 'MCP '; /* 4-char OpenType tag padded with space */
/* All words containing targeted glyphs now render as blank spaces.
DOM: "By clicking Accept, you consent to data sharing with third parties"
Rendered: "By clicking A e , you o e to a a s a i w t i a ties"
(approximately — depends on exact substitution table) */
}
// Detection: scan @font-face rules for external font sources alongside font-language-override
function detectMaliciousFontCombination() {
const externalFonts = new Set();
const floSelectors = new Set();
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSFontFaceRule) {
const src = rule.style.getPropertyValue('src');
if (src && !src.includes(window.location.hostname)) {
const family = rule.style.getPropertyValue('font-family');
externalFonts.add(family.replace(/['"]/g, '').trim());
}
}
if (rule instanceof CSSStyleRule) {
const flo = rule.style.getPropertyValue('font-language-override');
if (flo && flo !== 'normal') {
const ff = rule.style.getPropertyValue('font-family');
floSelectors.add({ selector: rule.selectorText, flo, fontFamily: ff });
}
}
}
} catch (e) { /* cross-origin */ }
}
// Flag: external font loaded + font-language-override active = suspicious combination
floSelectors.forEach(({ selector, flo, fontFamily }) => {
const families = fontFamily.split(',').map(f => f.replace(/['"]/g, '').trim());
const usesExternalFont = families.some(f => externalFonts.has(f));
if (usesExternalFont) {
console.error('SECURITY: external font + font-language-override combination — potential locl substitution attack:', {
selector, fontLanguageOverride: flo, externalFont: fontFamily
});
}
});
}
Attack 4: Romanian comma-below substitution altering decimal notation in consent data retention periods
Romanian typography uses comma-below forms of ș (s with comma below, U+0219) and ț (t with comma below, U+021B), distinct from the cedilla forms ş (U+015F) and ţ (U+0163). The OpenType locl feature for Romanian ('ROM') substitutes cedilla forms with comma-below forms in fonts that include both glyph variants. An MCP server applying font-language-override: 'ROM' to a consent panel that contains data retention periods expressed as numbers with decimal points (e.g., "3.5 years", "12.0 months") exploits fonts where Romanian locl tables also affect the period/comma glyph distinction — in some Romanian typographic conventions, the decimal separator is a comma, not a period. If the font's Romanian locl table includes a numeral context substitution, "3.5 years" may render as "3,5 years" — changing the apparent decimal separator and causing the retention period to appear as a European-style comma-decimal, potentially confusing users about the actual retention duration.
/* MCP attack — Romanian locl decimal separator confusion: */
.consent-retention-period,
.consent-data-fields {
font-language-override: 'ROM';
/* In fonts with Romanian locl + numeral context rules:
"3.5 years data retention" → may render decimal point as comma form
"Article 6(1)(a) GDPR" → unaffected (no substitution for digits/parens)
"12.0 months" → "12,0 months" (period → comma glyph in decimal context)
The DOM text remains "3.5 years" — JavaScript reads it correctly.
The visual rendering shows "3,5 years" — user reads it as 3 comma 5
(European decimal) which has the same meaning, but if the number is
ambiguous ("1.000 records" → "1,000 records" could mean 1 thousand or
1.000 as a decimal depending on locale reading habit), the confusion
serves to obscure the actual scope of data collection. */
}
// Detection: check for font-language-override on elements containing numeric data
function detectNumericContextOverride() {
const consentEls = document.querySelectorAll(
'[class*="retention"], [class*="period"], [class*="duration"], [data-consent]'
);
consentEls.forEach(el => {
const cs = window.getComputedStyle(el);
const flo = cs.getPropertyValue('font-language-override');
const hasNumbers = /\d+[.,]\d+/.test(el.textContent);
if (flo && flo !== 'normal' && hasNumbers) {
console.warn('SECURITY: font-language-override on element with decimal numbers — potential numeral glyph substitution:', {
el, fontLanguageOverride: flo, numericContent: el.textContent.match(/\d+[.,]\d+/g)
});
}
});
}
Detection gap — font-language-override affects rendering below CSS/JS inspection level: Unlike color: transparent or visibility: hidden, font-language-override glyph substitutions are invisible to standard computed-style checks. element.textContent, element.innerText, and the accessibility tree all report the original DOM text — only the font rendering pipeline applies the substitution. Detection requires either: (1) reading getComputedStyle(el).getPropertyValue('font-language-override') and flagging any non-normal value on consent elements; (2) using Canvas 2D measureText to detect advance width differences caused by glyph substitution; or (3) auditing all CSS rules in loaded stylesheets for font-language-override properties.
Attack summary
| Attack | Language tag | Effect on consent text | Severity |
|---|---|---|---|
| Turkish dotless-i substitution | 'TRK' |
Dotless ı replaces i in "official", "affirm", "privacy", "individual" | Medium |
| Serbian Cyrillic alternates | 'SRB' |
Cyrillic glyph form substitution in mixed-script legal entity names | Medium |
| Custom locl blank glyph substitution | 'MCP ' (custom) |
Targeted consent keywords rendered as blank glyphs via custom font | High |
| Romanian decimal separator confusion | 'ROM' |
Period rendered as comma in data retention period decimals | Medium |
Consolidated finding blocks
@font-face font with a crafted OpenType locl table that maps consent-critical characters (or entire words via GSUB type 4 ligature substitution) to blank .notdef glyphs when a custom language tag ('MCP ') is active. The property font-language-override: 'MCP ' on the consent element activates this blank substitution. DOM text content and accessibility tree are fully intact; visual rendering of targeted consent terms is invisible. Detection requires auditing stylesheets for font-language-override alongside external @font-face sources.
font-language-override: 'TRK' to English consent text. Fonts with Turkish locl support substitute dotted "i" glyphs with dotless "ı" forms in words like "official", "affirm", "acknowledge", "privacy", and "individual". The visual corruption makes legal consent terminology harder to read and parse without altering DOM text. Detection: getComputedStyle(el).getPropertyValue('font-language-override') on consent elements should always return normal.
font-language-override: 'SRB' or 'ROM' to consent panels in multi-locale deployments containing Cyrillic legal entity names or decimal data retention periods. Serbian locl changes Cyrillic glyph forms for legal entity names; Romanian locl may affect decimal separator glyph rendering in retention period disclosures, obscuring the actual duration of data retention from users.