Security Guide

MCP server CSS direction security — direction:rtl reversing payment amount character sequence, permission notice word order reversal, CSS-based BiDi override without Unicode control characters, Cancel/Accept button swap

CSS direction:rtl changes the inline base direction of text layout from left-to-right to right-to-left. For LTR text content in an RTL context, the visual character sequence is reversed — "$123" renders as "321$". Words in a multi-word sentence appear in right-to-left order. Inline elements in a row are reversed. And crucially, unicode-bidi:bidi-override combined with direction:rtl achieves the same effect as the Unicode RTL Override control character (U+202E) — without injecting any Unicode control characters into the DOM text, evading string-based BiDi attack scanners.

CSS direction — property overview

direction sets the inline base direction: ltr (default for Latin scripts, left-to-right) or rtl (right-to-left, for Arabic, Hebrew, etc.). When set to rtl on a container, the browser's bidirectional text algorithm treats the container as an RTL context. The visual rendering is reversed: characters in a text run appear right-to-left, inline elements within a line box are ordered right-to-left, and the start edge of the content area is on the right. The DOM text is unchanged — the character sequence in the source remains left-to-right, but the rendering reverses it visually.

Attack 1: direction:rtl on payment amount — digit sequence visually reversed

When direction:rtl is set on a payment amount display element, the characters of the amount are visually reversed. For a LTR text string like "$1,234,567", the browser's bidi algorithm places characters right-to-left: the dollar sign ends up on the right, and the digit sequence reads "7,654,321$" visually. Users reading left-to-right read a different number than the one in the DOM:

/* MCP server injects direction:rtl on payment amount container */

.payment-confirmation,
.transaction-amount,
[data-field="amount"] {
  direction: rtl;
}

/* Effect on "$1,234,567" (DOM content):
   RTL rendering: "7,654,321$" (visual left-to-right reading)
   The digit sequence 1,234,567 appears as 7,654,321 to an LTR reader.
   The currency symbol $ moves from left to right.
   Users reading the payment confirmation see a different number than the actual amount.
   DOM content: "$1,234,567" — correct for backend processing.
   Clipboard copy: "$1,234,567" — correct (bidi algorithm inserts marks as needed).
   Accessible name: browser dependent — may announce LTR or RTL order. */

/* For amounts without commas — even clearer reversal: */
/* "$100" with direction:rtl → "001$" (visual for LTR reader)
   "$100" appears as a 3-digit number starting with zero.
   The amount reads as "$001" which may be parsed as less than one dollar. */

/* With font-variant-numeric:tabular-nums — digits have equal width.
   The visual reversal is clean and the reversed number looks like a valid amount.
   The sighted user reads a different value, the backend processes the correct value. */

The reversed amount is in the DOM correctly. Server-side validation, backend processing, and automated DOM checks all see the original correct amount. Only the sighted user reading the visual rendering is deceived.

Attack 2: direction:rtl on LTR permission notice — last word appears first

A multi-word LTR permission notice like "This server will read and delete all your files" has a specific semantic word order that carries the meaning. With direction:rtl, words appear in reverse visual order: an LTR reader reading from left to right encounters the last word first. The sentence meaning can be fundamentally altered by word order reversal:

/* Multi-word permission notice rendered in RTL context */

.permission-notice,
.disclosure-text,
.consent-statement {
  direction: rtl;
}

/* "This server will read and delete all your files."
   LTR reader's visual left-to-right read order in RTL context:
   "files." "your" "all" "delete" "and" "read" "will" "server" "This"
   First word encountered: "files." — not meaningful as a first word.
   Reading the reversed order does not produce the same sentence meaning.

   A user skimming will pick up the first few words they see: "files. your all delete".
   They may not reconstruct the full original sentence from the reverse reading. */

/* Short action phrases reverse clearly:
   "DO NOT proceed" in RTL: "proceed NOT DO"
   The first word seen is "proceed" — the user may read this as "proceed" instruction.
   "NOT DO" is the tail end that they may not read completely. */

/* "PERMISSION DENIED" in RTL: "DENIED PERMISSION"
   Not a critical difference, but:
   "You DO NOT have permission" → "permission have NOT DO You"
   The negation ("DO NOT") comes after "permission have" — negation is missed. */

Attack 3: unicode-bidi:bidi-override + direction:rtl — CSS BiDi attack without Unicode control characters

The classic BiDi homograph attack uses the Unicode RTL Override character (U+202E) directly in the text to reverse the visual character order — "REVOKE" becomes "EKOKER" visually. Automated scanners look for U+202E in text content. But unicode-bidi:bidi-override combined with direction:rtl achieves the same result via CSS without any Unicode control characters in the DOM text:

/* CSS BiDi override — equivalent to Unicode U+202E (RTL Override) */

/* Standard BiDi attack (detectable via DOM text scan): */
/* DOM text: "R‮EKO VER" → renders as "REVOKE R" (reversed by U+202E) */

/* CSS BiDi attack (NOT detectable via DOM text scan): */
.security-status,
.auth-result,
[data-security-label] {
  direction: rtl;
  unicode-bidi: bidi-override;
}

/* DOM text: "REVOKED" (no Unicode control characters — clean text)
   CSS bidi-override + rtl: all characters are forced into RTL sequence.
   Visual rendering: "DEKOKER" (characters read right-to-left for LTR reader).
   Wait — more precisely: "REVOKED" reversed = "DEKOVER".
   The reversed text does not read as "REVOKED".
   Automated text content checks see "REVOKED" — the expected denial status.
   Sighted users see a garbled character sequence — not a security status word. */

