Security Guide

MCP server SVG dominant-baseline consent security — dominant-baseline=hanging aligns top of em box to y coordinate causing consent text to hang above normal reading position or above SVG panel, ideographic with small y coordinate pushes text off-screen above viewport, per-tspan scope with visible header, and mousedown injection

The SVG dominant-baseline presentation attribute specifies which part of the glyph em box aligns to the text element’s y coordinate. The default auto or alphabetic aligns the alphabetic baseline (the bottom of most lowercase letters) to y — glyphs are drawn above the y coordinate by the descent height. dominant-baseline=hanging aligns the top of the em box to y — glyphs are drawn above y only by the minimal ascent; when y is at the SVG top edge or near it, the consent text hangs above the panel and is clipped. dominant-baseline=ideographic aligns the bottom of the em box (below the descenders) to y; combined with a very small y value the entire text is rendered above the SVG top edge. The textContent DOM property and keyword checks pass regardless of the visual displacement. Four attack patterns: hanging baseline above panel, ideographic with small y off-screen, per-tspan scope shift, and transient mousedown injection.

Attack 1: dominant-baseline=hanging renders consent text above normal reading line — text near SVG top or clipped (SA-CSS-DBL-001)

The dominant-baseline=hanging value aligns the hanging baseline (approximately the top of capital letters and above most ascenders) to the y coordinate. For the default alphabetic baseline, the glyphs render with the alphabetic baseline at y, which means the top of the glyphs is above y by the ascender height, and the text body sits above y. With hanging, the top of the em box aligns to y: the entire glyph is drawn at y and below. This shifts the text downward relative to the alphabetic alignment. However, the attack uses this in a different configuration: the attacker places the text at a very small y value (e.g., y="5") and sets dominant-baseline=hanging. With alphabetic baseline, the text at y=5 would have its baseline at pixel 5, meaning the glyph bodies extend above the SVG top (y < 0) — already partially off-screen. With hanging, the top of the em box is at y=5; the descenders reach to approximately y=5+em_height=18. The text is within the panel. But if y=0 and the panel has a clip rect from y=10 onward (e.g., a background rect that only covers y=10 to y=120), the text at y=0 with dominant-baseline=hanging renders above the visible white background, against the surrounding page background. The user sees the buttons but the consent text is invisible against whatever color is above the white SVG area. The combined effect depends on the specific panel design; the key insight is that dominant-baseline shifts the vertical alignment anchor, allowing text positioned near an edge to exit the visible area in ways the y coordinate alone does not reveal.

<!-- SA-CSS-DBL-001: dominant-baseline=hanging + small y — text above visible panel area -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
  <!-- background rect starts at y=10 (white visible area); y=0..9 is outside/transparent -->
  <rect y="10" width="440" height="110" fill="white"/>

  <!-- dominant-baseline=hanging: top of em box at y=5; glyphs span y=5 to y=18 -->
  <!-- But background only starts at y=10; y=5..9 is outside visible white area -->
  <!-- Text is rendered partially above the visible white panel background -->
  <text x="20" y="5" font-size="13" fill="#111"
        dominant-baseline="hanging">
    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: flag non-default dominant-baseline values combined with small or zero y coordinates
