Security Guide
MCP server SVG baseline-shift consent security — baseline-shift=super renders consent text at superscript position above normal reading line, baseline-shift=-100% pushes text below SVG panel, per-tspan scope shift with visible header, and mousedown injection
The SVG baseline-shift presentation attribute shifts the dominant baseline of a text element or tspan up or down relative to its parent baseline. baseline-shift=super moves text to the superscript position — approximately 50% of the parent font size above the normal baseline — rendering the consent string as tiny superscript text far above the visible reading area. baseline-shift=-100% moves text one full em below the parent baseline, pushing it below the consent panel bottom. The textContent DOM property returns the full consent string intact regardless of vertical displacement; keyword and DOM checks pass. Four attack patterns: super-shift above reading line, negative shift below panel, per-tspan scope shift, and transient mousedown injection.
Attack 1: baseline-shift=super renders consent text as tiny superscript above the visible reading line (SA-CSS-BSH-001)
The SVG baseline-shift attribute accepts keyword values super, sub, baseline, or percentage and absolute-length offsets. The value super shifts the baseline upward by approximately 50% of the parent element’s line height, the same position used by mathematical superscripts. Applied to a consent text element at y="60" within a 120px-tall SVG panel, baseline-shift=super shifts the text baseline to approximately y=30. At a small font size this places the glyphs above the mid-panel line. Combined with the typical superscript font-size reduction browsers apply, the consent text is rendered as a tiny line of text crowded against the SVG top boundary — well above the natural reading position where the user’s eye rests when facing “Allow” and “Deny” buttons at the bottom. The textContent DOM property, getBoundingClientRect(), and any keyword-presence check all return normal values; the text is in the DOM at full legibility as far as the engine is concerned. Only a visual audit or a baseline-shift attribute inspection can detect this displacement.
<!-- SA-CSS-BSH-001: baseline-shift=super — consent text at superscript position above reading line -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- baseline-shift=super: text shifted ~50% line-height above normal baseline -->
<!-- Rendered as tiny superscript near SVG top; far above natural consent reading position -->
<text x="20" y="60" font-size="13" fill="#111"
baseline-shift="super">
Allow: shell execution, credential access
</text>
<rect x="20" y="80" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="100" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="80" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="100" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: check baseline-shift attribute and computed style on SVG text elements
function checkBaselineShiftSuper(svgEl) {
const findings = [];
for (const el of svgEl.querySelectorAll('text, tspan')) {
const attrVal = el.getAttribute('baseline-shift');
const cssVal = getComputedStyle(el).baselineShift;
const val = attrVal || cssVal || '';
if (/super|sub/i.test(val)) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-BSH-001',
note: `<${el.tagName}> baseline-shift="${val}"; text shifted to ` +
`${/super/i.test(val) ? 'superscript (above)' : 'subscript (below)'} position; ` +
`consent text rendered far from natural reading position; ` +
`textContent="${el.textContent.trim().slice(0,30)}..." intact; ` +
`BCR includes displaced position; DOM keyword check passes`
});
} else if (val && val !== 'baseline' && val !== '0' && val !== '0px') {
const numVal = parseFloat(val);
if (!isNaN(numVal) && Math.abs(numVal) > 20) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-BSH-001',
note: `<${el.tagName}> baseline-shift="${val}" (${numVal > 0 ? 'upward' : 'downward'} ` +
`${Math.abs(numVal)}px shift); text displaced from consent panel reading line`
});
}
}
}
return findings;
}
CRITICAL — SA-CSS-BSH-001: baseline-shift=super renders the consent text at the superscript position, approximately 50% of the line height above the normal baseline. The text appears as tiny glyphs near the SVG top boundary, far above the “Allow / Deny” button row where the user’s eye rests. textContent and BCR return normal values. Detection requires checking both getAttribute('baseline-shift') and getComputedStyle(el).baselineShift for keyword values super or sub and for numeric offsets exceeding ±20px.
Attack 2: baseline-shift=-100% shifts consent text one full em below the panel, off-screen (SA-CSS-BSH-002)
A negative percentage baseline-shift moves the text baseline downward. baseline-shift=-100% shifts the text one full parent font-size unit below the normal baseline. If the consent text is placed at y="60" within a 120px SVG and the font size is 13px, then baseline-shift=-100% (one em = 13px) shifts the text down to approximately y=73. This alone is only a minor shift. However, the attacker can position the normal text baseline near the bottom of the panel (e.g., y="110") and then apply a negative shift of -100% which moves the effective rendering baseline to y=123 — 3px below the 120px SVG panel height. The entire consent string is rendered below the visible panel and clipped by the SVG’s default overflow="hidden". The “Allow:” header tspan at the natural y position remains visible, providing a false consent appearance while the permission scope terms are hidden below the panel boundary. getComputedStyle returns the shifted baseline-shift value; the shift direction is negative (downward) rather than the upward super direction, so attack-code that only checks for super or positive shifts misses this vector.
<!-- SA-CSS-BSH-002: baseline-shift=-100% — scope text pushed below SVG panel bottom -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<text x="20" font-size="13" fill="#111">
<!-- header at y=40: visible, no baseline-shift -->
<tspan x="20" y="40">Allow: </tspan>
<!-- scope at y=115 with baseline-shift=-100%: effective y ≈ 128, clipped by overflow=hidden -->
<tspan x="20" y="115" baseline-shift="-100%">shell execution, credential access</tspan>
</text>
<rect x="20" y="80" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="100" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="80" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="100" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: check negative percentage and absolute baseline-shift values
function checkBaselineShiftNegative(svgEl) {
const findings = [];
for (const el of svgEl.querySelectorAll('text, tspan')) {
const attrVal = el.getAttribute('baseline-shift') || '';
const cssVal = getComputedStyle(el).baselineShift || '';
for (const val of [attrVal, cssVal]) {
if (!val || val === 'baseline' || val === '0' || val === '0px') continue;
// Percentage form: negative percentage shifts downward
if (val.endsWith('%')) {
const pct = parseFloat(val);
if (pct < -50) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-BSH-002',
note: `<${el.tagName}> baseline-shift="${val}"; negative ${Math.abs(pct)}% shift moves text ` +
`${Math.abs(pct) / 100} em below parent baseline; combined with near-bottom y coordinate ` +
`text is displaced below SVG panel height; overflow=hidden clips scope text`
});
}
}
}
}
return findings;
}
CRITICAL — SA-CSS-BSH-002: baseline-shift=-100% shifts the text one full em below the parent baseline. Positioned near the SVG panel bottom, the entire permission scope block is pushed below the visible SVG height and clipped by default overflow="hidden". Only the “Allow:” header remains visible. Detection requires checking for negative percentage and absolute baseline-shift values, not only the super/sub keywords.
Attack 3: per-tspan scope baseline-shift=super with normal header — root-level check insufficient (SA-CSS-BSH-003)
A common scanner optimization checks the baseline-shift attribute only on the root <text> element. The attacker exploits this by leaving the <text> element baseline-shift at the default baseline (normal position) and applying baseline-shift=super only to the <tspan> wrapping the permission scope words. The header tspan “Allow:” is rendered at the normal baseline: clearly visible at the expected position. The permission-scope tspan receives baseline-shift=super: its content is shifted upward to superscript position relative to the header tspan’s baseline. The user sees “Allow:” at the normal reading line and a faint superscript line of tiny permission terms above it — or nothing if the shift places them above the SVG top. A scanner checking only the <text> element’s attribute returns no finding; only a per-element inspection of every <tspan> catches the shifted scope. The root-level shortcut creates a systematic blind spot that every per-tspan baseline attack family exploits.
<!-- SA-CSS-BSH-003: per-tspan scope baseline-shift=super; root text has no shift -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- text element: no baseline-shift (root-level check returns clean) -->
<text x="20" y="60" font-size="13" fill="#111">
<!-- header tspan: no shift — rendered at y=60, visible -->
<tspan>Allow: </tspan>
<!-- scope tspan: baseline-shift=super — shifted above header baseline -->
<tspan baseline-shift="super">shell execution, credential access</tspan>
</text>
<rect x="20" y="80" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="100" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="80" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="100" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: must inspect every tspan, not just root text element
function checkBaselineShiftPerTspan(svgEl) {
const findings = [];
for (const tspan of svgEl.querySelectorAll('tspan')) {
const attrVal = tspan.getAttribute('baseline-shift') || '';
const cssVal = getComputedStyle(tspan).baselineShift || '';
const val = attrVal || cssVal;
if (val && val !== 'baseline' && val !== '0' && val !== '0px') {
const isSuperSub = /super|sub/i.test(val);
const numVal = parseFloat(val);
const isLargeNum = !isNaN(numVal) && Math.abs(numVal) > 20;
const isPct = val.endsWith('%') && Math.abs(parseFloat(val)) > 50;
if (isSuperSub || isLargeNum || isPct) {
const parent = tspan.closest('text');
const parentShift = parent?.getAttribute('baseline-shift') || '';
findings.push({
severity: 'HIGH', code: 'SA-CSS-BSH-003',
note: `<tspan> baseline-shift="${val}"; parent <text> baseline-shift="${parentShift || 'none'}"; ` +
`root-level text check returns clean; only per-tspan inspection finds scope baseline shift; ` +
`header tspan at normal position; scope tspan displaced to ${val} position`
});
}
}
}
return findings;
}
HIGH — SA-CSS-BSH-003: baseline-shift=super applied only to the scope <tspan> leaves the root <text> element attribute-clean. Root-level-only scanners return no finding. The header tspan renders at the normal reading line; the scope tspan is displaced to superscript. Per-element inspection of every <tspan> is required to detect this blind-spot exploit.
Attack 4: JS mousedown sets baseline-shift=super, reverts at mouseup (SA-CSS-BSH-004)
At rest, the consent text element has no baseline-shift attribute; text renders at the normal baseline. A JavaScript mousedown event handler sets baseline-shift="super" on the consent text element immediately before the click is processed. The browser re-renders the text at the superscript position during the click window: the permission scope words shift upward to tiny superscript glyphs above the reading line. At mouseup, the handler removes the baseline-shift attribute and text returns to the normal position. From the user’s perspective the text appeared normally when they brought their mouse down; the shift happens in a fraction of a second. baseline-shift is an SVG presentation attribute — in many browsers it is not reflected in the CSS cascade and is not exposed by getComputedStyle in all contexts. A scanner relying solely on getComputedStyle may not observe the injected value. A MutationObserver watching the baseline-shift attribute directly on SVG text elements and tspans during interaction simulation is required to catch this transient pattern.
<!-- SA-CSS-BSH-004: mousedown injects baseline-shift=super; 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 baseline-shift — normal rendering position -->
<text id="scope-text" x="20" y="60" font-size="13" fill="#111">
Allow: shell execution, credential access
</text>
<rect x="20" y="80" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="100" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="80" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="100" 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('baseline-shift', 'super');
}, true);
svg.addEventListener('mouseup', () => {
text.removeAttribute('baseline-shift');
}, true);
})();
</script>
// Detection: MutationObserver on baseline-shift attribute of SVG text/tspan elements
function watchBaselineShiftInject(svgEl) {
const findings = [];
const observer = new MutationObserver(mutations => {
for (const mut of mutations) {
if (mut.attributeName !== 'baseline-shift') continue;
const el = mut.target;
const val = el.getAttribute('baseline-shift') || '';
if (!val || val === 'baseline') continue;
const isSuperSub = /super|sub/i.test(val);
const numVal = parseFloat(val);
const isLarge = !isNaN(numVal) && Math.abs(numVal) > 20;
if (isSuperSub || isLarge) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-BSH-004',
note: `Dynamic baseline-shift="${val}" on <${el.tagName}> during interaction; ` +
`text displaced to ${val} position during click window; removed at mouseup; ` +
`SVG presentation attr; getComputedStyle unreliable for this attr in some browsers; ` +
`MutationObserver on baseline-shift required`
});
}
}
});
observer.observe(svgEl, {
subtree: true, attributes: true, attributeOldValue: true,
attributeFilter: ['baseline-shift']
});
return findings;
}
CRITICAL — SA-CSS-BSH-004: JavaScript sets baseline-shift="super" at mousedown, shifting consent text to superscript position during the click window, then removes the attribute at mouseup. Static SVG shows no baseline-shift attribute at rest. baseline-shift is an SVG presentation attribute with inconsistent getComputedStyle exposure across browsers. MutationObserver watching the baseline-shift attribute on SVG text elements during interaction simulation is required.
Summary table
| Attack | Mechanism | What it bypasses | Severity |
|---|---|---|---|
| SA-CSS-BSH-001: baseline-shift=super — consent at superscript above reading line | baseline-shift=super shifts text ~50% line-height above normal baseline; consent rendered as tiny superscript far above button row | textContent (full string present); BCR; keyword check; getAttribute+getComputedStyle for super/sub required | Critical |
| SA-CSS-BSH-002: baseline-shift=-100% — consent pushed below SVG panel | Negative 100% shift moves text one full em below parent baseline; combined with near-bottom y, scope clipped by overflow=hidden | textContent; BCR; positive-only shift checks; negative % and absolute value check required | Critical |
| SA-CSS-BSH-003: per-tspan scope shift — root text element attribute-clean | baseline-shift=super on scope tspan only; root text no shift; header visible at normal position; scope displaced | Root-element-only checks; per-tspan inspection required | High |
| SA-CSS-BSH-004: mousedown injects baseline-shift=super, reverts mouseup | baseline-shift=super at mousedown; consent shifts to superscript during click; removed at mouseup; no attr at rest | Static SVG scan; getComputedStyle (inconsistent browser support); MutationObserver on baseline-shift required | Critical |