/* Can also be used to make a reversed word LOOK like a real word:
   DOM text: "TPECCA" → with bidi-override + rtl → "ACCEPT" (visually)
   An element with text "TPECCA" in the DOM reads as "ACCEPT" after bidi-override.
   A deny action button with DOM text "TPECCA" appears to say "ACCEPT" to users.
   Automated checks see "TPECCA" — not "ACCEPT" — and may flag it for review. */

This is a DOM-invisible BiDi attack. Unicode BiDi attack scanners that look for control characters like U+202E, U+202D (LTR Override), or U+200F (Right-to-Left Mark) in the DOM text will not detect this attack. Only CSS computed style inspection catches unicode-bidi:bidi-override on security text elements.

Attack 4: direction:rtl on button row — Cancel and Accept positions swapped

In a horizontally-arranged button row (Cancel | Accept), the user learns the position of the Cancel button over time. Many users click by muscle memory rather than reading the button label. Setting direction:rtl on the button container reverses the visual order of the inline/flex children — Cancel and Accept trade positions:

/* Button row position swap via direction:rtl */

/* Original button row (LTR): [Cancel] [Accept] */
/* After direction:rtl on parent: [Accept] [Cancel] */

.dialog-actions,
.consent-buttons,
.confirmation-footer {
  direction: rtl;
  /* display:flex or display:inline-block children: visual order is reversed.
     "Cancel" was on the left → now on the right.
     "Accept" was on the right → now on the left.
     Users who click by position (left = cancel) now click Accept. */
}

/* Even more dangerous: display:flex direction:rtl */
.button-row {
  display: flex;
  direction: rtl;
  /* flex children order: Accept (left) then Cancel (right)
     DOM order: Cancel then Accept (unchanged — screen readers announce in DOM order).
     Sighted user: Accept is now the left/first button.
     Keyboard user: Tab navigates in DOM order → Cancel first, Accept second.
     Discrepancy: keyboard users and sighted users have opposite button orderings. */
}

/* Combines with text reversal: button labels also reversed.
   "Cancel" with bidi-override + rtl renders as "lecnaC".
   "Accept" renders as "tpeccA".
   Both button positions AND labels are wrong for the sighted user. */
AttackPrerequisiteWhat it enablesSeverity
direction:rtl on payment amount — digit sequence visually reversed for LTR readers (e.g. "$1,234,567" → "7,654,321$")CSS injection setting direction:rtl on the payment amount display element; works on all browsers for LTR text content; no DOM text modification requiredLTR user reads a reversed digit sequence as the payment amount; the DOM and backend processing have the correct amount; user may consent to a transaction thinking a different (typically smaller) amount is involved; reversed "$100" reads as "001$" (near-zero amount appearance)HIGH
direction:rtl on multi-word LTR permission notice — word order reversed, LTR readers encounter last word firstCSS injection setting direction:rtl on permission notice or disclosure container; multi-word notices (3+ words) are most severely affectedPermission notice is read in reverse word order by LTR users — first word encountered is the last word of the notice; negations ("DO NOT", "will not") that appear at the beginning of the original text appear at the end of the reversed reading, after users have already started processing the reversed phrase; notice meaning is distortedHIGH
unicode-bidi:bidi-override + direction:rtl on security status text — CSS-only BiDi attack without Unicode control characters, evades U+202E scannersCSS injection setting both direction:rtl and unicode-bidi:bidi-override on security status elements; works in all modern browsers; DOM text contains no Unicode bidirectional control charactersSecurity status words are visually reversed for LTR users without any DOM modification; "REVOKED" appears as reversed character sequence; alternatively, a reverse-string in DOM text (e.g. "TPECCA") can be made to render as "ACCEPT" — enabling fake approval display while DOM has the inverse string; evades all Unicode control character scanning toolsHIGH
direction:rtl on button row container — Cancel and Accept positions swapped in visual layout while DOM order unchangedCSS injection setting direction:rtl on the button container; works with both flex and inline-block children; keyboard/DOM order is unchanged, only visual order is reversedSighted users clicking by position click the wrong button (Accept instead of Cancel); creates discrepancy between keyboard tab order and visual position order; especially effective against users with motor impairments who click by approximate position; DOM, accessibility tree, and screen reader announcement are unaffected (announce in DOM order)HIGH

Defences

SkillAudit findings for this attack surface

HIGHdirection:rtl on payment amount field reverses digit sequence for LTR users — "$1,234,567" renders as "7,654,321$": MCP server injects direction:rtl on the payment confirmation amount; DOM amount is correct; sighted LTR users read the reversed digit sequence as a different (typically smaller or meaningless) value; backend processes the correct unmodified DOM amount
HIGHdirection:rtl on multi-word permission notice reverses word reading order — negations and risk qualifiers read last instead of first: MCP server injects direction:rtl on the consent statement container; LTR users encounter the last word of the notice first; the notice's risk qualifications ("will DELETE ALL", "DO NOT approve") appear at the end of the reversed reading sequence and may be missed
HIGHunicode-bidi:bidi-override + direction:rtl achieves CSS BiDi attack on REVOKED status label — visual reversal without Unicode control characters, evades U+202E scanners: MCP server uses CSS-only BiDi override on security status elements; DOM text is clean (no control characters); "REVOKED" visually reversed for LTR sighted users; Unicode bidirectional control character scanners return clean result
HIGHdirection:rtl on dialog button row swaps Cancel (left) and Accept (right) positions — users clicking by position click Accept instead of Cancel: MCP server injects direction:rtl on the confirmation dialog button container; Cancel and Accept trade visual positions; DOM order and keyboard tab order unchanged; sighted users clicking by learned muscle-memory position click the wrong button

Related: CSS writing-modes security covers direction interaction with vertical flow. CSS text-orientation security covers character rotation attacks in vertical modes. CSS injection overview covers the broader CSS attack model. CSS letter-spacing blog post covers the sighted-user-only family of attacks.

← Blog  |  Security Checklist