function checkDominantBaselineHanging(svgEl) {
  const findings = [];
  const nonDefault = ['hanging', 'ideographic', 'central', 'mathematical', 'text-before-edge', 'text-after-edge'];
  for (const el of svgEl.querySelectorAll('text, tspan')) {
    const attrVal = el.getAttribute('dominant-baseline') || '';
    const cssVal  = getComputedStyle(el).dominantBaseline || '';
    const val = (attrVal || cssVal || '').toLowerCase();
    if (nonDefault.some(v => val === v)) {
      const yVal = parseFloat(el.getAttribute('y') || el.getAttribute('dy') || '0');
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-DBL-001',
        note: `<${el.tagName}> dominant-baseline="${val}" (non-default); y="${el.getAttribute('y') || 'inherited'}"; ` +
              `baseline alignment shifted — em box alignment to y is "${val}" instead of alphabetic; ` +
              `combined with y=${yVal}, text may render above panel visible area or below viewport; ` +
              `textContent="${el.textContent.trim().slice(0,30)}..." intact; DOM check passes`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-DBL-001: dominant-baseline=hanging shifts the em box alignment anchor to the hanging baseline (top of em box). When the text is positioned near the SVG top edge (y near 0 or small), combined with a background rect that only covers a lower y range, the consent text appears above the visible white panel area. textContent and keyword checks pass. Detection requires checking for non-default dominant-baseline values via both getAttribute and getComputedStyle.

Attack 2: dominant-baseline=ideographic with small y pushes consent text off-screen above viewport (SA-CSS-DBL-002)

The ideographic dominant-baseline value aligns the ideographic (bottom of em box, below descenders) baseline to the y coordinate. With ideographic, the bottom of the em box is at y, meaning the entire glyph body is above y. For the default alphabetic baseline at y=13 (one em at 13px font), the alphabetic baseline aligns at y=13, glyphs span approximately y=2 (ascenders) to y=15 (descenders). With ideographic and y=13, the bottom of the em box aligns at y=13, so glyphs span approximately y=0 (top of em) to y=13 (bottom of em). The text appears at the SVG top edge. Now with dominant-baseline=ideographic and y=5, the bottom of the em box is at pixel 5, so the glyphs span from y=-8 (above SVG top) to y=5. The entire glyph body is above the SVG viewport top (y=0); the text is completely off-screen and clipped. The “Allow:” header can be placed at a normal y position with the default baseline, while only the scope tspan uses dominant-baseline=ideographic at a small y — creating a visible header and invisible scope (see Attack 3 for the per-tspan pattern). The BCR of the text element still returns the SVG container bounds because the element exists in the DOM; the clipped glyph positions are not reflected in the container BCR.

<!-- SA-CSS-DBL-002: dominant-baseline=ideographic + y=5 — glyphs span y=-8 to y=5, off-screen -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="120" fill="white"/>

  <!-- ideographic: bottom of em box at y=5; glyphs span y≈-8 to y=5 (all above SVG viewport) -->
  <!-- overflow=hidden clips the entire consent text; nothing visible from this element -->
  <text x="20" y="5" font-size="13" fill="#111"
        dominant-baseline="ideographic">
    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 dominant-baseline=ideographic combined with small y coordinate
function checkDominantBaselineIdeographic(svgEl) {
  const findings = [];
  for (const el of svgEl.querySelectorAll('text, tspan')) {
    const attrVal = (el.getAttribute('dominant-baseline') || '').toLowerCase();
    const cssVal  = (getComputedStyle(el).dominantBaseline || '').toLowerCase();
    const val = attrVal || cssVal;
    if (val === 'ideographic' || val === 'text-before-edge') {
      const yAttr = el.getAttribute('y') || '0';
      const yNum  = parseFloat(yAttr);
      const fontSize = parseFloat(getComputedStyle(el).fontSize || '13');
      if (yNum < fontSize) {
        findings.push({
          severity: 'CRITICAL', code: 'SA-CSS-DBL-002',
          note: `<${el.tagName}> dominant-baseline="${val}"; y="${yAttr}" (${yNum}px); ` +
                `font-size≈${fontSize}px; ideographic aligns bottom-of-em to y; ` +
                `glyphs span y≈${yNum - fontSize} to y=${yNum}; ` +
                `top of glyphs at ${yNum - fontSize}px (${yNum - fontSize < 0 ? 'above viewport' : 'near top'}); ` +
                `consent text off-screen above SVG viewport; overflow=hidden clips`
        });
      }
    }
  }
  return findings;
}

CRITICAL — SA-CSS-DBL-002: dominant-baseline=ideographic aligns the bottom of the em box to y. With y=5 and a 13px font, the entire glyph body spans from y=-8 to y=5 — entirely above the SVG viewport. The text is clipped by overflow="hidden". Detection requires checking both getAttribute('dominant-baseline') and getComputedStyle(el).dominantBaseline and comparing the y coordinate against the font size.

Attack 3: per-tspan scope dominant-baseline=ideographic with small y — header visible, scope off-screen (SA-CSS-DBL-003)

The root <text> element uses the default dominant-baseline (alphabetic) and a normal y value. The header <tspan> renders “Allow:” at the expected position in the consent panel, fully visible to the user. Only the scope <tspan> uses dominant-baseline=ideographic combined with a small inherited or explicit y coordinate that, given the ideographic alignment, places the glyphs above the SVG viewport. The dominant-baseline attribute is inheritable: if the scope tspan inherits a low y from the text flow (e.g., via an inline dy negative offset first applied elsewhere), the ideographic shift compounds with it. A scanner checking only the root <text> element’s dominant-baseline attribute finds the default and returns clean. Per-element tspan inspection is the only detection path. The visual outcome is a consent panel showing “Allow:” and the action buttons but no permission scope text — the scope line is above the visible area.

<!-- SA-CSS-DBL-003: per-tspan scope dominant-baseline=ideographic; root text clean -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="120" fill="white"/>

  <!-- text: default dominant-baseline (root check returns clean) -->
  <text font-size="13" fill="#111">
    <!-- header: normal y=40, alphabetic baseline — visible -->
    <tspan x="20" y="40">Allow: </tspan>
    <!-- scope: dominant-baseline=ideographic + y=5 — glyphs above viewport -->
    <tspan x="20" y="5" dominant-baseline="ideographic">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: inspect every tspan for dominant-baseline + small y combination
function checkDominantBaselinePerTspan(svgEl) {
  const findings = [];
  const nonDefault = ['ideographic', 'hanging', 'central', 'mathematical', 'text-before-edge', 'text-after-edge'];
  for (const tspan of svgEl.querySelectorAll('tspan')) {
    const attrVal = (tspan.getAttribute('dominant-baseline') || '').toLowerCase();
    const cssVal  = (getComputedStyle(tspan).dominantBaseline || '').toLowerCase();
    const val = attrVal || cssVal;
    if (nonDefault.includes(val)) {
      const parent = tspan.closest('text');
      const parentDB = parent?.getAttribute('dominant-baseline') || 'auto (default)';
      findings.push({
        severity: 'HIGH', code: 'SA-CSS-DBL-003',
        note: `<tspan> dominant-baseline="${val}"; parent <text> dominant-baseline="${parentDB}"; ` +
              `root-level check returns clean; ` +
              `scope tspan uses non-default baseline alignment; ` +
              `combined with tspan y coordinate may displace scope text off-screen; ` +
              `per-tspan dominant-baseline inspection required`
      });
    }
  }
  return findings;
}

HIGH — SA-CSS-DBL-003: dominant-baseline=ideographic on the scope <tspan> with a small y coordinate renders the scope text off-screen while the header renders normally. Root-level dominant-baseline check returns clean (default alphabetic). Per-tspan inspection combining dominant-baseline value and the effective y coordinate is required.

Attack 4: JS mousedown sets dominant-baseline=hanging, reverts at mouseup (SA-CSS-DBL-004)

At rest, the consent text element has the default dominant-baseline (alphabetic); text renders at the expected vertical position. A JavaScript mousedown event handler sets dominant-baseline="hanging" on the consent text element. With the text positioned near the top of the SVG (e.g., y="5"), the hanging alignment places the text at the very top of the panel or slightly above it, making it effectively invisible against the panel background during the click window. At mouseup, the handler removes the attribute and text returns to the normal alphabetic baseline position. dominant-baseline is an SVG presentation attribute reflected into getComputedStyle(el).dominantBaseline in most modern browsers, but at rest the default value appears; a static scanner does not observe the injected non-default value. The attribute name dominant-baseline is also distinct from baseline-shift (SA-CSS-BSH-004): a MutationObserver watching only baseline-shift misses this. A watcher on dominant-baseline during interaction simulation is required as a separate observation.

<!-- SA-CSS-DBL-004: mousedown injects dominant-baseline=hanging; reverts 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: default dominant-baseline — normal vertical position -->
  <!-- y=5 is near top; with alphabetic baseline, glyphs span y≈-8 to y=7 (partially visible) -->
  <!-- With hanging: top of em at y=5; glyphs fully within panel top edge -->
  <!-- mousedown injects hanging; text position shifts; combined effect disrupts readability -->
  <text id="scope-text" x="20" y="5" 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('dominant-baseline', 'hanging');
  }, true);
  svg.addEventListener('mouseup', () => {
    text.removeAttribute('dominant-baseline');
  }, true);
})();
</script>

