MCP Security Reference

MCP server CSS hyphenate-character security

The CSS hyphenate-character property (CSS Text Module Level 4) sets the string inserted at automatic hyphenation break points when hyphens: auto is active. MCP servers set this to Unicode look-alikes, directional control characters, or multi-character strings that alter the visual meaning of consent words at line-break boundaries. The textContent of the element is unaffected — only the rendered glyph at the break point changes. Standard text-content consent checkers miss the attack entirely.

Attack findings

HIGHSA-CSS-HYPC-001 — hyphenate-character:"–" (en dash) at word breaks; consent reads "un-install" on-screen but "install" in textContent; visual meaning inverted at break point
HIGHSA-CSS-HYPC-002 — hyphenate-character:"‮" (RIGHT-TO-LEFT OVERRIDE) at break; characters after break rendered right-to-left, reversing word fragment meaning; textContent unaffected
HIGHSA-CSS-HYPC-003 — hyphenate-character multi-character string " not" inserted at "authorize" break → "author­ize" in DOM; displayed as "author- not" at break; "authorize" reversed in meaning
MEDIUMSA-CSS-HYPC-004 — JS mousedown sets hyphenate-character on consent element to Unicode directional mark; attack fires at install click; soft-hyphens (­) must already exist in text

Background: how hyphenate-character works

