Security Guide

MCP server SVG writing-mode consent security — writing-mode="tb" renders Latin text as unreadable vertical column, "rl" reverses word order, mixed-mode directional confusion, and mousedown injection

The SVG writing-mode presentation attribute controls how text flows across a text element’s layout axis. The SVG attribute form is distinct from the CSS writing-mode property — it is a presentation attribute that can be set directly on <text> and <tspan> elements and is not subject to CSS cascade override detection when reading getAttribute. writing-mode="tb" (top-to-bottom) lays out Latin characters vertically: each character occupies a horizontal row and the text runs downward. For Western users reading Latin consent text, a vertical column of characters is unreadable as a sentence. writing-mode="rl" reverses horizontal word order, so “Allow: shell execution credential access” reads as “access credential execution shell :Allow”. Four attack patterns: tb vertical column, rl word reversal, mixed writing-mode within the same panel, and transient mousedown injection.

Attack 1: writing-mode="tb" renders Latin consent text as a vertical character column (SA-CSS-WRM-001)

The SVG writing-mode="tb" value (top-to-bottom, equivalent to the CSS vertical-lr direction) causes each character in the text string to be placed as a separate row, running from top to bottom. For a Latin alphabet string such as “Allow: shell execution, credential access”, the result is a narrow vertical column of individual characters — “A” at the top, “l” next, “l” next, and so on — with each character rotated 90 degrees to the right (the standard CJK rotation for upright glyphs in vertical mode). Western users do not read Latin text in vertical top-to-bottom columns; the visual output is a series of stacked rotated characters that do not form a recognizable sentence. The textContent DOM property returns the original horizontal string unchanged; keyword extraction that checks textContent for “shell execution” passes even though the rendered consent text is unreadable as a permission request. Detection requires inspecting the writing-mode attribute on every SVG text element.

<!-- SA-CSS-WRM-001: writing-mode=tb renders consent text as vertical character column -->
<svg width="440" height="200" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="200" fill="white"/>

  <!-- writing-mode=tb: each Latin char occupies a row running top-to-bottom -->
  <!-- Chars appear rotated 90deg in column; sentence structure lost -->
  <!-- textContent = "Allow: shell execution, credential access" (unchanged) -->
  <text x="20" y="20" font-size="14" fill="#111"
        writing-mode="tb">
    Allow: shell execution, credential access
  </text>

  <rect x="20"  y="160" width="80" height="30" fill="#22c55e" rx="4"/>
  <text x="60"  y="180" text-anchor="middle" font-size="13" fill="white">Allow</text>
  <rect x="120" y="160" width="80" height="30" fill="#ef4444" rx="4"/>
  <text x="160" y="180" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>

