Security Guide

MCP server CSS writing-mode: vertical-rl security — rotating consent text 90° in narrow containers to reduce reading comprehension

CSS writing-mode: vertical-rl rotates text to run top-to-bottom right-to-left. For horizontal-script readers, vertical reading speed is approximately 30% of horizontal. An MCP server sets writing-mode: vertical-rl on a consent disclosure container constrained to 20–30px width — each line of consent text becomes a narrow vertical strip showing 2-3 characters. Combined with text-orientation: sideways, English text is fully rotated 90°. Font-size, color, visibility, and display are unchanged — the attack is spatial and ergonomic. Property-based scanners report the consent element as fully accessible.

writing-mode: vertical-rl and its interaction with container width

The writing-mode property changes the direction in which text flows and how block and inline directions are mapped:

/* writing-mode values and their text flow directions */

writing-mode: horizontal-tb;   /* default: left-to-right, top-to-bottom */
writing-mode: vertical-rl;     /* vertical: top-to-bottom, columns right-to-left */
writing-mode: vertical-lr;     /* vertical: top-to-bottom, columns left-to-right */

/* Key effect on layout dimensions:
   In horizontal-tb (default):
   - The INLINE direction is horizontal (left-to-right for LTR)
   - The BLOCK direction is vertical (top-to-bottom)
   - A paragraph's WIDTH constrains the inline direction (line length)
   - A paragraph's HEIGHT grows with content (block direction)

   In vertical-rl:
   - The INLINE direction is VERTICAL (top-to-bottom)
   - The BLOCK direction is HORIZONTAL (right-to-left for vertical-rl)
   - A container's HEIGHT constrains the inline direction (line "length" = height)
   - A container's WIDTH grows with content (block direction)

   CRITICAL CONSEQUENCE:
   In vertical-rl, the container's WIDTH determines how many "lines" fit.
   Each "line" is a vertical strip as wide as one character.
   A 20px-wide container at 16px font-size fits approximately ONE character per strip.
   The text wraps to a new strip (moving left) when the height is exhausted. */

/* text-orientation values (only relevant for vertical writing modes) */
text-orientation: mixed;     /* default: upright CJK, sideways Latin */
text-orientation: upright;   /* all characters upright (rotated to face up) */
text-orientation: sideways;  /* all characters rotated 90° clockwise */

/* For English/Latin consent text in vertical-rl:
   text-orientation: mixed    → each Latin letter is rotated 90° (default for vertical-rl)
   text-orientation: sideways → same for Latin (explicit, not functionally different for Latin)
   text-orientation: upright  → each Latin letter is upright but stack vertically (letter by letter)
   → 'upright' is actually HARDER to read for Latin text (disconnected letters) */

Ergonomic attack, not property attack: writing-mode: vertical-rl does not change font-size, color, opacity, visibility, display, or overflow. All the properties that standard CSS security scanners check for consent element accessibility are unchanged. The attack is entirely about spatial orientation — consent text is present, rendered, and technically readable, but requires a ~70% slower reading mode for horizontal-script readers.

Attack 1: writing-mode:vertical-rl in a 20px-wide container — 2-3 characters per "line"

The primary attack constrains the container width to force a very narrow vertical column of consent text:

/* MCP INJECTION: vertical consent text in 20px-wide strip */
.consent-disclosure {
  writing-mode: vertical-rl;
  text-orientation: sideways;  /* all Latin characters rotated 90° clockwise */
  width: 20px;                 /* forces ~1 character per vertical strip */
  height: 200px;               /* visible height — consent text visible in this region */
  overflow: hidden;            /* clips text beyond 200px height */
}

/* LAYOUT RESULT:
   Font-size: 16px (unchanged). Characters are 8-10px wide in horizontal mode.
   In vertical-rl, the font's inline metrics apply to the vertical direction:
   each character's "line height" (now in the vertical axis) is ~16px × 1.5 = 24px.
   Container HEIGHT: 200px → approximately 8-9 characters per vertical strip.
   Container WIDTH: 20px → approximately 1 strip (1 character-column width ≈ 16-20px).

   The consent disclosure: "By clicking Accept, you grant SkillAudit access to..."
   Rendered as a single vertical strip:
   B
   y
   (space)
   c
   l
   i
   c
   k (clipped here — 200px = 8 lines at 24px each)
   ...rest of consent invisible

   With overflow:hidden, only the first ~8 characters of the consent are visible.
   The entire consent is in a 20px × 200px visible window.
   The consent "line" would need to be ~1200px tall to show 50 characters of consent text.
   A 200px container shows ~8 characters. A 200-word consent = ~1200 characters → 150 strips × 20px = 3000px width.

   With overflow:hidden, the rest of the consent is off to the left (vertical-rl wraps left).
   The 3000px-wide content is hard-clipped at the 20px container boundary. */

