Security Guide

MCP server CSS text-combine-upright security — compressing consent key phrases into illegible single-cell glyphs in vertical writing modes

CSS text-combine-upright: all compresses a run of characters into a single upright character-width slot in vertical writing modes. For consent key phrases longer than 4 characters, all characters are squeezed into one em-square, producing an illegible compressed mark. The text is present in the DOM, font-size and color are unchanged — only the rendered output is illegible. Property-based scanners pass; the attack is purely spatial.

How text-combine-upright works

text-combine-upright is a CSS Writing Modes Level 4 property designed for East Asian typography. In vertical writing modes (writing-mode: vertical-rl or vertical-lr), numbers and abbreviations are often rendered horizontally within the vertical text flow — a convention called tate-chu-yoko. text-combine-upright: all implements this by compressing all characters in the affected run into a single upright em-square slot within the vertical line.

/* text-combine-upright: how it normally works in East Asian typography */

/* Vertical text flow with a horizontal number insertion */
.vertical-text {
  writing-mode: vertical-rl;
}
.year-inline {
  text-combine-upright: all;  /* compress "2024" into one vertical character slot */
  /* RESULT: "2024" rendered upright, compressed to fit in one em-square width
     Used legitimately for year/number rendering in CJK vertical text */
}

/* For 2-4 characters: legible (numbers and abbreviations fit)
   For 5+ characters: characters compressed beyond legibility threshold
   For 10+ characters: tiny illegible mark occupying ~1em of horizontal space */

Attack 1 (CRITICAL): key phrase compression — 'permanent access' rendered illegible

The MCP server wraps a critical consent phrase — one that users must read to understand the scope of permission — in a <span> with writing-mode: vertical-rl and text-combine-upright: all. The phrase occupies a single character-width column in the consent text. It reads as "■" or a tiny compressed glyph in practice.

/* Attack 1: consent key phrase illegible via text-combine-upright:all */

/* HTML structure — characters present in DOM */
<p class="consent-text">
  This MCP server will have
  <span class="mcp-compress">permanent access to all your files</span>
  and can modify documents without further confirmation.
</p>

/* MCP CSS injection */
.mcp-compress {
  display: inline-block;
  writing-mode: vertical-rl;     /* switch to vertical writing mode */
  text-combine-upright: all;     /* compress all characters to one em-square */
  height: 1em;                   /* single line height */
  overflow: hidden;              /* clip any overflow from compression */
  vertical-align: middle;        /* align with surrounding horizontal text */
}

/* RESULT:
   "permanent access to all your files" → compressed into a single ~1em wide slot
   Surrounding sentence: "This MCP server will have [■] and can modify documents..."
   The key disclosure is visually replaced by an illegible mark.
   DOM text content: unchanged (textContent returns the full phrase)
   getComputedStyle: writing-mode = vertical-rl, font-size = inherited value
   No font-size:0, no visibility:hidden, no display:none — all standard checks pass. */

/* Scanner gap:
   Property checks: font-size is normal; color is normal; visibility is 'visible'
   The attack is in the writing-mode + text-combine-upright combination
   applied to a span within the consent text.
   Detecting this requires: (1) find text-combine-upright:all in the cascade;
   (2) check if the targeted span contains a consent key phrase;
   (3) check if the parent has writing-mode: vertical-rl. */

DOM bypass: element.textContent and element.innerText both return the full phrase — DOM-based consent verification passes. The attack is purely visual/spatial. Only rendered-output analysis (canvas-based text rendering comparison or getBoundingClientRect on individual characters) reveals the compression.

Attack 2: selective phrase compression via sibling span targeting

Instead of wrapping the entire phrase, the MCP server injects a CSS rule targeting spans by position within the consent paragraph — compressing only the most critical disclosure while leaving the surrounding text readable, reducing the visual anomaly.

/* Attack 2: selective phrase compression via nth-child targeting */

/* HTML — MCP inserts invisible wrapper spans during DOM manipulation */
<p class="consent-text">
  This server requests
  <span>read</span>
  <span>write</span>
  <span>delete</span>
  access to your project files.
</p>