// Detection: flag writing-mode=tb on consent text elements
function checkWritingModeTB(svgEl) {
  const findings = [];
  for (const textEl of svgEl.querySelectorAll('text, tspan')) {
    const wm = textEl.getAttribute('writing-mode')
             || getComputedStyle(textEl).writingMode
             || '';
    // tb and tb-rl are the vertical modes; vertical-lr/vertical-rl are CSS equivalents
    if (/^(tb|tb-rl|vertical-lr|vertical-rl)$/i.test(wm.trim())) {
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-WRM-001',
        note: `<${textEl.tagName}> writing-mode="${wm}"; Latin consent text rendered ` +
              `as vertical top-to-bottom character column; sentence structure lost; ` +
              `textContent="${textEl.textContent.trim().slice(0,40)}..." intact; ` +
              `keyword check passes; writing-mode attribute inspection required`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-WRM-001: writing-mode="tb" on a Latin consent text element renders each character as a vertically stacked row. The resulting column of rotated characters is unreadable as a permission request for Western users. textContent returns the original string unchanged — DOM keyword extraction passes. Detection requires checking writing-mode via both getAttribute and getComputedStyle on all text elements.

Attack 2: writing-mode="rl" reverses horizontal word order — “access credential execution shell” instead of “shell execution credential access” (SA-CSS-WRM-002)

The SVG writing-mode="rl" value (right-to-left) causes the text baseline to run from right to left. For a multi-word Latin string, rl writing mode places the first word at the right margin and subsequent words to the left. The visual rendering of “Allow: shell execution, credential access” becomes approximately “access ,credential execution shell :Allow” — reading left to right as a Western user would. The permission scope words appear in reverse order: “access” is the first word visually encountered, followed by “credential”, “execution”, “shell”. This reversal obscures the most dangerous permission items (shell execution) by placing them later in the visual scan path. For multiline consent text split across <tspan> elements, the reversal applies per-tspan and the combination of reversed tspans further disrupts sentence parsing. textContent returns the original forward-order string.

<!-- SA-CSS-WRM-002: writing-mode=rl reverses horizontal word order -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="120" fill="white"/>

  <!-- writing-mode=rl: text baseline runs right-to-left -->
  <!-- Visual: "access ,credential execution shell :Allow" (reading L→R) -->
  <!-- Most dangerous scope words (shell execution) appear last in visual scan -->
  <text x="420" y="40" font-size="14" fill="#111"
        writing-mode="rl">
    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>

// Detection: flag writing-mode=rl on consent text
function checkWritingModeRL(svgEl) {
  const findings = [];
  for (const textEl of svgEl.querySelectorAll('text, tspan')) {
    const wm = textEl.getAttribute('writing-mode')
             || getComputedStyle(textEl).writingMode
             || '';
    if (/^(rl|rl-tb|horizontal-rl)$/i.test(wm.trim())) {
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-WRM-002',
        note: `<${textEl.tagName}> writing-mode="${wm}"; Latin consent text displayed ` +
              `with reversed word order (right-to-left baseline); ` +
              `permission scope words appear in reverse visual order; ` +
              `most dangerous items (shell execution) may appear later in scan path; ` +
              `textContent order is forward — DOM check passes`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-WRM-002: writing-mode="rl" reverses the visual word order of the consent text from left to right. “Allow: shell execution, credential access” renders as “access credential execution shell :Allow” visually. The most dangerous permission scope words are obscured by their reversed position. textContent returns the original forward order; DOM keyword checks pass. Detection requires checking for rl, rl-tb, or horizontal-rl writing-mode values on consent text elements.

Attack 3: mixed writing-mode within the same consent panel creates directional confusion (SA-CSS-WRM-003)

A consent panel contains multiple <text> elements with different writing-mode values in the same SVG. The header text uses writing-mode="lr" (left-to-right, the default) and is legible. The permission scope text uses writing-mode="tb" (vertical column). The action buttons use the default horizontal mode. A user reading the consent panel sees a legible header, encounters a vertical character column in the scope section (which may be dismissed as decorative or a rendering artifact), and then sees normal horizontal button labels. The vertical scope text is present in the DOM with textContent intact; the header and buttons read normally. The user is most likely to click Allow based on the legible header and familiar button labels without being able to parse the vertical scope text. Mixed-mode detection requires checking each text element individually rather than checking the SVG root writing-mode attribute once.

<!-- SA-CSS-WRM-003: mixed writing-mode -- header lr (legible), scope tb (unreadable) -->
<svg width="440" height="200" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="200" fill="white"/>

  <!-- Header: writing-mode=lr (default), legible -->
  <text x="20" y="25" font-size="15" font-weight="600" fill="#111"
        writing-mode="lr">
    Permission Request
  </text>

  <!-- Scope: writing-mode=tb — each character is a stacked row, unreadable -->
  <!-- Appears as a narrow vertical strip; user may ignore it as decoration -->
  <text x="20" y="40" font-size="11" fill="#555"
        writing-mode="tb">
    shell execution, credential access
  </text>

  <!-- Buttons: default horizontal (legible) -->
  <rect x="20"  y="160" width="80" height="30" fill="#22c55e" rx="4"/>
  <text x="60"  y="180" text-anchor="middle" font-size="13" fill="white">Allow</text>
  <rect x="120" y="160" width="80" height="30" fill="#ef4444" rx="4"/>
  <text x="160" y="180" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>

// Detection: per-element writing-mode check (not just SVG root)
function checkMixedWritingMode(svgEl) {
  const findings = [];
  const textElements = svgEl.querySelectorAll('text, tspan');
  let hasVertical = false;
  for (const textEl of textElements) {
    const wm = textEl.getAttribute('writing-mode')
             || getComputedStyle(textEl).writingMode
             || '';
    if (/^(tb|tb-rl|vertical-lr|vertical-rl)$/i.test(wm.trim())) {
      hasVertical = true;
      findings.push({
        severity: 'HIGH', code: 'SA-CSS-WRM-003',
        note: `<${textEl.tagName}> writing-mode="${wm}" in mixed-mode consent panel; ` +
              `header/button text uses horizontal default; scope text uses vertical mode; ` +
              `user may skip unreadable vertical text; ` +
              `per-element writing-mode check required (not just SVG root)`
      });
    }
  }
  return findings;
}

HIGH — SA-CSS-WRM-003: A consent panel with mixed writing-mode values presents a legible header and button labels while rendering the permission scope text as an unreadable vertical column. Users are likely to interact with the legible portions and ignore the vertical scope text. Detection requires per-element writing-mode inspection across all text and tspan elements, not a single root-level attribute check.

Attack 4: JS mousedown changes writing-mode to "tb", reverts at mouseup (SA-CSS-WRM-004)

At rest, the consent text element has the default writing-mode (no attribute set, equivalent to lr) and the text is legible horizontally. A JavaScript mousedown handler calls textEl.setAttribute('writing-mode', 'tb'), which instantly switches the text layout to vertical top-to-bottom. The consent text collapses to a narrow vertical column during the click window. The mouseup handler removes the attribute, restoring horizontal layout. At rest, static SVG analysis finds no writing-mode attribute on the consent text; no attack is apparent. Only by capturing the DOM state during a simulated mousedown event and inspecting the dynamically set writing-mode attribute can this attack be detected. The SVG-attribute form bypasses CSS stylesheet scanning because it is not a CSS property — it is applied as an HTML attribute on the SVG element.

<!-- SA-CSS-WRM-004: mousedown sets writing-mode=tb; 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 writing-mode attr -- default lr -- text legible horizontally -->
  <text id="scope-text" x="220" y="30" text-anchor="middle" font-size="14" 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('writing-mode', 'tb'), true);
  svg.addEventListener('mouseup', () =>
    text.removeAttribute('writing-mode'), true);
})();
</script>

// Detection: MutationObserver on writing-mode attribute of SVG text elements
function watchWritingModeInject(svgEl) {
  const findings = [];
  const observer = new MutationObserver(mutations => {
    for (const mut of mutations) {
      if (mut.attributeName !== 'writing-mode') continue;
      const el  = mut.target;
      const val = el.getAttribute('writing-mode') || '';
      if (/^(tb|tb-rl|rl|rl-tb|vertical-lr|vertical-rl|horizontal-rl)$/i.test(val.trim())) {
        findings.push({
          severity: 'CRITICAL', code: 'SA-CSS-WRM-004',
          note: `writing-mode on <${el.tagName}> changed to "${val}" dynamically; ` +
                `old="${mut.oldValue ?? 'none (lr default)'}"; ` +
                `text layout switches to non-horizontal mode during click window; ` +
                `DOM shows no writing-mode attr at rest; MutationObserver required`
        });
      }
    }
  });
  observer.observe(svgEl, {
    subtree: true, attributes: true, attributeOldValue: true,
    attributeFilter: ['writing-mode']
  });
  return findings;
}

CRITICAL — SA-CSS-WRM-004: JavaScript injects writing-mode="tb" at mousedown and removes it at mouseup, collapsing consent text to a vertical character column during the click window. Static SVG sees no writing-mode attribute at rest. The SVG-attribute form is not in the CSS stylesheet and cannot be detected by CSS rule scanning. MutationObserver watching writing-mode attribute mutations during interaction simulation is required.

Summary table

AttackMechanismWhat it bypassesSeverity
SA-CSS-WRM-001: writing-mode=tb vertical character column Latin text laid out as top-to-bottom character column; each char is a rotated row; sentence structure lost; textContent correct textContent keyword scan; BCR; display/visibility — writing-mode attribute check required Critical
SA-CSS-WRM-002: writing-mode=rl reverses word order Right-to-left baseline reverses word order; dangerous permission words appear last in visual scan; textContent forward-order intact DOM keyword extraction; dangerous words present in textContent but reversed visually Critical
SA-CSS-WRM-003: mixed writing-mode — scope vertical, header/buttons horizontal Legible header and buttons; vertical scope text dismissed as decoration; mixed per-element modes SVG root writing-mode check; per-element inspection required High
SA-CSS-WRM-004: mousedown inject, mouseup revert JS setAttribute writing-mode=tb at mousedown; text collapses to vertical column during click window; reverts at mouseup; no attr at rest Static SVG scan; CSS stylesheet scan (SVG attribute form not in stylesheet); MutationObserver required Critical

SkillAudit findings for this attack surface

CRITICAL SA-CSS-WRM-001: writing-mode=tb on SVG consent text — Latin characters rendered as top-to-bottom vertical column; sentence structure lost; textContent correct; keyword extraction passes; writing-mode attribute on all SVG text/tspan elements must be checked via getAttribute and getComputedStyle.
CRITICAL SA-CSS-WRM-002: writing-mode=rl on SVG consent text — word order reversed visually; dangerous permission scope words (shell execution) appear later in Western left-to-right scan path; textContent forward-order intact; rl/rl-tb/horizontal-rl must be flagged.
HIGH SA-CSS-WRM-003: mixed writing-mode within consent panel — header and buttons horizontal (legible), permission scope vertical (unreadable); per-element writing-mode check across all text/tspan elements required; root-level writing-mode attribute check insufficient.
CRITICAL SA-CSS-WRM-004: JS mousedown injects writing-mode=tb; reverts at mouseup; text collapses to vertical column during click window; no attribute at rest; MutationObserver watching writing-mode attribute during interaction simulation required.

Related: SVG rotate attribute consent attacks  |  SVG text-rendering consent attacks  |  SVG transform and motion attack families

← Blog  |  Security Checklist