When hyphens: auto is set, the browser identifies valid hyphenation points within words (using the document language's hyphenation dictionary or soft-hyphen ­ characters explicitly in the text) and inserts the hyphenate-character string at break points when a line wraps. The default is the hyphen-minus character (U+002D). CSS Text Module Level 4 allows any string value. The property was designed for languages that use a different break character (e.g., a double hyphen in some German typographic conventions), but accepts arbitrary Unicode.

Prerequisite: hyphenate-character has no effect unless hyphens: auto (or hyphens: manual with soft-hyphen characters) is also set. MCP servers that exploit this property always pair it with hyphens: auto and narrow container widths that force frequent line breaks.

Attack 1 — en dash look-alike substitution at consent word break (SA-CSS-HYPC-001)

The standard hyphen-minus (-, U+002D) and the en dash (, U+2013) look nearly identical in most fonts at small sizes. An MCP server sets hyphenate-character: "\2013". The consent text contains the word "uninstall" with a soft-hyphen after "un": un­install. When the container is narrow enough to break at that point, the browser renders "un–" at end of line and "install" at start of the next. The visual rendering is "un–install" — which reads as an en-dash negation — but el.textContent returns the original string without the hyphenate-character insertion. A consent checker reading textContent sees "uninstall". A user sees "un–install".

/* Attack: en dash look-alike at hyphenation break changes consent meaning */
.consent-text {
  hyphens: auto;
  hyphenate-character: "\2013"; /* U+2013 EN DASH — looks like minus but wider */
  width: 180px; /* narrow container forces break at soft-hyphen */
}
/* DOM: "By clicking install, you agree to un­install all monitoring tools..." */
/* Visual: "By clicking install, you agree to un–" / "install all monitoring tools..." */
/* textContent: "By clicking install, you agree to uninstall all monitoring tools..." */

SA-CSS-HYPC-001 (High). textContent reports original consent string without hyphenate-character insertions. Visual rendering at line breaks differs. Standard text-content consent auditors miss the meaning alteration entirely. Detection: check getComputedStyle(el).hyphenateCharacter for non-default values (anything other than 'auto' or '-'); flag if non-ASCII characters or multi-character strings are present.

/* Detection */
function checkHyphenateCharacter(el) {
  const cs = getComputedStyle(el);
  const hyphens = cs.hyphens;
  const hchar = cs.hyphenateCharacter;
  if (!hyphens || hyphens === 'none') return null;
  if (!hchar || hchar === 'auto') return null;
  // flag any non-standard hyphenate-character
  // default is '-' or implementation-defined; any non-ASCII or multi-char is suspicious
  const isDefault = /^["']?-["']?$/.test(hchar.trim());
  if (!isDefault) {
    const codePoints = [...hchar.replace(/["']/g, '')].map(c => c.codePointAt(0));
    const hasNonASCII = codePoints.some(cp => cp > 127);
    const isMultiChar = codePoints.length > 1;
    if (hasNonASCII || isMultiChar) {
      return { vuln: 'SA-CSS-HYPC-001', detail: `hyphenateCharacter:${hchar}, hyphens:${hyphens}` };
    }
  }
  return null;
}

Attack 2 — RIGHT-TO-LEFT OVERRIDE directional mark injection (SA-CSS-HYPC-002)

Unicode U+202E (RIGHT-TO-LEFT OVERRIDE) is a bidirectional control character that reverses the rendering direction of all following characters until the corresponding PDF character (U+202C). When inserted as the hyphenate-character, it is placed at the line-break point. All characters after the break on the same line are rendered right-to-left. In a consent sentence like "you agree to grant-access", the fragment "access" at the start of the wrapped line is rendered as "ssecca" — reversed and unreadable. The attack exploits both the directional override and the line-break timing.

/* Attack: RIGHT-TO-LEFT OVERRIDE at break reverses next line fragment */
.consent-text {
  hyphens: manual; /* uses explicit soft-hyphens in DOM */
  hyphenate-character: "\202E"; /* U+202E RIGHT-TO-LEFT OVERRIDE */
  /* soft-hyphen is placed in DOM text before "grant": "grant-­access" */
}
/* Visual at break: "grant-‮access" → "ssecca" rendered right-to-left */
/* textContent: "grant-access" — no override character visible to scanner */

SA-CSS-HYPC-002 (High). Bidirectional control characters as hyphenate-character values can reverse rendering of the word following a line break. Not visible in textContent. Browser renders U+202E insertion only at actual hyphenation break points — static rendering without line-break simulation may not show the effect. Detection: flag hyphenateCharacter values containing U+200B–U+206F or U+2028–U+202F range (Unicode directional and special formatting characters).

Attack 3 — multi-character string " not" inserted at authorization break (SA-CSS-HYPC-003)

CSS Text Level 4 allows hyphenate-character to be a string of multiple characters. An MCP server encodes the word "authorize" with a soft-hyphen after "author": author­ize. The container is sized so this break fires. The hyphenate-character is set to the string " not" (space + "not"). At the break, the browser renders "author not" at end of line and "ize" at start of next. The consent "you authorize this server" becomes "you author not" / "ize this server". The word "authorize" is fragmented into a phrase that contains its opposite. The DOM text node contains the original string; only the rendered output is corrupted.

/* Attack: multi-character hyphenate-character reverses consent authorization */
.consent-text {
  hyphens: manual;
  hyphenate-character: " not";  /* string inserted at break point */
  width: 200px;  /* forces break at soft-hyphen in "author­ize" */
}
/* DOM text: "By proceeding you author­ize this server to read your files." */
/* Rendered: "By proceeding you author not" / "ize this server to read your files." */
/* textContent: "By proceeding you authorize this server to read your files." */

Attack 4 — JS mousedown hyphenate-character injection (SA-CSS-HYPC-004)

Soft-hyphens encoded in the consent text node (­ characters) are inert when hyphens: manual is set but hyphenate-character is the default. An MCP server pre-encodes soft-hyphens at strategic positions in the consent text at page load — in positions that look like legitimate hyphenation hints. At the mousedown event on the install button, JS sets hyphenate-character to the attack string and ensures the container width is narrow enough to trigger breaks at those positions. The pre-encoded soft-hyphens become active attack vectors. Static CSS analysis at page load sees only a blank hyphenate-character (default) and correctly-encoded consent text.

/* Attack: soft-hyphens pre-encoded in consent text; hyphenate-character set at click */
/* Page load: DOM text contains "author­ize" (looks innocent — standard hyphenation hint) */
/* hyphenate-character: auto (default — break renders as standard hyphen) */

installBtn.addEventListener('mousedown', () => {
  consentEl.style.hyphens = 'manual';
  consentEl.style.hyphenateCharacter = ' not';
  consentEl.parentElement.style.width = '200px'; /* force narrow container */
});

/* Detection: MutationObserver on consent element style attribute */
new MutationObserver(() => {
  const cs = getComputedStyle(consentEl);
  if (cs.hyphens !== 'none' && cs.hyphenateCharacter && cs.hyphenateCharacter !== 'auto') {
    flagTampering('SA-CSS-HYPC-004');
    installBtn.disabled = true;
  }
}).observe(consentEl, { attributes: true, attributeFilter: ['style'] });

SkillAudit detection: SkillAudit flags any non-default hyphenateCharacter value — especially non-ASCII, multi-character strings, or Unicode directional/formatting characters — when paired with hyphens:auto or hyphens:manual. It also checks for soft-hyphen characters (­) in consent text that could become active attack vectors if hyphenate-character is injected at runtime. Run a free audit →

Detection summary

Attack IDProperties involvedKey detection signal
SA-CSS-HYPC-001hyphens:auto + hyphenate-character:"–" (en dash look-alike)hyphenateCharacter is non-default, contains non-ASCII character
SA-CSS-HYPC-002hyphens:manual + hyphenate-character:"‮" (RTL override)hyphenateCharacter contains Unicode bidirectional control character (U+202E range)
SA-CSS-HYPC-003hyphens:manual + hyphenate-character:" not" (multi-char string)hyphenateCharacter is multi-character string (length > 1 after quote removal)
SA-CSS-HYPC-004JS mousedown sets hyphens:manual + hyphenate-character + narrow width on pre-soft-hyphenated textMutationObserver style change → hyphenateCharacter check; also check textContent for ­