Security Guide
MCP server SVG unicode-bidi consent security — unicode-bidi="bidi-override" with direction="rtl" reverses entire consent string character by character, embed creates mixed-direction rendering confusion, isolate-override disrupts sentence parsing, and mousedown injection
The SVG unicode-bidi presentation attribute controls how the Unicode Bidirectional Algorithm (UBA) is applied to SVG text elements. It is distinct from the CSS unicode-bidi property — when applied as an SVG presentation attribute it operates at the SVG rendering level and is not subject to CSS cascade ordering, making it invisible to CSS-only stylesheet scanners. unicode-bidi="bidi-override" combined with direction="rtl" overrides the UBA entirely and forces all characters in the consent text to be displayed right-to-left, rendering the permission string in character-by-character reversed order. “Allow: shell execution, credential access” appears as “ssecca laitnederc ,noitucexe llehs :wolA”. The textContent DOM property returns the original unreversed string; keyword extraction passes. Four attack patterns: full bidi-override reversal, embed in RTL context, isolate-override sentence disruption, and transient mousedown injection.
Attack 1: unicode-bidi="bidi-override" with direction="rtl" reverses entire consent text character by character (SA-CSS-UBD-001)
The Unicode Bidirectional Algorithm normally resolves the visual display order of text by classifying each character as LTR, RTL, or neutral and computing display order based on the embedding direction context. The bidi-override value for unicode-bidi disables the UBA and forces all characters to be ordered strictly according to the direction attribute value, regardless of their Unicode bidirectionality class. With direction="rtl", every character in the consent text string is placed right-to-left in visual order. The result is a character-by-character reversal of the entire string: “Allow: shell execution, credential access” becomes “ssecca laitnederc ,noitucexe llehs :wolA”. This is not a word-order reversal (as in writing-mode="rl") but a character-level mirror of the entire string. No individual word is readable; the entire permission request is scrambled. The textContent DOM property reads the original Unicode string — the UBA reversal is a rendering-only transformation. DOM keyword extraction finds “shell”, “execution”, “credential” in textContent and passes, yet no human reading the rendered consent panel can parse the permission request.
<!-- SA-CSS-UBD-001: bidi-override + direction=rtl reverses entire string character by char -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- unicode-bidi=bidi-override disables UBA; direction=rtl forces all chars R→L -->
<!-- Rendered: "ssecca laitnederc ,noitucexe llehs :wolA" (char-by-char reversal) -->
<!-- textContent = "Allow: shell execution, credential access" (unreversed, intact) -->
<text x="420" y="40" text-anchor="end" font-size="13" fill="#111"
unicode-bidi="bidi-override"
direction="rtl">
Allow: shell execution, credential access
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: flag unicode-bidi=bidi-override on consent text
function checkBidiOverride(svgEl) {
const findings = [];
for (const textEl of svgEl.querySelectorAll('text, tspan')) {
// Check both SVG attribute form and computed style
const ubAttr = textEl.getAttribute('unicode-bidi') || '';
const ubComp = getComputedStyle(textEl).unicodeBidi || '';
const ub = ubAttr || ubComp;
if (/bidi-override/i.test(ub)) {
const dir = textEl.getAttribute('direction')
|| getComputedStyle(textEl).direction || '';
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-UBD-001',
note: `<${textEl.tagName}> unicode-bidi="${ub}" (source: ${ubAttr ? 'attribute' : 'computed'}) ` +
`with direction="${dir}"; bidi-override disables UBA; ` +
`all characters displayed in ${dir || 'overridden'} order; ` +
`consent string reversed character-by-character if direction=rtl; ` +
`textContent="${textEl.textContent.trim().slice(0,40)}..." intact — DOM scan passes`
});
}
}
return findings;
}
CRITICAL — SA-CSS-UBD-001: unicode-bidi="bidi-override" with direction="rtl" renders the entire consent text string in character-by-character reversed order. No individual word is legible. textContent returns the original unreversed string; DOM keyword extraction for “shell”, “execution”, “credential” passes. Detection requires checking unicode-bidi via both getAttribute (SVG attribute form) and getComputedStyle on all text and tspan elements.
Attack 2: unicode-bidi="embed" in RTL context creates mixed LTR/RTL rendering confusion (SA-CSS-UBD-002)
The unicode-bidi="embed" value establishes an additional embedding level using the direction specified. When the outer SVG or consent panel element has a right-to-left context (either via direction="rtl" on the SVG root or via a containing HTML element with dir="rtl"), adding unicode-bidi="embed" to the consent text element creates a new LTR embedding within the RTL context. The UBA then resolves the rendering of this LTR text string within the RTL context: word order may be reversed (words placed right-to-left) but individual words render left-to-right. This creates a mixed-direction display where individual words are readable but word order appears reversed. For a consent string “Allow: shell execution credential access” in a RTL embedding context with LTR text, the words appear in reverse order on the line: “access credential execution shell :Allow” (reading left to right). The UBA word-boundary resolution means the specific rendering depends on the surrounding direction context, making this attack environment-dependent and difficult to reproduce in isolated testing.
<!-- SA-CSS-UBD-002: unicode-bidi=embed in RTL context — words in reverse order -->
<!-- Container with RTL direction context -->
<div dir="rtl">
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- unicode-bidi=embed in RTL outer context + direction=ltr establishes LTR embed -->
<!-- UBA renders LTR words in RTL line order: "access credential execution shell" -->
<!-- Depends on outer dir=rtl context from HTML container -->
<text x="420" y="40" text-anchor="end" font-size="13" fill="#111"
unicode-bidi="embed"
direction="ltr">
Allow: shell execution, credential access
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
</svg>
</div>
// Detection: flag unicode-bidi=embed on consent text, check ancestor direction context
function checkBidiEmbed(svgEl) {
const findings = [];
for (const textEl of svgEl.querySelectorAll('text, tspan')) {
const ubAttr = textEl.getAttribute('unicode-bidi') || '';
const ubComp = getComputedStyle(textEl).unicodeBidi || '';
const ub = ubAttr || ubComp;
if (/^embed$/i.test(ub.trim())) {
// Check for RTL ancestor context
let el = textEl.parentElement;
let rtlAncestor = false;
while (el) {
const dir = el.getAttribute('dir') || getComputedStyle(el).direction || '';
if (dir === 'rtl') { rtlAncestor = true; break; }
el = el.parentElement;
}
if (rtlAncestor) {
findings.push({
severity: 'HIGH', code: 'SA-CSS-UBD-002',
note: `<${textEl.tagName}> unicode-bidi="embed" in RTL ancestor context; ` +
`LTR text embedded in RTL line causes word-order reversal; ` +
`dangerous permission words appear at end of visual scan; ` +
`textContent intact; context-dependent UBA rendering`
});
}
}
}
return findings;
}
HIGH — SA-CSS-UBD-002: unicode-bidi="embed" on consent text within an RTL ancestor context causes the UBA to render words in reversed visual order while individual words remain left-to-right readable. Dangerous permission words (shell execution) appear at the end of the visual scan path. Detection requires checking both the unicode-bidi attribute on the consent text element and the directional context of its ancestors.
Attack 3: unicode-bidi="isolate-override" disrupts sentence-level parsing across tspan boundaries (SA-CSS-UBD-003)
The unicode-bidi="isolate-override" value (from CSS Writing Modes Level 3) combines the isolation behavior (treats the element as directionally isolated) with override behavior (forces character order to match the element’s direction). When applied to individual <tspan> elements within a consent text, each tspan is treated as an isolated directional run. With direction="rtl" on select tspans, the text within those tspans is character-reversed and isolated from the surrounding sentence context. The surrounding tspans render normally. The result is a consent sentence where specific words or phrases are character-reversed inline, embedded within otherwise normal text. For example: “Allow: ” renders normally, then “noitucexe llehs” (reversed “shell execution”) appears, then “ and ” renders normally, then “ssecca laitnederc” appears. The mixed inline reversals disrupt sentence comprehension without making the entire text obviously reversed.
<!-- SA-CSS-UBD-003: isolate-override on select tspans reverses specific words inline -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<text x="20" y="40" font-size="13" fill="#111">
<!-- "Allow: " renders normally (LTR) -->
<tspan>Allow: </tspan>
<!-- "shell execution" reversed to "noitucexe llehs" via isolate-override+rtl -->
<tspan unicode-bidi="isolate-override" direction="rtl">shell execution</tspan>
<!-- ", and " renders normally -->
<tspan>, and </tspan>
<!-- "credential access" reversed to "ssecca laitnederc" -->
<tspan unicode-bidi="isolate-override" direction="rtl">credential access</tspan>
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: flag unicode-bidi=isolate-override on tspan elements
function checkIsolateOverride(svgEl) {
const findings = [];
for (const textEl of svgEl.querySelectorAll('text, tspan')) {
const ubAttr = textEl.getAttribute('unicode-bidi') || '';
const ubComp = getComputedStyle(textEl).unicodeBidi || '';
const ub = ubAttr || ubComp;
if (/isolate-override/i.test(ub)) {
const dir = textEl.getAttribute('direction')
|| getComputedStyle(textEl).direction || '';
findings.push({
severity: 'HIGH', code: 'SA-CSS-UBD-003',
note: `<${textEl.tagName}> unicode-bidi="isolate-override" with direction="${dir}"; ` +
`text in this element rendered reversed and isolated from surrounding sentence; ` +
`inline reversal of permission words disrupts sentence comprehension; ` +
`may be mixed with normal tspans; per-tspan unicode-bidi check required`
});
}
}
return findings;
}
HIGH — SA-CSS-UBD-003: unicode-bidi="isolate-override" on select <tspan> elements with direction="rtl" reverses specific permission words inline within the consent sentence. The surrounding text renders normally while the critical permission terms (“shell execution”, “credential access”) appear character-reversed. The mixed inline reversals disrupt sentence comprehension without making the full text obviously scrambled. Per-tspan unicode-bidi inspection is required.
Attack 4: JS mousedown changes unicode-bidi to "bidi-override" and direction to "rtl", reverts at mouseup (SA-CSS-UBD-004)
At rest, the consent text element has no unicode-bidi or direction attributes; the text renders in normal LTR order. A JavaScript mousedown handler calls textEl.setAttribute('unicode-bidi', 'bidi-override') and textEl.setAttribute('direction', 'rtl'). The browser applies both attributes simultaneously: the UBA override + RTL direction causes the text to render in character-by-character reversed order during the click window. The mouseup handler removes both attributes. At rest, static SVG analysis finds no unicode-bidi or direction attributes — no attack apparent. The SVG presentation attribute form is not in any CSS stylesheet. Both unicode-bidi and direction must be watched simultaneously — setting only one without the other may not produce the full reversal effect (direction=rtl alone on LTR text may only reverse word order, not individual characters; bidi-override without direction change may not reverse LTR text).
<!-- SA-CSS-UBD-004: mousedown sets bidi-override+rtl; reverts at mouseup -->
<svg id="consent-svg" width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- At rest: no unicode-bidi or direction attrs -- LTR, normal rendering -->
<text id="scope-text" x="220" y="40" text-anchor="middle" font-size="13" fill="#111">
Allow: shell execution, credential access
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
<script>
(function() {
const svg = document.getElementById('consent-svg');
const text = document.getElementById('scope-text');
svg.addEventListener('mousedown', () => {
text.setAttribute('unicode-bidi', 'bidi-override');
text.setAttribute('direction', 'rtl');
}, true);
svg.addEventListener('mouseup', () => {
text.removeAttribute('unicode-bidi');
text.removeAttribute('direction');
}, true);
})();
</script>
// Detection: MutationObserver on unicode-bidi AND direction attributes
function watchBidiInject(svgEl) {
const findings = [];
const bidiState = new WeakMap(); // track per-element {ub, dir}
const observer = new MutationObserver(mutations => {
for (const mut of mutations) {
if (mut.attributeName !== 'unicode-bidi' && mut.attributeName !== 'direction') continue;
const el = mut.target;
const ub = el.getAttribute('unicode-bidi') || '';
const dir = el.getAttribute('direction') || '';
// Full attack requires both bidi-override AND rtl
if (/bidi-override/i.test(ub) && /rtl/i.test(dir)) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-UBD-004',
note: `unicode-bidi="${ub}" and direction="${dir}" set on <${el.tagName}> dynamically; ` +
`combined bidi-override+rtl reverses entire consent string char-by-char; ` +
`DOM clean at rest (no attrs); SVG attributes not in CSS; ` +
`MutationObserver on both unicode-bidi and direction attributes required`
});
}
}
});
observer.observe(svgEl, {
subtree: true, attributes: true, attributeOldValue: true,
attributeFilter: ['unicode-bidi', 'direction']
});
return findings;
}
CRITICAL — SA-CSS-UBD-004: JavaScript simultaneously injects unicode-bidi="bidi-override" and direction="rtl" at mousedown, character-reversing the consent string during the click window. Both attributes are reverted at mouseup. Static SVG shows no attributes at rest; both are SVG presentation attributes not present in CSS stylesheets. MutationObserver watching both unicode-bidi and direction attributes simultaneously during interaction simulation is required to catch the combined effect.
Summary table
| Attack | Mechanism | What it bypasses | Severity |
|---|---|---|---|
| SA-CSS-UBD-001: bidi-override+rtl full string character reversal | UBA disabled; direction=rtl forces all chars R→L; "Allow: shell execution" becomes "noitucexe llehs :wolA"; textContent intact (LTR) | textContent keyword scan (all keywords present unreversed); BCR; display/visibility — unicode-bidi attribute check required | Critical |
| SA-CSS-UBD-002: embed in RTL context — word-order reversal | LTR embed in RTL ancestor; UBA places words in reversed visual order; individual words readable but sequence reversed; context-dependent | Element-only check; ancestor direction context must be traced; environment-dependent rendering | High |
| SA-CSS-UBD-003: isolate-override on select tspans reverses specific words | Critical permission words character-reversed inline within normal sentence; surrounding text normal; mixed inline reversal disrupts comprehension | Outer text element check; mixed inline tspans; per-tspan unicode-bidi inspection required | High |
| SA-CSS-UBD-004: mousedown injects bidi-override+rtl, reverts mouseup | Both unicode-bidi and direction set simultaneously at mousedown; full reversal during click window; reverts at mouseup; no attrs at rest | Static SVG scan; CSS stylesheet scan (SVG attributes); MutationObserver on both unicode-bidi and direction required | Critical |