MCP server CSS dominant-baseline security: dominant-baseline:alphabetic at y=0 clips text above viewBox, hanging above viewport, ideographic overflow, and glyph-orientation shift attacks on inline SVG consent panels
Published 2026-07-25 — SkillAudit Research
The dominant-baseline property (CSS and SVG presentation attribute) controls which part of a text glyph aligns to the element's y coordinate when rendering inline SVG text elements. Different baseline values position the glyph differently relative to the y-position: alphabetic aligns the bottom of most lowercase Latin letters, hanging aligns the top of ascenders, ideographic aligns the bottom of CJK characters, mathematical aligns the mathematical center. The security relevance is that changing dominant-baseline shifts the entire text block vertically without changing the element's y attribute or computed dimensions.
SVG-based consent banners and disclosure panels — common in frameworks that use SVG for consistent cross-browser rendering of consent UI — are particularly vulnerable because SVG text is clipped to the viewBox boundary. An MCP server that shifts the dominant-baseline can move SVG consent text entirely above or below the viewBox clip region, rendering the SVG element as a blank rectangle while the consent text is painted in the invisible clipped region. getComputedStyle(el).dominantBaseline and getBoundingClientRect() both return normal values — the element occupies its expected screen area, but contains no visible text.
SVG text vs HTML text: These attacks apply specifically to SVG <text> and <tspan> elements, which are clipped to the SVG viewport defined by the viewBox attribute and overflow:hidden (the SVG default). HTML text is not affected by dominant-baseline in the same way — HTML line boxes have their own baseline alignment model. However, CSS dominant-baseline does affect HTML elements that establish a CSS baseline context (flex/grid items), and inline SVG embedded in HTML is the primary attack surface.
Attack 1: dominant-baseline:alphabetic at y=0 — text ascenders clipped above viewBox
In SVG, text at y="0" with dominant-baseline: alphabetic (the default) places the alphabetic baseline at y=0. Glyph ascenders extend upward above y=0 (into negative y space), outside the viewBox. With viewBox="0 0 600 200" and text at y="0", all text glyphs for capital letters and letters with ascenders (b, d, f, h, i, k, l, t) are clipped by the viewBox top edge. The visible area (y=0 to y=200) contains only descenders of letters below the baseline. For most Latin text (consent disclosures rarely use only descender characters), this means all text is clipped above the viewBox. The SVG element renders as a blank rectangle of the correct size.
/* SVG consent banner — normal: */
<svg viewBox="0 0 600 200" width="600" height="200">
<text y="20" font-size="14">By clicking Accept, you consent to...</text>
<text y="40" font-size="14">We share your data with...</text>
</svg>
/* MCP attack — change y to 0 on all text elements: */
svg text, svg tspan {
dominant-baseline: alphabetic; /* default — baseline AT y coordinate */
/* Now: change the y attribute or use dy to shift text to y=0 */
}
/* Or change viewBox to shift the clip region: */
/* MCP modifies the SVG viewBox attribute via JS: */
document.querySelector('svg.consent-panel').setAttribute('viewBox', '0 -200 600 200');
/* viewBox now shows y=-200 to y=0. Text positioned at original y values (20, 40...)
is below y=0, which is the bottom of the new viewBox — outside the visible area.
The SVG element on screen is 600x200px as before, showing an empty box. */
/* CSS attack — dominant-baseline on SVG text with y value manipulation: */
.consent-svg text {
dominant-baseline: hanging; /* aligns TOP of text to y coordinate */
/* Combined with y set close to 0:
text y="0" with hanging baseline → text starts AT y=0
With viewBox starting at 0, only text BELOW y=0 is clipped.
But: set y="-180" → text block shifts 180px above viewBox top.
All glyphs are in y=-180 to y=-166 range (for 14px text).
ViewBox shows 0 to 200. Nothing is visible. */
}
/* Targeted DOM manipulation: */
document.querySelectorAll('.consent-svg text').forEach(el => {
el.setAttribute('y', '-200'); /* move text to -200px (above viewBox) */
el.style.dominantBaseline = 'auto'; /* ensure baseline doesn't compensate */
});
// Detection: check SVG text positions against viewBox
function detectSVGBaselineShift() {
document.querySelectorAll('svg').forEach(svg => {
const vb = svg.viewBox.baseVal; // x, y, width, height
svg.querySelectorAll('text, tspan').forEach(el => {
const rect = el.getBoundingClientRect();
const svgRect = svg.getBoundingClientRect();
// If text bounding box is entirely outside the SVG element bounding box
if (rect.bottom < svgRect.top || rect.top > svgRect.bottom ||
rect.right < svgRect.left || rect.left > svgRect.right) {
console.error('SECURITY: SVG text rendered outside SVG element bounds — clipped:', {
el, text: el.textContent.slice(0, 40), rect, svgRect
});
}
// Also check dominant-baseline directly
const db = el.getAttribute('dominant-baseline') || window.getComputedStyle(el).dominantBaseline;
const y = parseFloat(el.getAttribute('y') || '0');
if (['hanging', 'text-before-edge'].includes(db) && y < 5) {
console.warn('SECURITY: dominant-baseline:hanging at near-zero y — text may be above viewBox:', {
el, dominantBaseline: db, y
});
}
});
});
}
Attack 2: dominant-baseline:hanging shifts entire consent block above SVG viewport
With dominant-baseline: hanging, the text is positioned so that the hanging baseline (top of most glyph ascenders) aligns to the y coordinate. If y is small or near the top of the viewBox, and the MCP server additionally applies a negative dy attribute to shift the text upward, the entire consent text block can be shifted above the viewBox top edge. The dy attribute is additive: each <tspan> with a negative dy shifts text further upward. An MCP that controls the consent SVG DOM can walk all <tspan> elements and apply cumulative negative dy values until the text is above y=0.
/* MCP attack — dominant-baseline:hanging + negative dy accumulation: */
/* Setup: consent SVG has text starting at y="30" */
<svg viewBox="0 0 600 300">
<text y="30" dominant-baseline="hanging">
<tspan x="10" dy="0">We collect your data to:</tspan>
<tspan x="10" dy="1.5em">• Provide the requested service</tspan>
<tspan x="10" dy="1.5em">• Share with advertising partners</tspan>
<tspan x="10" dy="1.5em">• Transfer to third countries</tspan>
</text>
</svg>
/* MCP JS attack: override dy to shift all tspans above viewBox */
const tspans = document.querySelectorAll('.consent-svg tspan');
let accumulatedDy = 0;
tspans.forEach((ts, i) => {
accumulatedDy -= 50; /* shift each tspan 50px further upward */
ts.setAttribute('dy', accumulatedDy + 'px');
/* After 4 tspans: dy = -200px from start.
Text block now at y=30 - 200 = y=-170, well above viewBox top (y=0).
SVG renders as empty box. */
});
/* CSS variant — negative text-anchor compensation: */
.consent-svg text {
dominant-baseline: ideographic; /* aligns bottom of CJK chars to y */
/* For Latin text, ideographic baseline is below the alphabetic baseline
by roughly 0.12em. This alone is a minor shift.
But combined with an MCP-controlled font with extreme descent values: */
font-family: 'MCPHiddenFont'; /* loaded via @font-face with height:0 metrics */
}
/* The custom font's ascender/descender metrics cause the browser to position
text vertically such that all glyphs are above or below the y coordinate
by more than the viewBox height. */
Attack 3: dominant-baseline:ideographic with CJK-descent font metrics shifts Latin text below viewBox
dominant-baseline: ideographic aligns the ideographic baseline (bottom of most CJK characters) to the y coordinate. For Latin characters, the ideographic baseline is below the alphabetic baseline — characters sit lower relative to the y position than with alphabetic. An MCP server combines this with a custom @font-face font that has extreme descent metrics (large negative unitsPerEm ratio for the descent value), causing the browser to place even more of the text below the y coordinate. With text at a y value near the bottom of the viewBox, this shifts text below the viewBox bottom edge, clipping it via the SVG overflow boundary.
/* MCP attack — custom font with extreme descent + ideographic baseline: */
@font-face {
font-family: 'MCPConsentFont';
src: url('/mcp-assets/consent-font.woff2') format('woff2');
/* This font has: unitsPerEm=1000, ascender=100, descender=-900
99% of the font's coordinate space is below the baseline.
All glyphs are positioned in the descent zone. */
descent-override: 900%; /* CSS @font-face descriptor — forces 900% descent */
/* With descent-override:900%, each character's descender extends
9× the font size below the baseline. At font-size:14px, descent = 126px.
Characters extend from baseline down to baseline+126px. */
}
.consent-svg text {
font-family: 'MCPConsentFont';
dominant-baseline: ideographic; /* aligns CJK bottom (= normal alphabetic baseline) to y */
/* The extreme descent means all glyph rendering happens BELOW y.
With viewBox "0 0 600 200" and text at y=180:
Text extends from y=180 to y=180+126 = y=306 — below the viewBox (max 200).
Glyphs are clipped by the viewBox bottom edge. Blank SVG. */
}
// Detection: check font metrics and descent-override
function detectFontDescentAttack() {
// Check @font-face rules for extreme descent-override values
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSFontFaceRule) {
const descentOverride = rule.style.getPropertyValue('descent-override');
if (descentOverride) {
const pct = parseFloat(descentOverride);
if (pct > 200) {
console.error('SECURITY: @font-face extreme descent-override:', descentOverride);
}
}
}
}
} catch (e) {}
}
// Check dominant-baseline values on SVG text in consent panels
document.querySelectorAll('svg text, svg tspan').forEach(el => {
const db = el.getAttribute('dominant-baseline') || window.getComputedStyle(el).dominantBaseline;
if (['ideographic', 'hanging', 'text-before-edge', 'text-after-edge'].includes(db)) {
console.warn('SECURITY: non-standard dominant-baseline on SVG consent text:', { el, dominantBaseline: db });
}
});
}
Attack 4: glyph-orientation-vertical rotates SVG consent text 90° into viewBox overflow
The CSS property glyph-orientation-vertical (deprecated in CSS Writing Modes Level 4 but still processed by some browsers from SVG presentation attributes) rotated individual glyphs within a text run. The equivalent modern property is text-orientation. An MCP server that sets SVG text to a vertical writing mode via writing-mode: vertical-lr and then adjusts the x coordinate can shift rotated text outside the horizontal extent of the SVG viewBox. At 90° rotation, a text string 200 characters wide becomes 200-character-heights tall — extending far below the viewBox bottom. The rotated text block is clipped by the viewBox, rendering as an empty SVG.
/* MCP attack — vertical writing mode rotates text out of viewBox: */
.consent-svg text {
writing-mode: vertical-rl; /* rotates text 90° — width becomes height */
dominant-baseline: auto;
/* consent-text-block that was 400px wide (horizontal) is now
400px TALL (vertical) — extends 400px below the start y position.
With text at y="10" and viewBox height 200:
text extends from y=10 to y=410 — clipped at y=200.
Only the first ~190px of text height is visible, which corresponds
to the first few characters of the consent text rotated sideways.
The rest of the consent body is below the viewBox bottom. */
}
/* Combined with x positioning outside viewBox: */
.consent-svg text {
writing-mode: vertical-rl;
/* MCP sets x attribute to 9999 via JS: */
}
document.querySelectorAll('.consent-svg text').forEach(el => {
el.setAttribute('x', '9999'); /* rotated text block starts at x=9999 */
/* SVG viewBox width is 600. x=9999 is 9399px off the right edge.
Clipped by viewBox overflow. No text visible. */
});
// Detection
function detectVerticalWritingModeOnSVGText() {
document.querySelectorAll('svg text, svg tspan').forEach(el => {
const cs = window.getComputedStyle(el);
const wm = cs.writingMode || el.getAttribute('writing-mode');
if (wm && wm !== 'horizontal-tb') {
const x = parseFloat(el.getAttribute('x') || '0');
const svgEl = el.closest('svg');
if (svgEl) {
const vb = svgEl.viewBox.baseVal;
if (x > vb.x + vb.width || x < vb.x) {
console.error('SECURITY: vertical-mode SVG text with x outside viewBox:', {
el, writingMode: wm, x, viewBox: `${vb.x} ${vb.y} ${vb.width} ${vb.height}`
});
}
}
}
});
}
SVG consent panels and IntersectionObserver: A consent framework that uses IntersectionObserver to verify that the consent panel is visible before enabling the accept button will report a positive intersection for an SVG element whose text is clipped — the SVG element itself intersects the viewport; the observer cannot distinguish between an SVG with visible text and an SVG with all text clipped outside the viewBox. Detection requires checking whether SVG text elements' getBoundingClientRect() is within the SVG container's bounds, not just whether the SVG container intersects the viewport.
Attack summary
| Attack | Properties | What is hidden | Severity |
|---|---|---|---|
| Alphabetic baseline at y=0 clips ascenders | dominant-baseline:alphabetic + y=0 | All Latin text (ascenders clipped) | High |
| Hanging baseline + negative dy accumulation | dominant-baseline:hanging + negative dy | Entire consent block shifted above viewBox | High |
| Ideographic + extreme @font-face descent | dominant-baseline:ideographic + descent-override:900% | Glyphs shifted below viewBox bottom | High |
| Vertical writing mode + off-screen x | writing-mode:vertical-rl + x=9999 | Rotated text block off-screen | Medium |
dominant-baseline: hanging (or text-before-edge) to SVG text elements at small y values, shifting the text block so all glyphs are at negative y coordinates — above the SVG viewBox top edge. The SVG element renders as a blank rectangle at the correct size. getComputedStyle() shows normal values. Detection requires comparing the getBoundingClientRect() of SVG text elements against the parent SVG element's bounding box.
@font-face with descent-override: 900%, then applies it to SVG consent text elements with dominant-baseline: ideographic. The extreme descent metric causes all glyphs to extend far below the baseline, placing them below the SVG viewBox bottom boundary. The SVG element and its area are fully detectable; the rendered glyph positions are not inspectable via standard DOM APIs.
writing-mode: vertical-rl on SVG text elements and moves the x coordinate outside the viewBox width. Rotated text blocks are placed to the right of or left of the SVG viewBox clip region. Consent text is present in the DOM but not visible in the SVG viewport.
Blog: CSS baseline alignment attacks | CSS baseline-shift attacks | Security Checklist