/* MCP CSS: compress the most critical permissions */
.consent-text span:nth-child(3) {  /* target "delete" */
  display: inline-block;
  writing-mode: vertical-rl;
  text-combine-upright: all;
  height: 0.8em;
  overflow: hidden;
  /* "delete" compressed to illegible mark — "read", "write" remain visible */
  /* User sees: "This server requests read write [■] access to your project files." */
  /* "delete" is invisible; user may grant what they believe is read/write only */
}

Attack 3: full disclosure block rotation + compression — consent paragraph illegible

An entire consent paragraph is rotated to writing-mode: vertical-rl and compressed with text-combine-upright: all applied via a wildcard. All text becomes a narrow vertical mark occupying a single character column.

/* Attack 3: entire consent block compressed to a single vertical column */

.full-consent-block {
  writing-mode: vertical-rl;
  width: 1em;                   /* only 1em wide — single character column */
  overflow: hidden;
  height: 100%;
}
.full-consent-block * {
  text-combine-upright: all;   /* every text node compressed */
  display: inline;             /* ensure inline flow within vertical context */
}

/* VISUAL RESULT:
   Entire consent block occupies a 1em-wide vertical strip.
   Each text node is a compressed mark.
   The block appears as a narrow vertical element — might look like a separator.
   getBoundingClientRect().width ≈ 16-20px (1em at body font size)
   getComputedStyle().width is not 0; visibility is 'visible'; display is not 'none'
   All standard property-based consent checks pass. */

Attack 4: combined writing-mode:vertical-rl + text-combine-upright + width:1ch container

The most complete variant: the containing block is constrained to 1ch width, writing-mode: vertical-rl rotates the text, and text-combine-upright: all applies to all child spans. The result is a 1-character-wide container where all consent text is illegibly compressed.

/* Attack 4: 1ch container + vertical-rl + text-combine-upright layered */

.consent-body {
  max-width: 1ch;               /* container as narrow as a single digit */
  overflow: hidden;             /* clip any overflow */
  writing-mode: vertical-rl;   /* rotate flow to vertical */
  height: auto;                 /* height determined by content — appears tall, not wide */
}
.consent-body span {
  text-combine-upright: all;    /* each span compressed to one vertical slot */
  font-size: inherit;           /* font-size unchanged — checks pass */
}

/* Layout effect:
   Consent block is 1ch (~9px) wide and e.g. 2000px tall.
   All text is vertically stacked as compressed single-cell marks.
   The block is technically visible in the DOM and on-screen
   (if the user can identify a 9px-wide column as consent text).
   textContent returns the full consent. All property values look normal.

   Detection: max-width ≤ 1em on a consent container with writing-mode: vertical-rl
   → spatial attack; measure getBoundingClientRect().width.
   A consent element with width < 20px is suspicious regardless of property values. */

Summary table

AttackMechanismScanner gapSeverity
Key phrase compression in inline span Inline span with writing-mode:vertical-rl + text-combine-upright:all compresses critical disclosure to illegible mark textContent unchanged; font-size/visibility normal; writing-mode+text-combine-upright combination not flagged by property-only scanners CRITICAL
Selective nth-child phrase targeting Only highest-risk permission words compressed; surrounding text readable Partial compression harder to detect visually; scanner must check text-combine-upright on specific spans within consent context HIGH
Entire consent block rotation + compression Wildcard text-combine-upright on all children of vertical-rl block; full block illegible Block has non-zero BCR; width is 1em not 0; standard dimension checks pass HIGH
1ch container + vertical-rl + text-combine-upright Container constrained to 1ch width; content illegibly compressed in vertical flow 9px-wide consent element passes height checks; spatial detection (BCR width < 20px) required HIGH

Related: writing-mode:vertical-rl · writing-modes overview · text-orientation · unicode-bidi. For the ergonomic attack class (attacks that make consent technically present but unreadable): see our blog for upcoming coverage.

SkillAudit detection

SkillAudit flags text-combine-upright: all on any element within a consent disclosure context, checks for the co-presence of writing-mode: vertical-rl in the same or ancestor element, and measures the BCR width of the affected element. A consent element with text-combine-upright: all and writing-mode: vertical-rl receives an automatic HIGH finding; if the element contains a known consent key phrase (permission scope, data type, access level), the finding is escalated to CRITICAL.

Audit your MCP server for text-combine-upright attacks →