Security reference · CSS injection · text-combine-upright · Vertical writing mode · Readability attack

MCP server CSS text-combine-upright consent security — vertical writing mode readability attacks

CSS text-combine-upright controls how characters are combined within a vertical text layout. In writing-mode: vertical-lr or vertical-rl, text flows vertically — characters are rotated 90 degrees by default. text-combine-upright: all compresses runs of characters back to a horizontal orientation within the vertical flow, fitting multiple characters into a single em-box. Applied aggressively to consent text, this produces a narrow vertical column where each position contains a horizontally compressed cluster of characters — technically present in the DOM with correct color and size, but rendered in a layout that English readers cannot parse. Three SA-CSS-TCU patterns cover whole-text stacking, digit compression in step numbers, and text-orientation: upright single-character column attacks.

text-combine-upright attack surface overview

Attack ID Technique Effect on consent Audit blind spot
SA-CSS-TCU-001 writing-mode: vertical-lr + text-combine-upright: all on consent text Each run of characters compressed into a single em-box in a vertical column — entire consent paragraph rendered as a narrow vertical strip of compressed horizontal text clusters, unreadable color, opacity, visibility, font-size all correct; writing-mode and text-combine-upright are not checked by standard consent auditors; text is present in DOM and accessible to screen readers
SA-CSS-TCU-002 text-combine-upright: digits 3 on step/item numbers in consent within a vertical-mode container Step numbers in consent lists rendered as compressed 3-digit horizontal sequences within vertical flow — surrounding consent words misalign and overlap, making numbered consent items unreadable text-combine-upright:digits is a legitimate CJK feature for mixing Roman numerals in vertical text; its misapplication in an English consent context is not audited
SA-CSS-TCU-003 writing-mode: vertical-lr + text-orientation: upright on consent text Each character individually upright and stacked in a single-character-width vertical column — "You agree" renders as 8 individual characters stacked top-to-bottom, each taking one line, readable only by rotating one's head 90 degrees and reading character by character text-orientation:upright is correct behavior for CJK ideographic characters in vertical text; applied to Latin text in consent, it produces a stacked column that no English reader will parse as consent language

Readability attack class: unlike opacity or visibility attacks which hide consent entirely, vertical writing mode attacks present consent text that appears to be present. The element is visible, the text has color, the bounding box occupies space. A quick visual scan by an auditor sees text content in approximately the right area. Only a careful reader who notices the text is arranged in a thin vertical column — and attempts to read the column from top to bottom — will notice that the consent is unreadable.

Background: writing-mode, text-orientation, and text-combine-upright

Three CSS properties interact in vertical text layouts:

These properties were designed for East Asian typography — Japanese novels set in vertical-rl mode, Korean signage with mixed numerics, etc. Their application to Latin-script consent text in Western UI contexts produces layouts that are functionally illegible to the target user population.

Attack 1: writing-mode + text-combine-upright: all (SA-CSS-TCU-001)

Setting writing-mode: vertical-lr and text-combine-upright: all on a consent element forces the entire text content into a vertical layout where each character cluster is compressed horizontally into a single em-box and stacked vertically. The visual result is a narrow column, approximately one character wide, containing compressed horizontal strings of characters.

/* SA-CSS-TCU-001: vertical writing mode + text-combine-upright:all on consent */

.consent-text {
  writing-mode: vertical-lr;
  text-combine-upright: all;
  /* text-combine-upright:all compresses ALL characters into horizontal units
     within the vertical flow — unlike the digits-only default.
     This is non-standard (spec defines 'none', 'all', 'digits') but
     Chrome and Firefox both support 'all'. */

  /* Visual result for "By installing this server you authorize read access":
     Each word or multi-character run is compressed into a single em-box height.
     The element renders as a narrow column ~1 character wide.
     The compressed horizontal strings within each em-box are unreadable at
     any typical font size because the characters are scaled to fit
     multiple glyphs within a 1em × 1em square. */

  /* Property audit of this element:
     color:       #1a1a1a  ✓ legible
     opacity:     1        ✓ fully opaque
     visibility:  visible  ✓ visible
     font-size:   14px     ✓ readable
     display:     block    ✓ displayed
     writing-mode: vertical-lr → not checked by standard consent auditors */
}

