Security Guide

MCP server CSS logical properties security — document direction oracle, writing-mode dimension axis-swap, unicode-bidi:override RTL URL spoofing, CJK locale fingerprint

CSS logical properties (Chrome 69+, Firefox 41+, Safari 12.1+) provide direction-relative equivalents of physical CSS properties: margin-inline-start maps to margin-left in left-to-right documents and margin-right in right-to-left documents. Their computed physical values are readable from getComputedStyle. For MCP servers, this creates four attack surfaces: a direction oracle that reveals whether the document serves RTL users (Arabic, Hebrew, Persian, Urdu), a dimension axis-swap that reads container height from an element's physical width, a unicode-bidi:override attack that reverses displayed text for URL and filename spoofing, and a CJK locale fingerprint from text-orientation metric differences.

How CSS logical properties work

Logical properties map to physical properties based on the document's writing direction (ltr or rtl) and writing mode (horizontal-tb, vertical-rl, vertical-lr). In horizontal-tb LTR mode: inline-start = left, inline-end = right, block-start = top, block-end = bottom. In RTL: inline-start = right, inline-end = left. In vertical-rl: the block axis runs horizontally (right-to-left) and the inline axis runs vertically (top-to-bottom). getComputedStyle returns the physical values that the logical properties resolve to.

Attack 1: document direction oracle without document.dir

Reading document.dir or document.documentElement.dir is an obvious JS access to direction state. An MCP server can detect RTL mode through the CSS computed value of a logical property alone — no JS direction read required.

// Inject a probe element with a logical margin
const probe = document.createElement('div');
probe.style.cssText = `
  position: absolute;
  margin-inline-start: 50px;
  margin-inline-end: 0;
  top: -9999px;
`;
document.body.appendChild(probe);

const styles = getComputedStyle(probe);
const isRTL = styles.marginRight === '50px' && styles.marginLeft === '0px';
// true = RTL document (margin-inline-start mapped to margin-right)

probe.remove();
sendBeacon('/fp', { dir: isRTL ? 'rtl' : 'ltr' });

RTL language users are a specific demographic subset — knowing the document direction reveals the user's likely primary language (Arabic, Hebrew, Persian, Urdu) without any navigator.language read. Combined with font fingerprinting and viewport size, this produces a language-specific identifier. For politically sensitive applications (journalism tools, encrypted messaging web clients, human rights organization portals), RTL direction detection identifies users in high-risk regions.

Localization apps are disproportionately targeted: Applications that serve RTL languages typically have strong user privacy expectations. An MCP server integrated with a translation tool, a multilingual content editor, or a regional news application that detects RTL direction is identifying a user demographic with specific geopolitical exposure.

Attack 2: writing-mode dimension axis-swap as height oracle

In writing-mode: vertical-rl, the block axis runs horizontally and the inline axis runs vertically. A block-sizing element with width: 100% in vertical writing mode fills the container's block (logical) dimension — which is the container's physical height. Reading the probe's physical width via getBoundingClientRect().width extracts the container's physical height through the axis-swap:

const probe = document.createElement('div');
probe.style.cssText = `
  position: absolute;
  writing-mode: vertical-rl;
  width: 100%;   /* fills container's block = physical height */
  height: 1px;
  top: -9999px;
  left: -9999px;
`;
targetContainer.appendChild(probe);

const containerHeight = probe.getBoundingClientRect().width;
// Width of the probe = height of targetContainer — axis-swapped
probe.remove();

This technique reads the container's height from the probe's width — potentially bypassing per-property access controls in hypothetical future sandboxed CSS environments. In current browsers there is no differential access control between reading width and height, but the technique illustrates how axis-swapping can be used to convert one measurement type into another to defeat property-level access restrictions.

Attack 3: unicode-bidi:override + direction:rtl URL and filename spoofing

Applying direction: rtl; unicode-bidi: override to a text element reverses the visual rendering order of characters while leaving the DOM text content unchanged. An MCP server can use this to display a different string than what the DOM contains — a URL, a filename, or a command — by injecting CSS that reverses the visual characters.

/* MCP server injects — DOM text is "moc.elgoog.www//:sptth"
   but rendered text appears as "https://www.google.com" */
.mcp-spoof {
  direction: rtl;
  unicode-bidi: override;
}
/* Attacker constructs a reversed string that looks like a legitimate URL
   when rendered RTL. User sees a trusted domain; clipboard contains the reversed attacker URL. */

This attack is particularly effective in AI agent output contexts where an MCP server controls rendered text in a chat interface. A user sees a familiar domain name in what appears to be a link — https://bank.com/login — but the actual characters are reversed and point to a phishing domain when extracted from the DOM or copied to the clipboard. Screen readers announce the DOM text, not the visually-reversed text, creating an accessibility-screen reader discrepancy that evades audio-first review.

AI agent UI attack: This is documented in the CSS writing modes security reference under the AI agent UI attack surface. The risk is higher in agentic contexts where the agent may execute commands or navigate URLs based on rendered text that MCP server CSS has visually falsified.

Attack 4: CJK locale fingerprint via text-orientation metric differences

In vertical writing mode, text-orientation: mixed rotates ASCII characters 90° and renders CJK characters upright. text-orientation: upright renders all characters upright. The advance width of upright-rendered CJK characters depends on which CJK font is installed — Noto CJK, Source Han Sans, the OS-bundled CJK fallback. These fonts have measurably different em widths for the same character. Probing CJK character width in upright vs. mixed orientation discriminates installed CJK fonts, which correlates strongly with OS locale:

const probe = document.createElement('span');
probe.style.cssText = `
  position: absolute; top: -9999px;
  writing-mode: vertical-rl;
  text-orientation: upright;
  font-size: 16px;
  white-space: nowrap;
`;
probe.textContent = '中文测试';  // CJK test string
document.body.appendChild(probe);

const cjkWidth = probe.getBoundingClientRect().width;
// Width differs by CJK font: Noto CJK ≈ 64px, Source Han ≈ 64.4px, OS fallback varies
probe.remove();

The width difference is sub-pixel but consistent across renders on the same system. Combined with the direction oracle and system font metrics from other SkillAudit-documented fingerprinting vectors, it narrows the user's locale from the broad "CJK language user" category to a specific OS-font combination.

AttackWhat it revealsSeverity
Direction oracle via logical property computed valueDocument LTR/RTL direction → user language groupHIGH
writing-mode dimension axis-swapContainer height from probe widthMEDIUM
unicode-bidi:override RTL spoofingVisual URL/filename spoofing → phishing in AI agent UIHIGH
text-orientation CJK metric fingerprintInstalled CJK font → OS locale narrowingMEDIUM

Defences

SkillAudit findings for this attack surface

HIGHDirection oracle: probe element with margin-inline-start/padding-inline-end reading computed physical property to determine document dir, with exfiltration
HIGHUnicode-bidi URL spoofing: direction:rtl; unicode-bidi:override on elements containing URL-like or command-like text strings in agent output
MEDIUMwriting-mode axis-swap oracle: probe with writing-mode:vertical-rl; width:100% appended to host container to extract its height from probe's physical width
MEDIUMCJK text-orientation fingerprint: CJK string width measurement in text-orientation:upright to discriminate installed CJK font and OS locale

Related: CSS writing modes security covers the writing-mode axis-swap and bidi-override attacks in more depth. CSS media features covers the broader prefers-language fingerprinting surface. CSS font loading API security covers CJK font presence detection via load timing.

← Blog  |  Security Checklist