/* Scanner checks at this point:
   font-size: 16px → passes (above threshold)
   color: #333 → passes (sufficient contrast)
   visibility: visible → passes
   display: block → passes
   opacity: 1 → passes
   height: 200px → passes (above "too small" threshold)
   writing-mode: vertical-rl → no rule in most scanners → passes

   The consent element fails readability tests that require writing-mode:horizontal-tb
   on consent-critical content for horizontal-script contexts. */

Attack 2: writing-mode:vertical-rl with height constraint — truncating tall consent at width

Inverting the constraint: the container is very narrow width with a large height, but text overflows beyond the specified height (which is now the "line length") and the width dimension holds only one strip:

/* ATTACK VARIANT: width constraint truncates vertical strips */

.consent-body {
  writing-mode: vertical-rl;
  width: 24px;           /* narrow — in vertical-rl, this is the BLOCK direction */
  height: 400px;         /* vertical — in vertical-rl, this is the INLINE direction (line length) */
  overflow: hidden;      /* clips content exceeding the block direction (width) */
}

/* In vertical-rl:
   INLINE direction = vertical (constrained by 'height' property → 400px per strip)
   BLOCK direction  = horizontal, right-to-left (constrained by 'width' property → 24px)

   A 400px height allows ~25 characters per strip at 16px font-size.
   The text begins on the RIGHT side of the container.
   First strip: right column, 0px to 24px wide, characters 1-25 top-to-bottom.
   Second strip: would start at 24px from the right edge (i.e., left of first strip).
   Container width: 24px. The second strip would need x-position: -24px (outside container).
   overflow:hidden clips the second strip and all subsequent strips.

   RESULT: Only the FIRST 25 characters of the consent are visible.
   The rest (potentially hundreds of characters) is clipped by the 24px container width.

   The user sees a 24px × 400px strip with 25 vertical characters:
   "By clicking Accept, you " (25 chars — cut off before any meaningful disclosure) */

/* getComputedStyle(consentBody).writingMode → "vertical-rl"
   getComputedStyle(consentBody).overflow    → "hidden"
   clientWidth: 24, scrollWidth: 3200 (full horizontal extent of all strips)
   → scrollWidth >> clientWidth → indicates truncation
   BUT: scrollWidth in vertical-rl measures the full horizontal extent of wrapped strips
   AND: some browsers may compute scrollWidth differently for vertical writing modes
   → scrollWidth check may give inconsistent results across browsers for vertical-rl */

Attack 3: writing-mode:vertical-rl + max-height on container (now limits "line length")

In vertical-rl, height and max-height constrain the inline direction (line length). A small max-height creates very short vertical strips where each "line" is only a few characters tall:

/* max-height in vertical-rl constrains line length */

.consent-panel {
  writing-mode: vertical-rl;
  max-height: 40px;   /* in vertical-rl: constrains INLINE direction = vertical */
  overflow: hidden;   /* clips inline overflow (text beyond 40px height per strip) */
  /* No width constraint — block direction grows naturally with number of strips */
}

/* LAYOUT:
   max-height: 40px → each vertical strip is at most 40px tall
   At 16px font-size with 1.5 line-height: 40px / 24px ≈ 1-2 characters per strip.
   Each strip is ~20px wide (1 character-width).
   Consent "By clicking..." → each character gets its own 20px × 40px strip.
   Display looks like scattered vertical character columns, 20px each, extending right-to-left.

   The layout is extremely unusual visually — characters appear as isolated columns.
   A viewer seeing this UI might interpret it as: broken text rendering, a CJK font issue,
   decorative styling, or a loading placeholder.
   In any case, it is not readable as English prose consent text.

   overflow:hidden clips vertical overflow (characters beyond 40px in height).
   For 2 characters per strip, each strip shows 2 characters.
   A 200-character consent = 100 strips × 20px = 2000px horizontal extent.
   If the container has max-width or is constrained by a parent, most strips are invisible. */

/* Combined with parent overflow:hidden at smaller width:
   .parent { width: 200px; overflow: hidden; }
   .consent-panel { writing-mode: vertical-rl; max-height: 40px; }
   → Only the rightmost 10 strips (200px / 20px) are visible → 20 characters of consent */

Attack 4: writing-mode:vertical-rl targeted at closing sentences — last paragraphs inaccessible

Consent text typically has the most critical disclosures in the final sentences. In vertical-rl, text flows top-to-bottom and wraps to the LEFT. A width constraint clips the leftward-flowing overflow — targeting the END of the consent text:

/* writing-mode:vertical-rl + overflow:hidden — end of consent clipped */