/* The element also occupies a very small horizontal footprint (1 character wide)
   which may cause it to overflow or clip if the container is not very tall.
   This can be combined with overflow:hidden on the parent to clip the column
   to less than its full height, further reducing the visible consent content. */

SA-CSS-TCU-001 (High). SkillAudit detects this by checking getComputedStyle(el).writingMode and getComputedStyle(el).textCombineUpright on all elements within consent containers. Any non-horizontal writing mode on a consent-text element (where the element's language is detected as Latin-script) is flagged as high severity. The language detection uses the element's lang attribute, the document language, or Unicode character class analysis of the text content.

Attack 2: text-combine-upright: digits 3 on numbered consent items (SA-CSS-TCU-002)

text-combine-upright: digits N compresses runs of N or fewer consecutive decimal digits into a horizontal unit within vertical text. This is designed for mixing dates and percentages into vertical CJK text — e.g., a Japanese article setting "2024年12月" in vertical mode where "12" should appear horizontal. In an English consent list with numbered items (e.g., "1. You authorize filesystem access"), applying text-combine-upright: digits 2 or digits 3 in a vertical-mode container compresses the numbers while leaving the surrounding words to wrap and overlap, creating a garbled layout.

/* SA-CSS-TCU-002: text-combine-upright:digits misaligns numbered consent items */

.install-consent-list {
  writing-mode: vertical-lr;  /* or applied by a parent */
}

.install-consent-list li {
  text-combine-upright: digits 3;
  /* Compresses runs of up to 3 digits into horizontal units.
     Effect on "1. Read access to your filesystem":
     - "1" compressed into a horizontal unit at list position
     - remaining text flows vertically with default text-orientation:mixed
       (Latin characters rotated 90 degrees, lying on their sides)
     The number and the text following it are in different orientations
     and different flow modes within the same line — they overlap and misalign.
     The resulting visual is a jumble of rotated and compressed characters. */
}

/* Combined with a small container width:
.install-consent-list {
  writing-mode: vertical-lr;
  width: 16px;   /* Only 1 character wide */
  overflow: hidden;
}
/* The consent list overflows its container and is clipped — only the
   compressed digit portion is visible, the word text is hidden. */

Attack 3: text-orientation: upright single-character column (SA-CSS-TCU-003)

text-orientation: upright within a vertical writing mode keeps each Latin character at its natural upright orientation (not rotated 90 degrees) but renders them stacked individually in a vertical column. A typical Latin character in vertical-lr mode with text-orientation:mixed lies on its side, which at least groups characters visually into word-shaped clusters. With text-orientation:upright, each character is a distinct upright glyph stacked above the next — the word "agree" becomes five separate upright characters stacked in a column one character wide. This is the vertical equivalent of writing consent with one letter per line in a word document.

/* SA-CSS-TCU-003: text-orientation:upright — single-character stack */

.consent-text {
  writing-mode: vertical-lr;
  text-orientation: upright;
  /* Each Latin character is:
     - At its natural upright orientation (not rotated)
     - Stacked vertically — one character per "line"
     - In a column approximately 1 character wide

     "You agree to" (13 chars including spaces) renders as a column
     13+ characters tall, 1 character wide.
     A full consent sentence of 80 characters would be an 80-line-tall
     single-character-wide column.

     Reading this requires:
     - Noticing the text flows downward in a single column
     - Reading each character individually, top to bottom
     - Mentally assembling the characters into words and sentences
     English-trained readers cannot do this fluently for a paragraph of text. */
}

/* Variant: combined with a width-limited parent to clip the column:
.consent-wrapper {
  writing-mode: vertical-lr;
  text-orientation: upright;
  height: 40px;    /* Shows only ~3 characters of the column */
  overflow: hidden;
}
/* Only the first 3 characters of consent are visible.
   User sees: Y / o / u — no consent content. */

Detection algorithm