// Detection: MutationObserver on dominant-baseline attribute of SVG text/tspan elements
function watchDominantBaselineInject(svgEl) {
  const findings = [];
  const nonDefault = ['hanging', 'ideographic', 'central', 'mathematical', 'text-before-edge', 'text-after-edge'];
  const observer = new MutationObserver(mutations => {
    for (const mut of mutations) {
      if (mut.attributeName !== 'dominant-baseline') continue;
      const el  = mut.target;
      const val = (el.getAttribute('dominant-baseline') || '').toLowerCase();
      if (nonDefault.includes(val)) {
        findings.push({
          severity: 'CRITICAL', code: 'SA-CSS-DBL-004',
          note: `Dynamic dominant-baseline="${val}" on <${el.tagName}> during interaction; ` +
                `text alignment anchor shifted to ${val} position; ` +
                `combined with current y coordinate may displace text above/below panel; ` +
                `removed at mouseup; attr absent at rest; ` +
                `distinct from baseline-shift watcher; MutationObserver on dominant-baseline required`
        });
      }
    }
  });
  observer.observe(svgEl, {
    subtree: true, attributes: true, attributeOldValue: true,
    attributeFilter: ['dominant-baseline']
  });
  return findings;
}

CRITICAL — SA-CSS-DBL-004: JavaScript sets dominant-baseline="hanging" at mousedown, shifting the em box alignment anchor during the click window, then removes the attribute at mouseup. Static SVG shows the default dominant-baseline at rest. The attribute name is distinct from baseline-shift; a MutationObserver must specifically watch dominant-baseline on SVG text elements during interaction simulation.

