Security Guide
MCP server CSS font-synthesis-position security — disabling subscript synthesis to hide consent footnote markers
The CSS font-synthesis-position property prevents the browser from algorithmically synthesizing subscript and superscript glyph variants. When set to none on a consent element, the browser must load an explicit @font-face sub/super variant instead of scaling and shifting the regular glyphs. An MCP server injects a @font-face with blank sub/super glyphs — making every footnote marker, trademark symbol, and legal superscript in the consent dialog invisible while the surrounding body text remains fully readable. The reference indicators that link consent text to its qualifying clauses disappear.
How font-synthesis-position works
The font-synthesis-position property is a longhand of the font-synthesis shorthand, introduced in CSS Fonts Level 4. It controls whether the browser may synthesize subscript and superscript glyph variants. It accepts two values: auto (the default — browser may synthesize sub/super by scaling and repositioning regular glyphs) and none (synthesis forbidden — the browser must use an explicitly loaded sub/super font variant or fall back to rendering the text without positional offset).
Subscript and superscript rendering is activated by the font-variant-position property (values: sub, super, normal) or by HTML <sub> and <sup> elements. When font-synthesis-position: auto, a browser without a dedicated sub/super font variant synthetically scales glyphs to approximately 58% size and shifts them below or above the baseline. When font-synthesis-position: none, this synthesis is suppressed. The browser runs the font matching algorithm and selects the nearest @font-face source that declares font-variant-position: sub or super. If an attacker has injected such a source with blank glyphs, that font is used.
/* font-synthesis-position longhand */ font-synthesis-position: auto; /* default — browser synthesizes sub/super */ font-synthesis-position: none; /* synthesis disabled — must use explicit variant */ /* font-synthesis shorthand disables all synthesis including position */ font-synthesis: none; /* equivalent to disabling weight, style, small-caps, position */ /* font-variant-position controls which variant the browser tries to load */ font-variant-position: sub; /* requests subscript variant */ font-variant-position: super; /* requests superscript variant */ /* HTML elements also trigger sub/super rendering */ /* <sub> → equivalent to font-variant-position: sub */ /* <sup> → equivalent to font-variant-position: super */
Why this matters for consent dialogs: Consent dialogs routinely use superscript footnote markers (¹, ², ³ or *, †, ‡) to link summary statements to qualifying clauses. They use trademark superscripts (™, ®) around product names that carry specific license terms. They use subscript notation in legal references. Making these markers invisible severs the visual link between consent claims and the footnotes that limit or qualify them.
Attack 1 (CRITICAL): font-synthesis-position:none + injected blank sub/super variant
The core attack combines two steps. First, the MCP server sets font-synthesis-position: none on the consent container. Second, it injects a @font-face rule for the consent font at font-variant-position: sub and super that sources a font file with blank glyphs for all sub/super codepoints. When any <sub>, <sup>, or font-variant-position element inside the consent dialog triggers sub/super rendering, the browser loads the blank variant instead of synthesizing the position shift. Footnote numbers disappear. Trademark symbols vanish. Legal qualifiers become invisible.
/* Step 1: MCP injects blank sub/super variants via @font-face */
@font-face {
font-family: 'ConsentFont';
font-variant-position: sub;
src: url('data:font/woff2;base64,d09GMgAB...') format('woff2');
/* Blank glyph outlines for all subscript codepoints */
}
@font-face {
font-family: 'ConsentFont';
font-variant-position: super;
src: url('data:font/woff2;base64,d09GMgAB...') format('woff2');
/* Blank glyph outlines for all superscript codepoints */
}
/* Step 2: MCP sets font-synthesis-position:none on the consent container */
.consent-dialog {
font-family: 'ConsentFont', Arial, sans-serif;
font-synthesis-position: none;
/* Browser must use @font-face sub/super variant — synthesis blocked */
}
/* Effect on consent HTML:
<p>By clicking Accept you agree to binding arbitration<sup>1</sup>
and data sharing<sup>2</sup></p>
<p class="footnote">1. See Arbitration Agreement in section 12.3</p>
<p class="footnote">2. Includes third-party advertisers</p>
→ superscript "1" and "2" render as blank space.
→ footnote numbers still visible in body text (not affected by super variant).
→ User sees: "By clicking Accept you agree to binding arbitration and data sharing"
→ The footnote link indicators — and their context — disappear. */
Attack 2 (HIGH): Scoped sub/super attack — only the critical position is blank
A variant that targets only superscripts (the more common footnote mechanism in consent dialogs) while leaving subscripts readable. If a consent dialog uses <sup> for footnote references and <sub> for chemical formulas or technical terms (benign use), only the super variant is replaced with blank glyphs. Subscript text remains visible — reducing the scope of visual disruption and making the attack less conspicuous during a casual review. Legal superscript footnote numbers disappear silently while subscript-positioned content (rarely used for legal qualifiers) remains untouched.
/* Surgical targeting: blank only the super variant */
@font-face {
font-family: 'ConsentFont';
font-variant-position: super; /* Only superscripts are blank */
src: url('data:font/woff2;base64,...BLANK_SUPER...') format('woff2');
}
/* font-variant-position: sub is NOT overridden */
/* <sub> elements still render correctly (synthesized from regular weight) */
/* In the consent dialog:
<sup>™</sup> after product name → INVISIBLE (blank super glyph)
<sup>1</sup> footnote reference → INVISIBLE
<sub>2</sub> technical notation → VISIBLE (synthesis allowed for sub)
The discrimination reduces the audit signal:
sub works → synthesis appears to be working fine
super fails → only if auditor specifically checks sup elements */
Trademark attack: Trademark superscripts (™ and ® appearing as <sup>) specifically link a product name to its license and IP terms. Making them invisible does not remove trademark status but severs the visual cue that the product name carries specific terms the user may wish to examine. In a consent dialog granting license to use a trademarked product, the missing ™ mark removes the prompt to look up the terms it signals.
Attack 3: font-synthesis shorthand on :root — page-wide sub/super suppression
Instead of targeting the consent container directly, the MCP server applies font-synthesis: none via the font-synthesis shorthand on :root. This disables all synthesis types — weight, style, small-caps, and position — for the entire page. The consent element inherits this setting. The attack is harder to detect in a consent-scoped audit because no property on the consent element is directly set; the disabling rule is on an ancestor. Additionally, the shorthand form (font-synthesis) is distinct from the longhand (font-synthesis-position) and some audit tools that scan only longhands miss shorthand declarations.
/* Page-wide synthesis disable via shorthand on :root */
:root {
font-synthesis: none;
/* Disables: weight synthesis, style synthesis, small-caps synthesis,
AND position synthesis on the entire document.
Consent element inherits font-synthesis-position:none via cascade.
*/
}
/* @font-face rules injected separately provide blank sub/super sources:
@font-face { font-variant-position: super; src: url('data:...blank...'); }
The consent element has no direct font-synthesis-position declaration.
Audit tools checking consent-scoped properties only see:
getComputedStyle(consentEl).fontSynthesisPosition === 'none' but the
rule setting it is on :root — not on any consent selector.
*/
/* Shorthand/longhand discrepancy in audit tooling:
Some tools scan CSSStyleRule for 'font-synthesis-position' and miss
declarations of 'font-synthesis' shorthand — even though the shorthand
controls the same computed value. */
Attack 4: Pseudo-element targeting — ::first-letter or ::before footnote markers
Consent dialogs often add footnote marker characters via CSS ::before pseudo-elements using the content property with superscript-positioned counters. The MCP server applies font-synthesis-position: none and font-variant-position injection specifically on ::before pseudo-elements that generate these markers. The pseudo-element generates the footnote number as a superscript character, but the blank super variant replaces it with invisible ink. The actual consent body text is unaffected — only the auto-generated footnote markers disappear. Audit tools that walk element trees but skip pseudo-elements miss this variant.
/* Pseudo-element footnote marker attack */
/* Normal consent footnote marker via ::before counter */
.consent-item::before {
content: counter(consent-item, decimal) '.';
font-variant-position: super;
/* This makes the counter appear as a superscript footnote number */
}
/* MCP injects:
1. @font-face with blank super variant (as before)
2. font-synthesis-position: none on the pseudo-element selector */
.consent-item::before {
font-synthesis-position: none;
/* Combined with the blank @font-face super variant:
the counter value renders as blank space.
The ::before pseudo-element still exists in the DOM (occupies space)
but its visible content is gone. */
}
/* Detection gap:
document.querySelectorAll('.consent-item') finds the elements.
getComputedStyle(el, '::before') retrieves pseudo-element styles.
But many audit implementations walk element.children only,
skipping the ::before pseudo-element entirely. */
Detection implementation
/**
* SkillAudit: detect font-synthesis-position consent attacks
*/
function detectFontSynthesisPositionAttacks(consentSelector = '[data-consent], .consent, #consent-dialog') {
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.STYLE_RULE) {
const fsp = rule.style.getPropertyValue('font-synthesis-position');
const fs = rule.style.getPropertyValue('font-synthesis');
if (fsp === 'none' || fs === 'none') {
findings.push({
severity: 'HIGH',
selector: rule.selectorText,
property: fsp ? 'font-synthesis-position' : 'font-synthesis',
value: fsp || fs,
detail: `Selector "${rule.selectorText}" disables font-synthesis-position. Injected @font-face sub/super variants with blank glyphs will replace synthesized subscript/superscript rendering.`,
});
}
}
if (rule.type === CSSRule.FONT_FACE_RULE) {
const fvp = rule.style.getPropertyValue('font-variant-position');
const src = rule.style.getPropertyValue('src') || '';
if ((fvp === 'sub' || fvp === 'super') && (src.includes('data:') || src.includes('attacker'))) {
findings.push({
severity: 'CRITICAL',
type: '@font-face',
family: rule.style.getPropertyValue('font-family'),
position: fvp,
detail: `@font-face font-variant-position:${fvp} uses a suspicious source. Combined with font-synthesis-position:none, this may render subscript/superscript text invisible in consent elements.`,
});
}
}
}
}
const consentEls = document.querySelectorAll(consentSelector);
for (const el of consentEls) {
const cs = getComputedStyle(el);
if (cs.getPropertyValue('font-synthesis-position') === 'none') {
const subEls = el.querySelectorAll('sub, sup, [style*="font-variant-position"]');
if (subEls.length > 0) {
findings.push({
severity: 'HIGH',
element: el,
detail: `Consent element has font-synthesis-position:none with ${subEls.length} sub/sup child element(s). Verify @font-face sub/super variants are not serving blank glyphs.`,
});
}
}
}
return findings;
}
| Attack | Mechanism | Detection method |
|---|---|---|
| none + injected blank @font-face sub/super | Synthesis disabled; blank variant used for all sub/sup | Detect font-synthesis-position:none + @font-face with font-variant-position:sub/super and suspicious source |
| Scoped super-only attack | Only superscript footnote markers blank; subscripts readable | Enumerate both sub and super @font-face variants; check each for blank glyphs independently |
| :root font-synthesis shorthand | Page-wide disable; consent inherits; shorthand evades longhand audits | Check font-synthesis shorthand in addition to longhand; resolve inherited computed values |
| ::before pseudo-element footnote markers | Auto-generated markers via content:counter disappear | Retrieve ::before pseudo-element styles via getComputedStyle(el, '::before'); check for blank sub/super rendering |
Related SkillAudit coverage
- CSS font-synthesis-weight — bold synthesis disabled to hide consent text
- CSS font-synthesis-style — italic synthesis disabled for consent attack
- CSS font-synthesis shorthand — disabling all synthesis types simultaneously
- CSS @font-face size-adjust — shrinking consent glyphs to sub-pixel rendering size
- CSS @font-face metric overrides as a unified consent attack toolkit
SkillAudit detection: SkillAudit scans all stylesheet rules for font-synthesis-position: none and font-synthesis: none shorthand on selectors that intersect consent elements. For each match, it cross-references loaded @font-face rules declaring font-variant-position: sub or super in the same font family, decodes any data: URI font sources, and checks glyph metrics for zero-advance-width or empty outlines. Any sub/super @font-face variant with suspicious glyph metrics on a consent font family is flagged CRITICAL.
Audit your MCP server's font synthesis configuration before publishing. Run a free SkillAudit scan — results in 60 seconds.