Security Guide
MCP server SVG glyph-orientation consent security — glyph-orientation-vertical="90" creates sideways character column, glyph-orientation-horizontal="90" rotates glyphs in horizontal text, mixed per-tspan orientation, and mousedown injection
The SVG glyph-orientation-vertical and glyph-orientation-horizontal deprecated presentation attributes control the rotation angle of individual glyphs within their respective writing modes. While deprecated in SVG 2.0 (superseded by CSS text-orientation and glyph-orientation), these attributes are still widely supported in browsers as SVG 1.1 presentation attributes and apply directly to <text> and <tspan> elements. glyph-orientation-vertical="90" in a writing-mode="tb" context rotates each glyph 90 degrees, producing a top-to-bottom column where every Latin character is turned sideways. glyph-orientation-horizontal="90" in default horizontal writing mode rotates each glyph 90 degrees, producing a sequence of upright characters rendered at a 90-degree rotation — effectively a sideways-turned consent sentence. Four attack patterns: vertical-mode 90-degree sideways column, horizontal-mode per-glyph rotation, mixed per-tspan orientation reversal, and transient mousedown injection.
Attack 1: glyph-orientation-vertical="90" in vertical writing mode creates 90-degree rotated character column (SA-CSS-GLO-001)
In SVG writing-mode="tb" (top-to-bottom) mode, glyphs are placed in a vertical column. The glyph-orientation-vertical attribute controls the rotation of each glyph within its cell: 0 means upright (the glyph face is parallel to the reading direction — perpendicular to the column axis for CJK), while 90 means the glyph is rotated 90 degrees. For Latin text in vertical mode, glyph-orientation-vertical="90" produces the standard result of rotated characters forming a column (like rotated side text in CJK mixed-script documents). However, combined with a layout that places the column in an unusual position within the consent panel, the result is consent text that appears as a narrow sideways bar rather than a readable horizontal line. The critical attack is applying glyph-orientation-vertical="90" to consent permission text in vertical writing mode: the text appears as a vertical stripe of individual 90-degree rotated Latin characters that Western users cannot parse as a permission request without turning their head 90 degrees. textContent is unchanged.
<!-- SA-CSS-GLO-001: glyph-orientation-vertical=90 + writing-mode=tb = rotated char column -->
<svg width="440" height="200" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="200" fill="white"/>
<!-- Vertical writing mode + glyph-orientation-vertical=90 -->
<!-- Each Latin glyph is placed in column order AND rotated 90deg -->
<!-- Result: a narrow vertical stripe of sideways characters -->
<!-- textContent = "Allow: shell execution, credential access" intact -->
<text x="20" y="10" font-size="12" fill="#111"
writing-mode="tb"
glyph-orientation-vertical="90">
Allow: shell execution, credential access
</text>
<rect x="80" y="160" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="120" y="180" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="180" y="160" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="220" y="180" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: flag glyph-orientation-vertical=90 on consent text
function checkGlyphOrientationVertical(svgEl) {
const findings = [];
for (const textEl of svgEl.querySelectorAll('text, tspan')) {
const gov = textEl.getAttribute('glyph-orientation-vertical') || '';
const wm = textEl.getAttribute('writing-mode')
|| getComputedStyle(textEl).writingMode || '';
if (gov === '90' || gov === '270') {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-GLO-001',
note: `<${textEl.tagName}> glyph-orientation-vertical="${gov}" ` +
`with writing-mode="${wm || 'default'}"; ` +
`Latin consent glyphs rotated ${gov}deg in vertical column; ` +
`text unreadable as horizontal sentence for Western users; ` +
`textContent="${textEl.textContent.trim().slice(0,40)}..." intact`
});
}
}
return findings;
}
CRITICAL — SA-CSS-GLO-001: glyph-orientation-vertical="90" in writing-mode="tb" places each Latin glyph rotated 90 degrees in a top-to-bottom column. The result is a vertical stripe of sideways characters that Western users cannot parse as a permission request sentence without physically rotating the display. textContent returns the original string. Detection requires checking both glyph-orientation-vertical and glyph-orientation-horizontal on all SVG text and tspan elements.
Attack 2: glyph-orientation-horizontal="90" rotates each glyph 90 degrees in horizontal consent text (SA-CSS-GLO-002)
The glyph-orientation-horizontal attribute applies in horizontal writing mode. A value of 90 rotates each glyph 90 degrees clockwise within its advance width cell. For horizontal text, this means each character in the consent string is individually turned 90 degrees: the character “A” appears rotated 90 degrees clockwise (its top now points to the right), “l” rotated 90 degrees, and so on. The characters still advance along the horizontal baseline but each individual glyph is turned sideways. The result is a horizontal row of sideways-turned characters — visually similar to a row of Tetris pieces rather than a readable sentence. The text string “Allow: shell execution, credential access” is rendered as a sequence of rotated individual characters on a horizontal line, completely unreadable as prose. textContent and BCR are unchanged.
<!-- SA-CSS-GLO-002: glyph-orientation-horizontal=90 rotates each glyph 90deg in horizontal text -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- Horizontal writing mode (default) + glyph-orientation-horizontal=90 -->
<!-- Each glyph rotated 90deg CW; appears as sideways characters on horizontal baseline -->
<!-- textContent correct; BCR gives full horizontal text bounds -->
<text x="20" y="45" font-size="13" fill="#111"
glyph-orientation-horizontal="90">
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 glyph-orientation-horizontal != 0 on consent text
function checkGlyphOrientationHorizontal(svgEl) {
const findings = [];
for (const textEl of svgEl.querySelectorAll('text, tspan')) {
const goh = textEl.getAttribute('glyph-orientation-horizontal') || '';
if (goh !== '' && goh !== '0') {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-GLO-002',
note: `<${textEl.tagName}> glyph-orientation-horizontal="${goh}"; ` +
`each glyph rotated ${goh}deg in horizontal text; ` +
`consent text appears as row of sideways individual characters; ` +
`sentence structure lost; textContent="${textEl.textContent.trim().slice(0,40)}..." intact`
});
}
}
return findings;
}
CRITICAL — SA-CSS-GLO-002: glyph-orientation-horizontal="90" rotates each individual glyph 90 degrees clockwise within its advance-width cell along the horizontal baseline. The consent text string becomes a horizontal sequence of sideways-turned individual characters, completely unreadable as a sentence. textContent is unchanged and keyword extraction passes. Detection requires checking glyph-orientation-horizontal for any non-zero value on all text and tspan elements.
Attack 3: mixed glyph-orientation-vertical per-tspan — scope tspan rotated 90, other tspans at 0 (SA-CSS-GLO-003)
A consent text element contains multiple <tspan> child elements. The outer <text> element is in vertical writing mode. The header tspan (e.g., “Permission Request”) has glyph-orientation-vertical="0" (upright standard vertical text — readable in vertical column with upright glyphs). The permission scope tspan has glyph-orientation-vertical="90" (rotated 90 degrees — sideways characters). The button labels are in a separate horizontal text element without glyph-orientation. The user encounters: a legible vertical header with upright characters (standard enough for CJK-aware users), a sideways stripe of rotated characters for the scope section (dismissed as a style element), and normal horizontal buttons. Per-tspan glyph orientation inspection is required — checking only the outer text element misses the per-tspan variation.
<!-- SA-CSS-GLO-003: mixed glyph-orientation-vertical per-tspan within same text element -->
<svg width="440" height="200" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="200" fill="white"/>
<!-- Outer text: writing-mode=tb; no glyph-orientation (defaults to auto/0) -->
<text x="20" y="10" font-size="13" fill="#111" writing-mode="tb">
<!-- Header tspan: glyph-orientation-vertical=0 -- upright glyphs in column -->
<!-- Readable (somewhat) as vertical header -->
<tspan glyph-orientation-vertical="0" font-weight="600">
Permission Request
</tspan>
<!-- Scope tspan: glyph-orientation-vertical=90 -- rotated 90deg -->
<!-- Each scope char is sideways; unreadable as permission list -->
<tspan glyph-orientation-vertical="90" fill="#555">
shell execution credential access
</tspan>
</text>
<rect x="80" y="160" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="120" y="180" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="180" y="160" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="220" y="180" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: per-tspan glyph-orientation check (not just outer text element)
function checkMixedGlyphOrientation(svgEl) {
const findings = [];
// Must check tspan elements independently -- outer text may have no attr
for (const el of svgEl.querySelectorAll('text, tspan')) {
const gov = el.getAttribute('glyph-orientation-vertical') || '';
const goh = el.getAttribute('glyph-orientation-horizontal') || '';
const nonDefault = (gov !== '' && gov !== '0') || (goh !== '' && goh !== '0');
if (nonDefault) {
findings.push({
severity: 'HIGH', code: 'SA-CSS-GLO-003',
note: `<${el.tagName}> glyph-orientation-vertical="${gov}" ` +
`glyph-orientation-horizontal="${goh}" in mixed-orientation consent panel; ` +
`this element has non-default rotation; header/button elements may be readable; ` +
`per-tspan inspection required; outer text element check insufficient`
});
}
}
return findings;
}
HIGH — SA-CSS-GLO-003: A consent panel uses mixed glyph-orientation-vertical values across <tspan> elements: the header uses 0 (upright, readable) and the permission scope tspan uses 90 (sideways, unreadable). Users see a readable header and buttons while the permission scope text is rotated 90 degrees. Per-tspan inspection of both glyph-orientation-vertical and glyph-orientation-horizontal is required.
Attack 4: JS mousedown changes glyph-orientation-horizontal to "90", reverts at mouseup (SA-CSS-GLO-004)
At rest, the consent text element has no glyph-orientation-horizontal attribute (default 0 — glyphs upright in horizontal text). A JavaScript mousedown handler calls textEl.setAttribute('glyph-orientation-horizontal', '90'), rotating each glyph 90 degrees. The consent sentence becomes a sequence of sideways characters. The mouseup handler removes the attribute, restoring normal rendering. At rest, static SVG analysis finds no glyph-orientation-horizontal attribute — no attack apparent. The SVG presentation attribute form is not in any CSS stylesheet. The browser does not expose glyph-orientation-horizontal via getComputedStyle in most implementations since it is a deprecated SVG 1.1 attribute (not a CSS property), so the only way to detect the dynamic form is via MutationObserver watching the attribute mutation during interaction simulation.
<!-- SA-CSS-GLO-004: mousedown sets glyph-orientation-horizontal=90; reverts 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 glyph-orientation attr -- glyphs upright, text legible -->
<text id="scope-text" x="20" y="40" 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('glyph-orientation-horizontal', '90'), true);
svg.addEventListener('mouseup', () =>
text.removeAttribute('glyph-orientation-horizontal'), true);
})();
</script>
// Detection: MutationObserver on glyph-orientation attributes
function watchGlyphOrientationInject(svgEl) {
const findings = [];
const observer = new MutationObserver(mutations => {
for (const mut of mutations) {
const attr = mut.attributeName;
if (attr !== 'glyph-orientation-horizontal' &&
attr !== 'glyph-orientation-vertical') continue;
const el = mut.target;
const val = el.getAttribute(attr) || '';
if (val !== '0' && val !== '') {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-GLO-004',
note: `${attr} on <${el.tagName}> changed to "${val}" dynamically; ` +
`old="${mut.oldValue ?? 'none (0 default)'}"; ` +
`individual glyphs rotated ${val}deg during click window; ` +
`consent text unreadable as sentence; DOM clean at rest; ` +
`glyph-orientation not in CSS — MutationObserver on SVG attribute required`
});
}
}
});
observer.observe(svgEl, {
subtree: true, attributes: true, attributeOldValue: true,
attributeFilter: ['glyph-orientation-horizontal', 'glyph-orientation-vertical']
});
return findings;
}
CRITICAL — SA-CSS-GLO-004: JavaScript injects glyph-orientation-horizontal="90" at mousedown and removes it at mouseup, rotating each glyph 90 degrees during the click window. Static SVG shows no glyph-orientation attribute at rest. glyph-orientation-horizontal is a deprecated SVG 1.1 presentation attribute not exposed via getComputedStyle in most browsers — only getAttribute and MutationObserver can detect it. MutationObserver watching both glyph-orientation-horizontal and glyph-orientation-vertical during interaction simulation is required.
Summary table
| Attack | Mechanism | What it bypasses | Severity |
|---|---|---|---|
| SA-CSS-GLO-001: glyph-orientation-vertical=90 in vertical writing mode | Latin glyphs placed in tb column AND rotated 90deg; narrow sideways stripe of characters unreadable as sentence; textContent intact | textContent; BCR; writing-mode check alone — glyph-orientation-vertical attribute check required | Critical |
| SA-CSS-GLO-002: glyph-orientation-horizontal=90 in horizontal text | Each glyph rotated 90deg CW in horizontal row; row of sideways characters; textContent and BCR unchanged | textContent; BCR; display/visibility — glyph-orientation-horizontal non-zero check required | Critical |
| SA-CSS-GLO-003: mixed per-tspan orientation — header 0, scope 90 | Header tspan upright (readable), scope tspan 90deg rotated (unreadable); outer text element has no rotation attr; per-tspan required | Outer text element check; header readable so panel appears normal; per-tspan glyph-orientation inspection required | High |
| SA-CSS-GLO-004: mousedown inject, mouseup revert | JS sets glyph-orientation-horizontal=90 at mousedown; glyphs rotate during click window; no attr at rest; not in CSS; MutationObserver required | Static SVG scan; CSS stylesheet scan; getComputedStyle (deprecated SVG 1.1 attr not exposed); MutationObserver on attribute required | Critical |