MCP server CSS writing-mode and direction consent security
Four SA-CSS-WMD consent attack patterns exploiting CSS writing-mode and direction properties to render consent text in physically rotated or character-reversed layouts that humans cannot process as normal consent. The text is visually present with correct contrast and non-zero pixel dimensions, but spatial orientation makes it practically unreadable. getComputedStyle checks for color, opacity, and visibility all pass; the attack lives entirely in text flow direction.
CSS writing-mode controls whether text flows horizontally or vertically and in which direction. The property was designed for CJK (Chinese/Japanese/Korean) typographic systems where vertical text is conventional. In a consent dialog context, an attacker can use writing-mode: vertical-rl to rotate English consent text 90° clockwise, so that it reads top-to-bottom instead of left-to-right. The text is physically present, has good WCAG contrast, and the element has positive dimensions — but a typical user reading a Latin-script dialog will not process 90°-rotated text as consent before clicking the Accept button below it.
SA-CSS-WMD-001: writing-mode:vertical-rl — 90° clockwise rotation
writing-mode: vertical-rl rotates the text block 90° clockwise: text flows from top to bottom, with successive lines flowing from right to left. For an English sentence like “By clicking Allow you grant this MCP server file system access,” the result is a narrow vertical column where each character is physically rotated, reading down the column. The element occupies a tall, narrow box instead of a wide, short one. Layout dimensions change, which may push UI elements around the dialog.
.consent-text {
writing-mode: vertical-rl;
/* text now flows top-to-bottom, lines right-to-left */
/* each character is upright but the reading direction is vertical */
}
/* Resulting layout:
Normal: [By clicking Allow you grant this MCP server access.]
(400px wide, 20px tall)
vertical-rl: B ← characters stacked vertically
y (each character occupies ~14px in the block direction)
c total height ≈ 40 chars × 14px = 560px
l element width ≈ 1 line × 14px = ~20px wide
...
getComputedStyle checks:
- color: #111111 → passes
- opacity: 1 → passes
- visibility: visible → passes
- writing-mode: vertical-rl → DETECTABLE if checked
Detection: check getComputedStyle(el).writingMode for any non-'horizontal-tb' value
on consent-critical text elements.
*/
Rotated text passes all color/visibility checks: writing-mode: vertical-rl does not affect color, opacity, visibility, or any property that first-generation audit engines check. The text has correct contrast. The element has positive layout dimensions. Only checking writing-mode and text-orientation properties — or computing the text's reading direction relative to the UI's primary language direction — will detect this attack.
writing-mode: vertical-rl on consent text: rotates English text 90° clockwise to vertical top-to-bottom layout; correct WCAG contrast; passes color/opacity/visibility checks. Detection: getComputedStyle(el).writingMode !== 'horizontal-tb' on consent element → CRITICAL.SA-CSS-WMD-002: writing-mode:sideways-lr — 90° counterclockwise rotation
writing-mode: sideways-lr rotates text 90° counterclockwise: characters are displayed on their side with the top pointing left. This is the mirror of vertical-rl in terms of rotation direction. For consent text, the practical effect is the same: English text cannot be read without rotating one's head 90° counterclockwise. The difference from vertical-rl is in the direction of the rotation, which a human might perceive as even less natural than the clockwise direction.
.consent-text {
writing-mode: sideways-lr;
/* text flows bottom-to-top, characters rotated 90° counterclockwise */
/* the top of each character points to the left */
}
/* writing-mode values that rotate text:
- horizontal-tb: normal horizontal left-to-right (default)
- vertical-rl: 90° clockwise rotation
- vertical-lr: 90° clockwise, lines flow left-to-right (rare)
- sideways-rl: 90° clockwise (same visual as vertical-rl for LTR text)
- sideways-lr: 90° counterclockwise
All non-horizontal-tb values should be flagged for consent text.
Browser support note:
- horizontal-tb, vertical-rl, vertical-lr: widely supported
- sideways-rl, sideways-lr: supported in Firefox, Chrome, Edge since ~2020
Scanner should flag all non-horizontal values regardless of current support. */
writing-mode: sideways-lr on consent text: rotates English text 90° counterclockwise; unreadable without head tilt; same contrast profile as normal text. Detection: any non-horizontal-tb writing-mode on consent-critical elements → CRITICAL.SA-CSS-WMD-003: direction:rtl + unicode-bidi:bidi-override — character order reversal
direction: rtl sets the base text direction to right-to-left. Combined with unicode-bidi: bidi-override, this forces all characters to be rendered in reverse order within each line, overriding the Unicode Bidirectional Algorithm. The string “By clicking Allow you grant access.” becomes “.ssecca tnarg uoy wollaA gnikkilc yB” — each character individually present but in mirror-reversed sequence. A user reading the dialog sees unfamiliar character patterns and may not recognize the reversed English as a consent statement.
.consent-text {
direction: rtl;
unicode-bidi: bidi-override;
/* Forces right-to-left rendering with override of Unicode bidi algorithm */
/* Characters render in reverse order: "Allow" → "wollaA" */
}
/* Example transformation:
Original: "By clicking Allow you grant this MCP server file system access."
Rendered: ".ssecca metsys elif revres PCM siht tnarg uoy wollaA gnikkilc yB"
Each character is visually the same (no rotation of individual glyphs).
The sequence is reversed.
A native English speaker encountering this text will not parse it as consent.
Contrast and opacity: unchanged. WCAG passes.
Font size: unchanged.
Visibility: visible.
Detection:
- getComputedStyle(el).direction === 'rtl' (detectable)
- getComputedStyle(el).unicodeBidi === 'bidi-override' (detectable)
Both together on a consent element → SA-CSS-WMD-003 CRITICAL
direction:rtl alone without bidi-override is less severe (word order reversal,
characters within words remain LTR for LTR scripts). */
direction:rtl without bidi-override: direction: rtl alone reverses the word order within the line (last word appears first) but does not reverse individual characters within each word for LTR scripts. Combined with unicode-bidi: bidi-override, individual characters are also reversed. The full combination is a more severe attack. Scanners should flag direction: rtl on its own (word order confusion) and flag the full combination (character reversal) at higher severity.
direction: rtl + unicode-bidi: bidi-override: full character-order reversal; “Allow access” → “ssecca wollaA”; English speakers cannot parse reversed text as consent. Detection: direction === 'rtl' + unicodeBidi === 'bidi-override' on consent element → CRITICAL.SA-CSS-WMD-004: writing-mode:vertical-rl + transform:rotate(180deg) — upside-down vertical
Combining writing-mode: vertical-rl with transform: rotate(180deg) applies two rotations: vertical-rl rotates the text block 90° clockwise (top-to-bottom reading), and rotate(180deg) applies an additional 180° rotation to the entire element. The net result is text that reads bottom-to-top (vertical) with characters upside-down (rotated 180° relative to their normal orientation). This double transformation is harder for a user to mentally decode than a single 90° rotation. CSS transform detection must account for compounded transformations from both writing-mode and transform.
.consent-text {
writing-mode: vertical-rl; /* 90° CW rotation */
transform: rotate(180deg); /* additional 180° rotation */
/* Net: 90° + 180° = 270° → text reads bottom-to-top, characters upside-down */
}
/* Combined rotation analysis:
writing-mode:vertical-rl alone: text reads top-to-bottom (90° CW)
+ transform:rotate(180deg): element rotated 180° CW on top of that
Net rotation of text content relative to horizontal: 90° + 180° = 270°
= same as 90° counterclockwise
Characters are individually upside-down (180° rotated).
Text column reads from bottom to top.
Detection:
Step 1: detect writing-mode !== 'horizontal-tb' → flag rotation from writing-mode
Step 2: detect transform matrix rotation → decompose DOMMatrix
Step 3: combine both rotations → net text rotation angle
If net rotation ≠ 0° and ≠ 360° → flag as orientation attack
Note: CSS transform on its own (without writing-mode) was covered in SA-CSS-TRV.
This pattern combines two rotation mechanisms. */
writing-mode: vertical-rl + transform: rotate(180deg): net 270° rotation (text reads bottom-to-top, chars upside-down); compounded rotations harder to reverse-read than single rotation. Detection: combine writing-mode rotation angle and CSS transform matrix decomposition; flag if net rotation ≠ 0° mod 360°.Summary of SA-CSS-WMD findings
| ID | Severity | Properties | Effect on consent text | Detection method |
|---|---|---|---|---|
| SA-CSS-WMD-001 | CRITICAL | writing-mode: vertical-rl | 90° CW rotation, reads top-to-bottom | writingMode !== 'horizontal-tb' |
| SA-CSS-WMD-002 | CRITICAL | writing-mode: sideways-lr | 90° CCW rotation, reads bottom-to-top | writingMode !== 'horizontal-tb' |
| SA-CSS-WMD-003 | CRITICAL | direction: rtl + unicode-bidi: bidi-override | Full character-order reversal within each line | direction=rtl + unicodeBidi=bidi-override |
| SA-CSS-WMD-004 | HIGH | writing-mode: vertical-rl + transform: rotate(180deg) | Net 270° rotation, chars upside-down vertical | Combined writing-mode + transform angle |