Security Guide
MCP server SVG text-anchor consent security — text-anchor="end" with x="0" pushes entire consent string off-screen left, text-anchor="middle" with large negative x centers off-viewport, per-tspan mixed anchor hides scope text, and mousedown injection
The SVG text-anchor presentation attribute controls where the text element’s anchor point x coordinate aligns to the text string — at the start of the first glyph (start), at the geometric center of the string (middle), or at the end of the last glyph (end). When text-anchor="end" is combined with x="0", the last character of the consent string is positioned at x=0, and every preceding character extends to the left, outside the SVG viewport. The entire consent string is invisible. The textContent DOM property returns the full permission request; getBoundingClientRect returns the SVG element container bounds — not the off-screen text position. DOM-based keyword checks pass. Four attack patterns: end+x=0 off-screen, middle+negative x, per-tspan mixed anchor, and transient mousedown injection.
Attack 1: text-anchor="end" with x="0" pushes entire consent string off-screen left (SA-CSS-TAC-001)
The SVG text-anchor attribute defines the alignment of a text string relative to its positioning anchor point. With the default value start, the first character of the text is placed at the x coordinate and the string extends to the right. With text-anchor="end", the last character is placed at the x coordinate and the string extends to the left. An attacker exploits this by setting text-anchor="end" and x="0": the last character of the consent string is positioned at x=0 (the left edge of the SVG coordinate system), and every preceding character extends further to the left, outside the SVG viewport. A consent string of 40 characters at 8px average glyph advance width extends 320px to the left of x=0 — entirely outside the visible SVG area. The SVG element itself clips content at its viewport boundary by default (overflow="hidden"). The consent text is present in the DOM, returns its full string via textContent, and has a getBoundingClientRect that reflects the SVG element bounds — not the text rendering position.
<!-- SA-CSS-TAC-001: text-anchor=end + x=0 pushes entire consent string off-screen left -->
<svg width="440" height="120" overflow="hidden" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- text-anchor=end: last char at x=0; entire string extends left into negative x (off-screen) -->
<!-- "Allow: shell execution, credential access" — rendered at x=-300+ (invisible) -->
<!-- textContent = "Allow: shell execution, credential access" (intact) -->
<text x="0" y="40" text-anchor="end" 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>
// Detection: check text-anchor=end with x coordinate near or at 0
function checkTextAnchorEnd(svgEl) {
const findings = [];
for (const textEl of svgEl.querySelectorAll('text, tspan')) {
const anchor = textEl.getAttribute('text-anchor')
|| getComputedStyle(textEl).textAnchor || '';
if (/end/i.test(anchor)) {
const xVal = parseFloat(textEl.getAttribute('x') || '0');
// text-anchor=end: string extends LEFT from x; x near 0 means off-screen
// Estimate text width: approximate by textContent.length * avg glyph width
const approxTextWidth = (textEl.textContent.trim().length) * 8;
if (xVal - approxTextWidth < 0) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TAC-001',
note: `<${textEl.tagName}> text-anchor="end" x="${xVal}"; ` +
`string extends ~${approxTextWidth}px left from x=${xVal}; ` +
`estimated rendering start at x=${xVal - approxTextWidth} (off-screen); ` +
`textContent="${textEl.textContent.trim().slice(0,40)}..." intact; ` +
`BCR returns SVG container bounds — does not reflect off-screen text`
});
}
}
}
return findings;
}
CRITICAL — SA-CSS-TAC-001: text-anchor="end" with x="0" positions the end of the entire consent string at the left edge of the SVG coordinate system. The string renders entirely in the negative-x region, outside the SVG viewport. textContent returns the full permission string; getBoundingClientRect returns SVG container bounds; DOM keyword checks pass. Detection requires checking text-anchor via both getAttribute and getComputedStyle, computing the estimated rendering start position, and flagging cases where the string extends outside the SVG viewport.
Attack 2: text-anchor="middle" with large negative x centers consent text far off-screen (SA-CSS-TAC-002)
With text-anchor="middle", the geometric center of the text string is positioned at the anchor x coordinate. An attacker sets a large negative x value — for example, x="-500" — so that the center of the consent string is at x=-500, and the entire string spans from approximately x=-700 to x=-300, entirely outside the visible SVG viewport. The attack is less obvious than the end+x=0 pattern because the attacker can choose any large negative value, making simple threshold checks less reliable. The defender must compute the left edge of the text rendering region: left edge = x − (approx_text_width / 2). If this is negative, the string is off-screen.
<!-- SA-CSS-TAC-002: text-anchor=middle + large negative x centers text far off-screen -->
<svg width="440" height="120" overflow="hidden" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- text-anchor=middle: center of string at x=-500 — renders from x≈-700 to x≈-300 (all off-screen) -->
<text x="-500" 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>
// Detection: check text-anchor=middle with x far off-screen
function checkTextAnchorMiddle(svgEl) {
const findings = [];
for (const textEl of svgEl.querySelectorAll('text, tspan')) {
const anchor = textEl.getAttribute('text-anchor')
|| getComputedStyle(textEl).textAnchor || '';
if (/middle/i.test(anchor)) {
const xVal = parseFloat(textEl.getAttribute('x') || '0');
const approxHalfWidth = (textEl.textContent.trim().length) * 4;
const leftEdge = xVal - approxHalfWidth;
if (leftEdge < 0) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TAC-002',
note: `<${textEl.tagName}> text-anchor="middle" x="${xVal}"; ` +
`estimated left edge at x=${leftEdge} (off-screen); ` +
`textContent intact; string centered far outside SVG viewport`
});
}
}
}
return findings;
}
CRITICAL — SA-CSS-TAC-002: text-anchor="middle" with a large negative x value centers the consent string far outside the SVG viewport. The left edge of the rendered text is at approximately x − (text_width/2); if this is negative, the string is off-screen. Detection requires estimating string width from character count and average glyph advance, then checking the resulting rendered region against SVG viewport bounds.
Attack 3: per-tspan mixed text-anchor — header visible at start, scope hidden at end with x="0" (SA-CSS-TAC-003)
The root <text> element uses text-anchor="start" with a normal positive x coordinate — the “Allow:” header renders visibly. A child <tspan> element containing the permission scope words overrides text-anchor="end" and sets x="0". The tspan is rendered with its end at x=0, entirely to the left, outside the viewport. The parent <text> element reports text-anchor="start" when queried at the element level — the attack is invisible at the root element. A root-level scan that checks the <text> element’s text-anchor attribute finds start (normal) and passes. Only a per-tspan scan catches the scope tspan’s text-anchor="end"+x="0" combination.
<!-- SA-CSS-TAC-003: per-tspan mixed — header visible (start), scope off-screen (end+x=0) -->
<svg width="440" height="120" overflow="hidden" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<text x="20" y="40" text-anchor="start" font-size="13" fill="#111">
<!-- "Allow:" renders normally at x=20 -->
<tspan>Allow: </tspan>
<!-- scope words: text-anchor=end + x=0 → all characters to left of x=0, off-screen -->
<tspan x="0" text-anchor="end">shell execution, 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: per-tspan text-anchor check
function checkTextAnchorPerTspan(svgEl) {
const findings = [];
for (const tspan of svgEl.querySelectorAll('tspan')) {
const anchor = tspan.getAttribute('text-anchor')
|| getComputedStyle(tspan).textAnchor || '';
const xVal = parseFloat(tspan.getAttribute('x') ?? '');
if (/end/i.test(anchor) && !isNaN(xVal)) {
const approxTextWidth = tspan.textContent.trim().length * 8;
if (xVal - approxTextWidth < 0) {
findings.push({
severity: 'HIGH', code: 'SA-CSS-TAC-003',
note: `<tspan> text-anchor="end" x="${xVal}"; scope text pushes off-screen; ` +
`parent <text> text-anchor is start — root-level check insufficient; ` +
`per-tspan inspection required`
});
}
}
if (/middle/i.test(anchor) && !isNaN(xVal)) {
const approxHalfWidth = tspan.textContent.trim().length * 4;
if (xVal - approxHalfWidth < 0) {
findings.push({
severity: 'HIGH', code: 'SA-CSS-TAC-003',
note: `<tspan> text-anchor="middle" x="${xVal}"; scope text centered off-screen; ` +
`per-tspan inspection required`
});
}
}
}
return findings;
}
HIGH — SA-CSS-TAC-003: A <tspan> overrides text-anchor="end" and x="0" for the permission-scope words only. The parent <text> element reports text-anchor="start"; a root-level check passes. Per-tspan inspection of both text-anchor and the x coordinate is required to detect scope-only off-screen positioning.
Attack 4: JS mousedown sets text-anchor="end" and x="0", reverts at mouseup (SA-CSS-TAC-004)
At rest, the consent text element has a normal text-anchor="start" and a positive x coordinate. The text renders visibly in the consent panel. A JavaScript mousedown handler simultaneously sets text-anchor="end" and x="0": both attributes change at the same time, and the browser repaints the text at the off-screen position during the click window. The mouseup handler restores both attributes to their original values. A static SVG scan finds text-anchor="start" and a positive x value — no attack apparent. The attack requires a MutationObserver watching both text-anchor and x attributes simultaneously. Setting only text-anchor="end" without adjusting x may not push the text off-screen (depending on the original x position); the combination of both is required for reliable off-screen displacement.
<!-- SA-CSS-TAC-004: mousedown sets text-anchor=end + x=0; reverts mouseup -->
<svg id="consent-svg" width="440" height="120" overflow="hidden" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- At rest: text-anchor=start x=20 — normal visible consent text -->
<text id="scope-text" x="20" y="40" text-anchor="start" 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');
const origX = text.getAttribute('x');
svg.addEventListener('mousedown', () => {
text.setAttribute('text-anchor', 'end');
text.setAttribute('x', '0');
}, true);
svg.addEventListener('mouseup', () => {
text.setAttribute('text-anchor', 'start');
text.setAttribute('x', origX || '20');
}, true);
})();
</script>
// Detection: MutationObserver on text-anchor AND x attributes
function watchTextAnchorInject(svgEl) {
const findings = [];
const observer = new MutationObserver(mutations => {
for (const mut of mutations) {
if (mut.attributeName !== 'text-anchor' && mut.attributeName !== 'x') continue;
const el = mut.target;
const anchor = el.getAttribute('text-anchor') || '';
const xVal = parseFloat(el.getAttribute('x') || '0');
const approxTextWidth = el.textContent.trim().length * 8;
if (/end/i.test(anchor) && xVal - approxTextWidth < 0) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TAC-004',
note: `Dynamic text-anchor="end" x="${xVal}" on <${el.tagName}>; ` +
`string pushed off-screen (estimated left edge at x=${xVal - approxTextWidth}) ` +
`during click window; DOM shows normal values at rest; ` +
`MutationObserver on text-anchor and x attributes required`
});
}
}
});
observer.observe(svgEl, {
subtree: true, attributes: true, attributeOldValue: true,
attributeFilter: ['text-anchor', 'x']
});
return findings;
}
CRITICAL — SA-CSS-TAC-004: JavaScript simultaneously sets text-anchor="end" and x="0" at mousedown, pushing the consent string off-screen during the click window. Both attributes are restored at mouseup. DOM shows normal values at rest. MutationObserver watching both text-anchor and x attributes during interaction simulation is required to catch the combined effect.
Summary table
| Attack | Mechanism | What it bypasses | Severity |
|---|---|---|---|
| SA-CSS-TAC-001: text-anchor=end + x=0 off-screen left | String end at x=0; entire string extends to negative x, outside SVG viewport; default overflow=hidden clips text | textContent keyword scan (all keywords present); BCR (returns SVG container bounds); display/visibility | Critical |
| SA-CSS-TAC-002: text-anchor=middle + negative x centers off-screen | String center at large negative x; string spans entirely in negative-x region; width estimate required to detect | textContent; BCR; x-coordinate threshold check (negative x alone insufficient without anchor-aware width estimate) | Critical |
| SA-CSS-TAC-003: per-tspan mixed anchor — scope tspan off-screen | Parent text-anchor=start (normal); scope tspan overrides text-anchor=end + x=0; root-level check passes | Root text element attribute check; textContent; per-tspan x coordinate inspection required | High |
| SA-CSS-TAC-004: mousedown sets end+x=0, reverts at mouseup | Both text-anchor and x set simultaneously at mousedown; string off-screen during click; restored at mouseup; DOM clean at rest | Static SVG scan; CSS stylesheet scan; MutationObserver on both text-anchor and x required | Critical |