MCP server CSS baseline-shift security: baseline-shift:super accumulation, baseline-shift:100% line box overflow, SVG tspan negative dy stacking, and sub/super oscillation attacks on SVG consent text
Published 2026-07-25 — SkillAudit Research
The CSS baseline-shift property (SVG 1.1, CSS Inline Level 3) shifts an inline text element vertically relative to its parent's dominant baseline. Unlike vertical-align (which shifts an HTML inline element within its line box), baseline-shift operates within an SVG text context and directly adjusts the glyph rendering position without changing the element's layout dimensions. The values are sub (shift down by the subscript offset), super (shift up by the superscript offset), a length (absolute vertical shift), or a percentage (shift as a percentage of the element's line-height).
The critical attack characteristic of baseline-shift is that it accumulates across nested SVG <tspan> elements: each nested tspan with a baseline-shift value adds its shift to the parent's shift. Five nested tspan elements each with baseline-shift: super (approximately 40% of font-size per level) accumulate to a ~200% upward shift — at 14px font size, this is approximately 28px upward. If the consent text in an SVG is positioned at y=20px, a 28px upward shift places glyphs at y ≈ −8px, above the SVG viewBox top edge (clipped). The SVG element renders as a blank rectangle.
baseline-shift in SVG vs HTML: In HTML, vertical-align: super shifts inline elements within their line box, and the line box height expands to contain the shifted element. In SVG, baseline-shift: super shifts the glyph position in SVG user units, and the shift can place text outside the SVG viewport (clipped by the viewBox). The SVG viewBox clip does not expand to contain shifted text — it clips it. This makes SVG consent panels fundamentally more vulnerable to baseline-shift attacks than HTML-based consent panels.
Attack 1: nested tspan baseline-shift:super accumulation — consent text shifted above SVG viewBox
Each <tspan> child element with baseline-shift: super shifts its text upward by approximately 33–40% of the current font size (browser-dependent, derived from the font's superscript offset metric). Nested tspans accumulate these shifts: the innermost tspan has its glyphs shifted by the sum of all parent tspan shifts. An MCP server injects a structure of 5–6 nested tspans around the consent text content, each with baseline-shift: super. The accumulated shift moves the consent text above the SVG viewBox top boundary.
/* Normal SVG consent structure: */
<svg viewBox="0 0 600 200" width="600" height="200">
<text x="10" y="20" font-size="14">
By continuing you consent to data processing...
</text>
</svg>
/* MCP attack — inject nested tspans with accumulated baseline-shift:super: */
/* Step 1: MCP wraps consent text in nested tspans via DOM manipulation */
const consentText = document.querySelector('.consent-svg text');
let content = consentText.textContent;
consentText.textContent = '';
// Build 6 nested tspans, each with baseline-shift:super
let currentEl = consentText;
for (let i = 0; i < 6; i++) {
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan.setAttribute('baseline-shift', 'super');
// baseline-shift:super ≈ 33-40% of font-size per level
// 6 levels × 35% × 14px ≈ 6 × 4.9px = 29.4px upward
currentEl.appendChild(tspan);
currentEl = tspan;
}
currentEl.textContent = content; // consent text is in the innermost tspan
/* Result:
Original text y position: y=20px
After 6 × baseline-shift:super accumulation: position ≈ 20 - 29.4 = -9.4px
SVG viewBox starts at y=0 — text is at y=-9.4px, above viewBox.
All glyphs are clipped. SVG element appears blank. */
/* CSS variant — apply baseline-shift via CSS on nested elements: */
.consent-svg tspan.shift-1 { baseline-shift: 40%; }
.consent-svg tspan.shift-2 { baseline-shift: 40%; }
.consent-svg tspan.shift-3 { baseline-shift: 40%; }
.consent-svg tspan.shift-4 { baseline-shift: 40%; }
/* 4 levels × 40% × 14px = 22.4px upward shift */
// Detection
function detectBaselineShiftAccumulation() {
document.querySelectorAll('svg text, svg tspan').forEach(el => {
const bs = el.getAttribute('baseline-shift') || window.getComputedStyle(el).baselineShift;
if (bs && bs !== '0' && bs !== '0px' && bs !== 'baseline') {
// Count depth of nested baseline-shift elements
let depth = 0;
let parent = el.parentElement;
while (parent && (parent.tagName === 'text' || parent.tagName === 'tspan')) {
const pbs = parent.getAttribute('baseline-shift') || window.getComputedStyle(parent).baselineShift;
if (pbs && pbs !== '0' && pbs !== '0px' && pbs !== 'baseline') depth++;
parent = parent.parentElement;
}
if (depth >= 2) {
console.error('SECURITY: nested baseline-shift accumulation on SVG consent text:', {
el, baselineShift: bs, nestingDepth: depth,
note: `${depth+1} levels of baseline-shift may shift text outside SVG viewBox`
});
}
}
});
}
Attack 2: baseline-shift:100% absolute percentage shift overflows line box
When baseline-shift is specified as a percentage, it is computed relative to the element's line-height. With baseline-shift: 100% and line-height: 1.5em at 14px font, the shift is 100% × (1.5 × 14px) = 21px upward. If the SVG text's y coordinate is 10px, the actual glyph position is at y = 10 − 21 = −11px — above the viewBox. The attack is effective even with a single (non-nested) baseline-shift value if the percentage is high enough. baseline-shift: 500% would shift the text 500% × line-height upward, reliably exceeding any reasonable viewBox height.
/* MCP attack — extreme percentage baseline-shift: */
/* CSS targeting consent SVG text: */
.consent-svg text,
.consent-svg tspan {
baseline-shift: 500%; /* 500% of line-height upward */
/* At 14px font with line-height:1.5:
500% × (1.5 × 14px) = 500% × 21px = 105px upward.
Text at any y value within a 200px-tall viewBox will be shifted
more than 100px above the top — well outside the viewBox clip. */
}
/* More subtle: 120% baseline-shift — just enough to overflow typical viewBox: */
.consent-svg text:not(:first-child) {
baseline-shift: 120%;
/* Targets all text elements except the first (which contains only
the innocuous title "Privacy Preferences").
The actual consent body text (all subsequent text elements) is shifted up. */
}
// Detection
function detectPercentageBaselineShift() {
document.querySelectorAll('svg text, svg tspan').forEach(el => {
const bs = el.getAttribute('baseline-shift') || '';
if (/%/.test(bs)) {
const pct = parseFloat(bs);
if (pct > 50) {
// Get the SVG element to check viewBox dimensions
const svg = el.closest('svg');
const vb = svg?.viewBox.baseVal;
const cs = window.getComputedStyle(el);
const lh = parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.2;
const shiftPx = (pct / 100) * lh;
console.error('SECURITY: extreme percentage baseline-shift on SVG consent text:', {
el, baselineShift: bs, estimatedShiftPx: shiftPx,
viewBoxHeight: vb?.height,
note: shiftPx > (vb?.height || 0) * 0.5
? 'Shift likely exceeds viewBox height — text may be clipped'
: 'Shift may partially clip text'
});
}
}
});
}
Attack 3: SVG tspan dy attribute negative stacking — independent of baseline-shift property
The SVG dy attribute on <tspan> elements provides an alternative mechanism to baseline-shift: it applies a vertical offset to the glyph position in SVG user coordinate units. Like baseline-shift, dy values accumulate across nested tspan elements. Unlike baseline-shift (which is processed by CSS and detectable via getComputedStyle), dy is an SVG presentation attribute — it is not reflected in computed CSS properties and requires attribute inspection to detect.
/* MCP attack — negative dy accumulation via nested tspans: */
/* SVG structure with accumulated negative dy: */
<svg viewBox="0 0 600 200">
<text x="10" y="180">
<tspan dy="-50"> <!-- shift up 50px: y=130 -->
<tspan dy="-50"> <!-- shift up another 50px: y=80 -->
<tspan dy="-50"> <!-- shift up another 50px: y=30 -->
<tspan dy="-40"> <!-- shift up another 40px: y=-10 -->
By clicking Accept you consent to the collection, processing,
and international transfer of your personal data...
</tspan>
</tspan>
</tspan>
</tspan>
</text>
</svg>
/* Total dy: -190px from y=180 → final y = -10px → above viewBox (clips at y=0).
The SVG element is 600×200px; the consent text block is at y=-10px.
Only descenders of the tallest characters may be visible (at y=0 to y=4px).
All readable consent text is above the viewBox. */
/* JS injection approach: */
function injectNegativeDy(svgTextEl, targetShiftPx) {
const content = svgTextEl.textContent;
svgTextEl.textContent = '';
const levels = Math.ceil(targetShiftPx / 50);
let current = svgTextEl;
for (let i = 0; i < levels; i++) {
const ts = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
ts.setAttribute('dy', String(-Math.min(50, targetShiftPx - i * 50)));
current.appendChild(ts);
current = ts;
}
current.textContent = content;
}
// Detection: check SVG tspan dy attributes for negative accumulation
function detectNegativeDyAccumulation() {
document.querySelectorAll('svg text').forEach(textEl => {
let totalDy = 0;
textEl.querySelectorAll('tspan').forEach(ts => {
const dy = parseFloat(ts.getAttribute('dy') || '0');
totalDy += dy;
});
if (totalDy < -10) {
const svg = textEl.closest('svg');
const vb = svg?.viewBox.baseVal;
const y = parseFloat(textEl.getAttribute('y') || '0');
const effectiveY = y + totalDy;
if (effectiveY < (vb?.y || 0)) {
console.error('SECURITY: SVG tspan negative dy accumulation shifts consent text above viewBox:', {
textEl, totalDy, startY: y, effectiveY,
viewBoxTop: vb?.y
});
}
}
});
}
Attack 4: alternating sub/super baseline-shift oscillation makes consent text unreadable
Instead of shifting all consent text in one direction, an MCP server can apply alternating baseline-shift: super and baseline-shift: sub to individual characters or words within the consent text — making different words appear at different vertical positions. Legal reference strings like "Article 6(1)(a) GDPR" or "legitimate interest basis" have their characters scattered across multiple vertical positions (some super, some sub, some baseline), making the text visually unreadable even though all characters are painted. This attack is less effective at total suppression but more subtle — the consent text is visible but incomprehensible to a typical user scan.
/* MCP attack — alternating sub/super baseline-shift oscillation: */
/* JS: apply alternating baseline-shift to individual tspans wrapping each word */
function applyOscillatingBaselineShift(svgTextEl) {
const words = svgTextEl.textContent.split(' ');
svgTextEl.textContent = '';
words.forEach((word, i) => {
const ts = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
ts.setAttribute('baseline-shift', i % 2 === 0 ? 'super' : 'sub');
ts.textContent = word + (i < words.length - 1 ? ' ' : '');
svgTextEl.appendChild(ts);
});
/* Consent text renders with alternating words at superscript/subscript positions.
"Article" (super) "6(1)(a)" (sub) "GDPR" (super) "legitimate" (sub)...
The text is technically present and each word is individually painted,
but the vertical position oscillation makes it unreadable at a glance. */
}
/* CSS variant using nth-of-type or generated tspan structure: */
.consent-svg tspan:nth-child(odd) {
baseline-shift: super;
}
.consent-svg tspan:nth-child(even) {
baseline-shift: sub;
}
// Detection
function detectOscillatingBaselineShift() {
document.querySelectorAll('svg text').forEach(textEl => {
const tspans = Array.from(textEl.querySelectorAll('tspan'));
if (tspans.length < 3) return;
let hasSuper = false, hasSub = false;
tspans.forEach(ts => {
const bs = ts.getAttribute('baseline-shift') || window.getComputedStyle(ts).baselineShift;
if (bs === 'super' || (parseFloat(bs) > 0)) hasSuper = true;
if (bs === 'sub' || (parseFloat(bs) < 0)) hasSub = true;
});
if (hasSuper && hasSub && tspans.length > 5) {
console.warn('SECURITY: alternating super/sub baseline-shift oscillation on SVG consent text:', {
textEl, tspanCount: tspans.length,
note: 'Alternating baseline-shift may render consent text unreadable'
});
}
});
}
baseline-shift is not in computed style for SVG presentation attributes: When baseline-shift is applied as an SVG presentation attribute (on the element itself: <tspan baseline-shift="super">), it may or may not appear in getComputedStyle(el).baselineShift depending on the browser. Chrome reflects SVG presentation attributes in computed styles; Firefox's behavior varies. A robust audit must check both el.getAttribute('baseline-shift') (presentation attribute) and window.getComputedStyle(el).baselineShift (CSS property). The dy SVG attribute is never reflected in computed styles and must be detected via attribute inspection only.
Attack summary
| Attack | Properties | What is hidden | Severity |
|---|---|---|---|
| Nested tspan baseline-shift:super accumulation | 5–6 nested <tspan baseline-shift="super"> | All consent text (shifted above viewBox) | High |
| baseline-shift:500% percentage overflow | baseline-shift: 500% | All consent text (shifted 5× line-height up) | High |
| Negative dy accumulation via tspan | Nested <tspan dy="-50"> sum | All consent text (shifted above viewBox y=0) | High |
| Alternating sub/super oscillation | Alternating baseline-shift:super/sub per word | Consent text rendered unreadable | Medium |
<tspan> elements around the consent text, each with baseline-shift: super. Accumulated offsets (approx 33–40% of font-size per level) total 165–240% of font-size upward. Consent text positioned at any reasonable y value within the SVG viewBox is shifted above the viewBox top edge and clipped. The SVG element renders as a blank rectangle. Detection requires checking baseline-shift nesting depth and summing accumulated offsets against viewBox dimensions.
<tspan> elements with negative dy attributes. The sum of all dy values exceeds the text element's y coordinate, placing the effective glyph position at a negative y value — above the viewBox top edge. Unlike baseline-shift, dy is an SVG attribute not reflected in CSS computed styles; detection requires explicit attribute iteration on all tspan children.
baseline-shift: super and baseline-shift: sub to individual word-level tspan elements within the consent text. Words appear at different vertical positions — odd words at superscript height, even words at subscript depth — making the consent text visually incomprehensible despite all characters being technically rendered. Partial suppression attack: more subtle than full clip but still prevents informed consent.
CSS dominant-baseline attacks | Blog: CSS baseline alignment attacks | Security Checklist