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: "REKO 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. */
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| 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 required | LTR 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 first | CSS injection setting direction:rtl on permission notice or disclosure container; multi-word notices (3+ words) are most severely affected | Permission 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 distorted | HIGH |
| unicode-bidi:bidi-override + direction:rtl on security status text — CSS-only BiDi attack without Unicode control characters, evades U+202E scanners | CSS 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 characters | Security 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 tools | HIGH |
| direction:rtl on button row container — Cancel and Accept positions swapped in visual layout while DOM order unchanged | CSS 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 reversed | Sighted 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
- Freeze
directionandunicode-bidion all consent UI elements with!important. Adddirection: ltr !important; unicode-bidi: normal !important;to all payment amount displays, permission notices, security status labels, and button containers. This prevents injected RTL direction from reversing these elements. - Computed style check: audit
directionandunicode-bidiin the consent container. Any element withdirection:rtlorunicode-bidi:bidi-overridewithin the consent flow that contains LTR-intended security text should be flagged HIGH. - BiDi attack scanner must include CSS checks. Standard BiDi scanners check DOM text for Unicode control characters. Extend the scanner to also check computed CSS properties:
directionandunicode-bidion all security-critical elements. - Button position verification. After rendering, verify that the Cancel button is still the leftmost action in a LTR layout by checking the element's
getBoundingClientRect().leftrelative to the Accept button. Swap detection catchesdirection:rtlreordering. - SkillAudit flags:
direction:rtlon any element in a LTR consent UI whose computedlangattribute indicates an LTR language;unicode-bidi:bidi-overrideon any security text element; Cancel button bounding box left edge exceeds Accept button left edge in a LTR layout.
SkillAudit findings for this attack surface
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.