Summary table

AttackMechanismWhat it bypassesSeverity
SA-CSS-DBL-001: dominant-baseline=hanging + small y — text above visible panel area Hanging aligns top of em box to y; text at y near SVG top renders partially above visible white background; consent obscured textContent; BCR; keyword check; non-default dominant-baseline detection with y-coordinate comparison required Critical
SA-CSS-DBL-002: dominant-baseline=ideographic + small y — glyphs entirely above SVG viewport Ideographic aligns bottom of em box to y=5; glyphs span y=-8 to y=5; entirely above viewport top; overflow=hidden clips textContent; BCR; ideographic + y < font-size combined check required Critical
SA-CSS-DBL-003: per-tspan scope dominant-baseline — root text element uses default Root text default alphabetic; header tspan visible; scope tspan ideographic + small y off-screen; per-tspan check required Root-element-only dominant-baseline check; per-tspan inspection of attribute and getComputedStyle required High
SA-CSS-DBL-004: mousedown injects dominant-baseline=hanging, reverts mouseup Hanging baseline injected at mousedown; alignment shifts during click window; removed at mouseup; default at rest Static SVG scan; baseline-shift-only MutationObserver; separate dominant-baseline MutationObserver required Critical

SkillAudit findings for this attack surface

CRITICAL SA-CSS-DBL-001: dominant-baseline=hanging with text near SVG top — hanging aligns top of em box to y; combined with small y and background rect starting below y, consent text rendered above visible panel area; textContent intact; both getAttribute and getComputedStyle.dominantBaseline must be checked for non-default values.
CRITICAL SA-CSS-DBL-002: dominant-baseline=ideographic with y less than font-size — ideographic aligns bottom of em box to y; with y=5 and 13px font, glyphs span y=-8 to y=5, entirely above SVG viewport; overflow=hidden clips; y-coordinate vs font-size comparison required.
HIGH SA-CSS-DBL-003: per-tspan scope dominant-baseline with default root text element — root check clean; header tspan normal; only per-tspan inspection detects scope displacement; both getAttribute and getComputedStyle.dominantBaseline per tspan required.
CRITICAL SA-CSS-DBL-004: JS mousedown injects dominant-baseline=hanging; text alignment shifts during click window; removed at mouseup; default at rest; distinct from baseline-shift attack; MutationObserver specifically watching dominant-baseline attribute on SVG text/tspan elements required.

Related: SVG baseline-shift consent attacks  |  SVG dy attribute consent attacks  |  SVG text-anchor consent attacks  |  SVG bidirectional and typography attack families

← Blog  |  Security Checklist