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.
| Attack | What it reveals | Severity |
|---|---|---|
| Direction oracle via logical property computed value | Document LTR/RTL direction → user language group | HIGH |
| writing-mode dimension axis-swap | Container height from probe width | MEDIUM |
| unicode-bidi:override RTL spoofing | Visual URL/filename spoofing → phishing in AI agent UI | HIGH |
| text-orientation CJK metric fingerprint | Installed CJK font → OS locale narrowing | MEDIUM |
Defences
- CSP
style-srcblocks injection of direction/writing-mode/unicode-bidi rules. - In AI agent UIs, sanitize rendered MCP output by stripping or normalizing
direction,unicode-bidi, andwriting-modestyles before display. Any text that appears in an agent-output context and that could contain URLs, filenames, or commands should be rendered in a fixed LTR context regardless of document direction. - Validate URLs as Unicode strings, not visual appearance. When an MCP server output contains a URL-like string, extract it from the DOM text node (not from visual rendering) and validate it before navigation or execution.
- Use
:dir(ltr)pseudo-class on security-critical UI elements (auth forms, payment buttons, error messages) to lock their direction to LTR regardless of injected styles, ensuring consistent rendering independent of MCP server style injection.
SkillAudit findings for this attack surface
margin-inline-start/padding-inline-end reading computed physical property to determine document dir, with exfiltrationdirection:rtl; unicode-bidi:override on elements containing URL-like or command-like text strings in agent outputwriting-mode:vertical-rl; width:100% appended to host container to extract its height from probe's physical widthtext-orientation:upright to discriminate installed CJK font and OS localeRelated: 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.