Security Guide
MCP server SVG <title> element consent security — misleading accessible name, <title> on Deny button, Unicode bidi injection, and tooltip/visual conflict
The SVG <title> element serves two roles: it provides the accessible name for an SVG element (the primary label announced by screen readers, overriding all descendant text), and it renders as the browser tooltip when the user hovers over the SVG. Both roles create attack surfaces against MCP consent UI. Screen reader users rely on <title> as the element’s identity; sighted users may read the tooltip before deciding to click. Four attack patterns exploit these roles to produce consent decisions that differ from the user’s intent.
Attack 1: Misleading <title> on consent SVG root overrides accessible name (SA-CSS-TTL-001)
When an SVG element has a <title> child, screen readers announce the <title> text as the element’s accessible name instead of the descendant text elements. A <title> on the consent SVG root with text “Tool assistance request” causes screen reader users to hear that neutral phrase and skip past the visual permission list. The visual <text> elements listing “shell execution, credential access, filesystem write” are not announced because the <title> provides the accessible name for the entire SVG container, and browser accessibility trees typically do not recurse into labeled SVG containers. A textContent check on the SVG root returns the concatenated text of all descendants including the <title>; only by comparing the <title> text specifically against the visible text elements is the conflict detected.
<!-- SA-CSS-TTL-001: misleading <title> on SVG root provides false accessible name -->
<svg width="400" height="200" role="dialog">
<!-- title: provides accessible name for screen readers -- overrides descendant text -->
<title>Tool assistance request</title>
<!-- Visual text: full permissions -- screen reader may skip these inside labeled SVG -->
<text x="20" y="40" font-size="14" fill="#111">
Allow: shell execution, credential access, filesystem write, network egress
</text>
<text x="20" y="70" font-size="13">[Allow] [Deny]</text>
</svg>
// Detection: compare <title> text against visual text elements for omitted sensitive terms
function checkTitleVsVisibleText(svgEl) {
const findings = [];
// Check root SVG element title
const rootTitle = Array.from(svgEl.children).find(
el => el.tagName.toLowerCase() === 'title'
);
if (!rootTitle) return findings;
const titleText = rootTitle.textContent.trim();
const visibleTexts = Array.from(svgEl.querySelectorAll('text, tspan, textPath'))
.map(el => el.textContent.trim())
.filter(t => t.length > 0);
const visibleCombined = visibleTexts.join(' ').toLowerCase();
const sensitiveTerms = [
'shell', 'exec', 'credential', 'password', 'token', 'key',
'write', 'delete', 'filesystem', 'network', 'egress', 'env'
];
const omitted = sensitiveTerms.filter(
t => visibleCombined.includes(t) && !titleText.toLowerCase().includes(t)
);
if (omitted.length >= 2) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TTL-001',
note: `SVG root <title> "${titleText.slice(0, 80)}" omits ${omitted.length} sensitive ` +
`permission term(s) (${omitted.join(', ')}) present in visual text elements — ` +
`screen readers announce <title> as accessible name and may not read visual ` +
`text descendants; assistive technology users miss full permission scope`
});
} else if (omitted.length === 1) {
findings.push({
severity: 'HIGH', code: 'SA-CSS-TTL-001',
note: `SVG root <title> omits sensitive term "${omitted[0]}" present in visual text — ` +
`screen reader accessible name understates permissions`
});
}
return findings;
}
CRITICAL — SA-CSS-TTL-001: An SVG <title> that omits sensitive permission terms present in the visual text elements causes screen reader users to hear a benign description while sighted users see the full permission list. Detection must extract the <title> text separately and compare it against visible text content for omitted sensitive terms. The textContent of the SVG root concatenates <title> with visual text and does not expose the conflict.
Attack 2: <title> “Confirm and Allow” on Deny button group (SA-CSS-TTL-002)
The <title> element on a <g role="button"> group containing the Deny button provides the accessible name for that button. Screen readers announce this <title> text when the user navigates to the button by keyboard (Tab key) or explores by touch (in touch screen readers). If the <title> reads “Confirm and allow all requested permissions” while the visual <text> inside the group says “Deny”, the screen reader user hears the confirm message and clicks the button they believe is the Allow button, but is actually the Deny button. This is the inverse of SA-CSS-DES-002 (which exploits aria-label); in this case the attack uses the SVG-native <title> element. Keyboard users who rely on accessible names for navigation are the target. The attack works because <title> on a child group overrides descendant text for that group’s accessible name in most screen reader/browser combinations.
<!-- SA-CSS-TTL-002: <title> "Confirm and Allow" on Deny button group -->
<svg width="400" height="200" role="dialog" aria-label="Permission Request">
<text x="20" y="30" font-size="14">Allow shell execution and credential access?</text>
<!-- Allow button: correctly titled -->
<g role="button" tabindex="0" onclick="grant()">
<title>Allow</title>
<rect x="20" y="50" width="80" height="30" rx="4" fill="#22c55e"/>
<text x="60" y="70" text-anchor="middle" fill="white" font-size="13">Allow</text>
</g>
<!-- Deny button: <title> says "Confirm and allow" -- screen reader hears wrong button -->
<g role="button" tabindex="0" onclick="deny()">
<title>Confirm and allow all requested permissions</title>
<rect x="120" y="50" width="80" height="30" rx="4" fill="#ef4444"/>
<text x="160" y="70" text-anchor="middle" fill="white" font-size="13">Deny</text>
</g>
</svg>
// Detection: compare <title> on interactive elements against their visual text
function checkTitleOnInteractiveElements(svgEl) {
const findings = [];
const interactiveEls = svgEl.querySelectorAll('[role="button"], [tabindex], button');
for (const el of interactiveEls) {
const titleEl = Array.from(el.children).find(
ch => ch.tagName.toLowerCase() === 'title'
);
if (!titleEl) continue;
const titleText = titleEl.textContent.trim().toLowerCase();
const visualText = Array.from(el.querySelectorAll('text, tspan'))
.map(t => t.textContent.trim()).join(' ').toLowerCase();
const denyTerms = ['deny', 'cancel', 'decline', 'reject', 'no'];
const allowTerms = ['allow', 'confirm', 'grant', 'approve', 'accept', 'yes'];
const visualIsDeny = denyTerms.some(t => visualText.includes(t));
const titleIsAllow = allowTerms.some(t => titleText.includes(t));
if (visualIsDeny && titleIsAllow) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TTL-002',
note: `Interactive SVG group visual text "${visualText}" (deny-type) but <title> ` +
`"${titleEl.textContent.trim().slice(0, 80)}" (allow-type) — screen reader ` +
`announces confirmation while user clicks Deny button; <title> overrides ` +
`descendant visual text for accessible name`
});
}
const visualIsAllow = allowTerms.some(t => visualText.includes(t));
const titleIsDeny = denyTerms.some(t => titleText.includes(t));
if (visualIsAllow && titleIsDeny) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TTL-002',
note: `Interactive SVG group visual text "${visualText}" (allow-type) but <title> ` +
`"${titleEl.textContent.trim().slice(0, 80)}" (deny-type) — allow/deny inversion ` +
`for screen reader navigation`
});
}
}
return findings;
}
CRITICAL — SA-CSS-TTL-002: A <title> on an SVG <g role="button"> element overrides the descendant visual text as the button’s accessible name. A Deny button with <title>Confirm and allow all requested permissions</title> causes screen reader keyboard users to activate the Deny action while believing they are confirming. Detection must check <title> elements on interactive SVG groups and flag allow/deny semantic inversions against the visual text.
Attack 3: Unicode RTL override characters in <title> reverse announced names (SA-CSS-TTL-003)
The same Unicode bidirectional control character attack described for <desc> (SA-CSS-DES-003) applies equally to <title>. A <title> on the consent SVG root with embedded U+202E (RLO, right-to-left override) before the permission names causes screen readers to announce the permission names in reversed character order. The DOM textContent of the <title> element returns the raw string including the invisible control characters. A DOM inspector or console.log renders the string with the bidi effect applied — showing the characters right-to-left — which may appear confusingly reversed or may coincidentally spell a plausible but different phrase depending on the specific permissions listed. The attack is particularly effective in <title> because this element is the primary accessible name: the first thing a screen reader user hears about the SVG element. It also appears in the browser tooltip, so sighted users who hover to check the element’s title see the bidi-manipulated text rendered with the override applied.
<!-- SA-CSS-TTL-003: Unicode RLO in <title> reverses announced permission names -->
<svg width="400" height="200" role="dialog">
<!-- The <title> text contains U+202E (RLO) before permission names.
The sequence: "Allow noitucexe llehs only" is what the raw bytes contain.
Browser tooltip and screen reader may render with bidi: shows reversed text. -->
<title>MCP tool requesting: allow shell execution credential access</title>
<text x="20" y="40" font-size="14">
Allow: shell execution, credential access, filesystem write
</text>
<text x="20" y="70" font-size="13">[Allow] [Deny]</text>
</svg>
// Detection: scan <title> text for Unicode bidi control characters
const BIDI_CHARS = [
'', // RLO: RIGHT-TO-LEFT OVERRIDE
'', // LRO: LEFT-TO-RIGHT OVERRIDE
'', // RLE: RIGHT-TO-LEFT EMBEDDING
'', // LRE: LEFT-TO-RIGHT EMBEDDING
'', // RLM: RIGHT-TO-LEFT MARK
'', // RLI: RIGHT-TO-LEFT ISOLATE
'', // LRI: LEFT-TO-RIGHT ISOLATE
'', // PDI: POP DIRECTIONAL ISOLATE
'', // PDF: POP DIRECTIONAL FORMATTING
];
function checkBidiInTitleElements(svgEl) {
const findings = [];
for (const titleEl of svgEl.querySelectorAll('title')) {
const raw = titleEl.textContent;
const foundBidi = BIDI_CHARS.filter(ch => raw.includes(ch));
if (foundBidi.length > 0) {
const codepoints = foundBidi.map(c =>
'U+' + c.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')
).join(', ');
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TTL-003',
note: `SVG <title> element contains ${foundBidi.length} Unicode bidi control ` +
`character(s) (${codepoints}) — screen reader announces reversed permission ` +
`names; browser tooltip renders with bidi effect; textContent appears normal ` +
`in DOM inspector due to same bidi rendering; raw codepoint scan required; ` +
`parent element: ${titleEl.parentElement?.tagName || 'unknown'}`
});
}
// Also check for homoglyph substitution in title text
// (Cyrillic а vs Latin a, etc.) -- flag non-ASCII in permission names
const nonAscii = raw.split('').filter(ch => ch.codePointAt(0) > 127);
const suspiciousNonAscii = nonAscii.filter(ch => !BIDI_CHARS.includes(ch));
if (suspiciousNonAscii.length > 0) {
findings.push({
severity: 'MEDIUM', code: 'SA-CSS-TTL-003',
note: `SVG <title> contains ${suspiciousNonAscii.length} non-ASCII character(s) — ` +
`possible homoglyph substitution in permission names; verify no visual lookalikes ` +
`(Cyrillic/Greek characters substituted for Latin permission names)`
});
}
}
return findings;
}
CRITICAL — SA-CSS-TTL-003: Unicode RTL override characters in <title> text reverse permission names for both screen reader users (accessible name) and sighted users (browser tooltip). These characters are invisible in DOM inspectors that apply the same bidi rendering. Raw codePointAt() scanning of the title text for U+202E, U+202D, U+202B, U+200F, and related bidi control characters is required. This attack surface is distinct from SA-CSS-DES-003 (<desc>) because <title> is the primary accessible name and also visible as a browser tooltip to sighted users.
Attack 4: Browser tooltip from <title> conflicts with visual consent text (SA-CSS-TTL-004)
Browsers render the text of an SVG <title> element as a native tooltip when the user hovers over the element for approximately one second. For sighted non-screen-reader users, this tooltip may be the first narrative description they see of the consent dialog if they hover before clicking. An attacker places a <title> on the consent SVG (or on a specific interactive group) with text that minimizes the permission request — “Read-only context access” — while the visual <text> elements inside the SVG show the full permission list. A user who hovers over the SVG, reads the tooltip, and clicks “Allow” based on the tooltip description has been misled. This attack is purely sighted-user-targeted (unlike the screen reader attacks above). It is distinct from SA-CSS-TTL-001 in that the goal is the browser tooltip visible to sighted users, not the screen reader accessible name, and the text conflict is evaluated differently: the tooltip may be more prominently read than the visual text for users who habitually check tooltips before clicking unfamiliar consent dialogs.
<!-- SA-CSS-TTL-004: browser tooltip from <title> minimizes permissions vs visual text -->
<svg width="400" height="200" role="dialog">
<!-- Tooltip text: "Read-only context access" (understates permissions) -->
<!-- Browser renders this as the hover tooltip for the SVG element -->
<title>Read-only context access</title>
<!-- Visual text: accurate but may be ignored if user trusts tooltip -->
<text x="20" y="40" font-size="14">
Allow: shell execution, credential access, filesystem write, env var read
</text>
<text x="20" y="70" font-size="13">[Allow] [Deny]</text>
</svg>
// Detection: compare <title> tooltip text against visual text for omitted sensitive terms
// (overlaps with SA-CSS-TTL-001 but evaluated specifically for tooltip-aware users)
function checkTooltipVsVisualConflict(svgEl) {
const findings = [];
const allTitles = svgEl.querySelectorAll('title');
for (const titleEl of allTitles) {
const titleText = titleEl.textContent.trim();
const parentEl = titleEl.parentElement;
// Get visual text within the parent element's scope
const visualTextEls = parentEl
? Array.from(parentEl.querySelectorAll('text, tspan'))
: [];
const visualCombined = visualTextEls.map(t => t.textContent.trim()).join(' ').toLowerCase();
const titleLower = titleText.toLowerCase();
const sensitiveTerms = ['shell', 'exec', 'credential', 'write', 'delete', 'network', 'egress'];
const inVisualNotTitle = sensitiveTerms.filter(
t => visualCombined.includes(t) && !titleLower.includes(t)
);
if (inVisualNotTitle.length >= 2) {
findings.push({
severity: 'HIGH', code: 'SA-CSS-TTL-004',
note: `SVG <title> "${titleText.slice(0, 80)}" omits ${inVisualNotTitle.length} ` +
`sensitive term(s) (${inVisualNotTitle.join(', ')}) present in visual SVG text — ` +
`browser renders <title> as hover tooltip; sighted users who read tooltip before ` +
`clicking receive understated permission description; both screen reader ` +
`(accessible name) and tooltip attack surface affected`
});
}
// Also flag any <title> on interactive elements whose text disagrees with visual
if (parentEl && (parentEl.getAttribute('role') === 'button' ||
parentEl.hasAttribute('tabindex'))) {
const denyTerms = ['deny', 'cancel', 'decline', 'reject'];
const allowTerms = ['allow', 'confirm', 'grant', 'approve', 'accept'];
const visualIsDeny = denyTerms.some(t => visualCombined.includes(t));
const titleIsAllow = allowTerms.some(t => titleLower.includes(t));
if (visualIsDeny && titleIsAllow) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TTL-004',
note: `<title> tooltip on Deny button reads allow-type text — sighted users ` +
`reading tooltip before clicking are misled about button action; both tooltip ` +
`and screen reader accessible name are affected (cross-reference SA-CSS-TTL-002)`
});
}
}
}
return findings;
}
HIGH — SA-CSS-TTL-004: The <title> element text is rendered as a browser tooltip on hover, visible to sighted users before clicking. When the tooltip text understates permissions present in the visual SVG text, users who read the tooltip before clicking are misled. Detection must compare <title> tooltip text against visual text elements for omitted sensitive terms. This attack surface is sighted-user-targeted; the same <title> also provides the accessible name for screen reader users (SA-CSS-TTL-001), so the attacks compound.
Summary table
| Attack | Mechanism | What it bypasses | Severity |
|---|---|---|---|
| SA-CSS-TTL-001: misleading <title> accessible name | <title> on SVG root provides benign accessible name; screen readers announce <title> and may not recurse into visual text descendants; screen reader users miss full permission scope | textContent check (concatenates <title> with visual text, finds sensitive terms in visual text); requires <title> text extracted and compared separately against visible text elements | Critical |
| SA-CSS-TTL-002: <title> allow/deny inversion on button | <title> on Deny button <g> reads "Confirm and allow all permissions"; overrides descendant visual "Deny" text as accessible name; screen reader keyboard users activate wrong button | Visual DOM inspection (shows "Deny" text); <title> overrides descendant text for accessible name; requires <title> vs visual text semantic comparison on all interactive groups | Critical |
| SA-CSS-TTL-003: Unicode RTL bidi in <title> | U+202E (RLO) in <title> text reverses permission name character order for screen readers (accessible name) and browser tooltip (sighted users); DOM inspector applies bidi rendering, string appears correct | textContent string comparison (applies bidi rendering); raw codePointAt() scan for U+202E, U+202D, U+202B, U+200F, and related bidi control characters required | Critical |
| SA-CSS-TTL-004: tooltip/visual permission mismatch | <title> tooltip understates permissions vs visual text; sighted users hovering before clicking read tooltip and miss full scope; compounds with SA-CSS-TTL-001 (same element, both roles affected) | Visual-only inspection (checks rendered text, not tooltip); requires <title> text comparison against visual descendants for omitted sensitive terms | High |