/* In vertical-rl, content flows:
   - Inline (vertical): top → bottom (within a strip)
   - Block (horizontal): right → left (new strips added to the left)

   The BEGINNING of the text is in the RIGHTMOST strip.
   The END of the text is in the LEFTMOST strip (potentially outside the container).

   With overflow:hidden on a width-constrained container:
   Only the rightmost strips are visible — the BEGINNING of the consent text.
   The final paragraphs (typically containing the most critical disclosures
   about data sharing, permissions scope, liability limitations) are in
   the leftmost strips — clipped and invisible.

   This is the OPPOSITE of typical overflow behavior in horizontal text,
   where the END of text (bottom) is clipped — users scroll to read it.
   In vertical-rl, horizontal scroll to the LEFT is required,
   which is unintuitive for LTR-trained users who scroll DOWN not LEFT. */

.consent-container {
  writing-mode: vertical-rl;
  width: 160px;           /* shows rightmost 8 strips (~8 × 20px): first ~200 chars */
  height: 300px;          /* tall enough for ~12 chars per strip */
  overflow: hidden;       /* clips leftward overflow (end of consent text) */
}

/* A 500-character consent disclosure:
   Chars 1-200:   visible (rightmost 8 strips)
   Chars 200-500: invisible (clipped to the left by overflow:hidden)

   The critical final paragraph ("By using this tool you authorize MCP access to..."):
   - Typically begins at character 350+ in a well-structured consent
   - Is completely invisible in the 160px × 300px container
   - Requires horizontal scrolling LEFT — unintuitive for LTR users
   - No scroll indicator appears if the container has overflow:hidden (not overflow:auto) */

Summary table

Attack Mechanism Scanner detection gap Severity
vertical-rl + 20px container — 2-3 chars per strip Consent text rendered as 2-3 character vertical strips; ~70% slower reading speed font-size/color/visibility unchanged; writing-mode check missing in most scanners HIGH
vertical-rl + overflow:hidden + width=24px — only first 25 chars visible All consent beyond first ~25 characters clipped by container width in vertical mode scrollWidth check inconsistent in vertical-rl; overflow:hidden check passes at 24px (above threshold) CRITICAL
vertical-rl + max-height:40px — 1-2 chars per strip, scattered columns Consent appears as isolated character columns; visually unrecognizable as prose text Element has non-zero height and normal visual properties; writing-mode semantic check required HIGH
vertical-rl + overflow:hidden — end of consent clipped to the left Final paragraphs (critical disclosures) in leftmost strips; clipped by width; requires unintuitive leftward scroll Overflow direction reversal in vertical-rl; end-of-text is now to the left not the bottom HIGH

SkillAudit findings for CSS writing-mode: vertical-rl

CRITICAL MCP-injected writing-mode: vertical-rl on a consent-critical element with a narrow container width (<100px) that limits text to fewer than 5 character-wide strips, making consent text effectively unreadable for horizontal-script users. SkillAudit checks writing-mode on all consent-critical elements, and when vertical-rl or vertical-lr is found, evaluates the constraining dimension (width for vertical modes) for readability rather than the visual dimension.
HIGH writing-mode: vertical-rl combined with overflow: hidden on a consent container, causing the END of consent text (in leftmost strips) to be clipped while the BEGINNING (in rightmost strips) remains visible. SkillAudit detects consent truncation in vertical writing modes by checking the block-direction extent (scrollWidth in vertical-rl) against the container's constrained block-direction dimension, and flags when more than 20% of consent text is clipped in the non-scrolling direction.
HIGH writing-mode: vertical-rl with max-height less than twice the font-size, creating 1-2 character per strip columns where the consent text renders as scattered isolated character columns visually indistinguishable from decorative elements. SkillAudit flags writing-mode:vertical on consent elements where the inline-direction constraint (height or max-height in vertical-rl) is less than 3× the computed font-size.

Defences

Writing-mode semantic check: SkillAudit checks writing-mode on all consent-critical elements. Any non-horizontal-tb writing mode on a consent element triggers a context-aware readability evaluation: for horizontal-script consent text (English, French, Spanish, etc.), vertical writing modes are flagged as a readability attack vector regardless of other property values.

Block-direction truncation check for vertical writing modes: In vertical writing modes, SkillAudit re-maps which dimension constrains content visibility. For writing-mode: vertical-rl, the block direction is horizontal — SkillAudit checks scrollWidth vs clientWidth (rather than the usual scrollHeight vs clientHeight) to detect end-of-text clipping.

Characters-per-visible-area check: SkillAudit estimates the number of characters visible in the consent element's viewport region for both horizontal and vertical writing modes, flagging consent elements where fewer than 50% of the total text character count is visible in the current viewport region.

Related: CSS writing-mode (general) security · CSS text-orientation security · CSS direction:rtl consent attack · CSS fluid font-size security