/* Detection for SA-CSS-TCU patterns */
function detectWritingModeConsentAttacks() {
  const CONSENT_KEYWORDS = ['authorize', 'agree', 'terms', 'consent', 'permission', 'grant'];
  const VERTICAL_WRITING_MODES = ['vertical-lr', 'vertical-rl', 'sideways-lr', 'sideways-rl'];
  const findings = [];

  document.querySelectorAll('*').forEach(el => {
    const text = (el.textContent || '').toLowerCase();
    if (!CONSENT_KEYWORDS.some(k => text.includes(k))) return;
    if ((el.textContent || '').trim().length < 20) return;

    const cs = getComputedStyle(el);
    const writingMode = cs.writingMode;

    if (!VERTICAL_WRITING_MODES.includes(writingMode)) return;

    // Consent element is in a vertical writing mode — flag it
    const textCombine = cs.textCombineUpright;
    const textOrientation = cs.textOrientation;

    // Determine the attack pattern
    if (textCombine && textCombine !== 'none') {
      findings.push({
        id: textCombine.includes('digits') ? 'SA-CSS-TCU-002' : 'SA-CSS-TCU-001',
        severity: 'HIGH',
        element: el,
        writingMode,
        textCombineUpright: textCombine,
        desc: `Consent element in vertical writing mode (${writingMode}) with text-combine-upright:${textCombine} — text compressed into illegible layout`
      });
    } else if (textOrientation === 'upright') {
      findings.push({
        id: 'SA-CSS-TCU-003',
        severity: 'HIGH',
        element: el,
        writingMode,
        textOrientation,
        desc: `Consent element in vertical writing mode (${writingMode}) with text-orientation:upright — characters individually stacked in single-char-width column`
      });
    } else {
      // Any vertical writing mode on consent text is suspicious
      findings.push({
        id: 'SA-CSS-TCU-VWM',
        severity: 'MEDIUM',
        element: el,
        writingMode,
        desc: `Consent element in vertical writing mode (${writingMode}) — Latin consent text illegible in vertical layout to English readers`
      });
    }
  });

  // Also check for ancestors with vertical writing mode that affect consent children
  document.querySelectorAll('*').forEach(ancestor => {
    const cs = getComputedStyle(ancestor);
    if (!VERTICAL_WRITING_MODES.includes(cs.writingMode)) return;

    ancestor.querySelectorAll('*').forEach(child => {
      const text = (child.textContent || '').toLowerCase();
      if (CONSENT_KEYWORDS.some(k => text.includes(k)) && child.textContent.trim().length >= 20) {
        const childCs = getComputedStyle(child);
        // If child doesn't override writing-mode back to horizontal, it inherits vertical
        if (VERTICAL_WRITING_MODES.includes(childCs.writingMode)) {
          findings.push({
            id: 'SA-CSS-TCU-INH',
            severity: 'MEDIUM',
            element: child,
            ancestor,
            writingMode: childCs.writingMode,
            desc: `Consent child element inherits vertical writing mode from ancestor — consent text in illegible vertical layout`
          });
        }
      }
    });
  });

  return findings;
}

SkillAudit findings for SA-CSS-TCU

HIGH SA-CSS-TCU-001: writing-mode: vertical-lr/rl + text-combine-upright: all on consent element — entire consent text compressed into narrow vertical column of horizontal character clusters; unreadable to English readers.
HIGH SA-CSS-TCU-002: text-combine-upright: digits N in a vertical writing mode container on numbered consent items — digit compression misaligns surrounding Latin text, producing garbled overlapping layout.
HIGH SA-CSS-TCU-003: writing-mode: vertical-lr/rl + text-orientation: upright on consent element — characters individually upright in a single-character-width column; consent requires character-by-character vertical reading that English readers cannot perform fluently.
MEDIUM SA-CSS-TCU-VWM: Any vertical writing mode (vertical-lr, vertical-rl, sideways-lr) on a consent-text element with Latin-script content — writing mode is not appropriate for English consent and makes the text harder to read.

For related CSS readability attacks that keep text technically present but visually unreadable, see SA-CSS-BDF (backdrop-filter blur) and SA-CSS-LS (letter-spacing). To audit your MCP server for SA-CSS-TCU writing mode patterns, run a free SkillAudit scan.