Security Guide

MCP server CSS dominant-baseline consent security — text-top displacement, mathematical baseline shift, central additive nesting, and ideographic descent clip

The CSS dominant-baseline property specifies the alignment point used to position an SVG <text> element’s glyph reference frame relative to its declared y coordinate. Unlike the natural alphabetic baseline — which sits at the bottom of most capital letters — alternate dominant baseline values shift the reference point to different vertical positions within the em-box: the top of the ascenders (text-top), the mathematical center used for formula alignment (mathematical), the geometric center of the em-box (central), or the bottom of the ideographic em-square (ideographic). Each shift repositions the entire glyph block relative to the element’s declared y coordinate, causing text to render at a different vertical position than its numeric y attribute suggests. In MCP server consent panels, where SVG containers with overflow: hidden clip content outside their declared bounds, these baseline shifts can push consent text partially or entirely outside the visible region — making permission scope descriptions invisible while the text node itself reports a non-zero bounding rectangle to JavaScript visibility checks.

Attack 1: dominant-baseline: text-top hangs consent text below the em-box top, clipping it against a fixed-height SVG container (SA-CSS-DB-001)

The text-top dominant baseline positions the baseline reference point at the top of the em-box — the ascender line at the top of the capital letter height. Normally, when a <text y="10"> element uses the default alphabetic baseline, the bottom of capital letters sits at y=10 and the glyph body extends upward from that point: the top of the cap-height reaches approximately y=10 − 0.7×fontSize. With dominant-baseline: text-top, the semantics invert: the top of the glyph em-box sits at y=10, and the entire glyph body hangs downward from that point. For a 32px font, the em-box top is at y=10, the baseline sits at approximately y=10+22=32 (roughly 0.7×fontSize below the em-box top for typical fonts), and the descenders extend to approximately y=10+32=42.

An MCP consent panel implemented as an SVG with height="30" and overflow: hidden shows only the region from y=0 to y=30. A <text> element with y="0" and dominant-baseline: text-top at font-size: 32px places the top of the em-box exactly at y=0. The glyph body runs from y=0 to y=32. With the container clipping at y=30, only the topmost 30 pixels of the 32-pixel em-box are visible — but those top pixels are the region above the cap-height, which in typical antialiased font rendering is the very edge of the glyph’s antialiased top border: 1–2 pixels of sub-pixel gradient. The consent text appears effectively invisible: no readable letterforms, just a faint 2px antialiased smear at the container top edge. A JavaScript visibility check using getBoundingClientRect() on the SVG text node returns a bounding rect with non-zero width and height values — because the element’s full em-box is non-zero in size — and incorrectly reports the text as visible.

<!-- SA-CSS-DB-001: dominant-baseline:text-top on 32px font at y=0
     Container height=30, overflow:hidden
     Text em-box: top at y=0, bottom at y=32
     Visible region: y=0 to y=30 — only top 30px of 32px em-box shown
     Visible content: ~2px of antialiased glyph top edge — illegible
     getBoundingClientRect() on <text> returns non-zero rect — evades emptiness check -->

<!-- Consent SVG container: fixed height, overflow clipping active -->
<svg
  id="consent-svg"
  width="400"
  height="30"
  style="overflow: hidden; border: 1px solid #ccc; display: block;"
>
  <!-- Background rect for the consent panel -->
  <rect x="0" y="0" width="400" height="30" fill="white" />

  <!-- Consent permission text:
       y="0" with text-top baseline → top of em-box at y=0
       font-size 32px → em-box bottom at y=32
       Clip boundary at y=30 → only top 30/32 of em-box visible
       The top 30px contains the region above the cap-height (nearly empty) -->
  <text
    x="10"
    y="0"
    font-size="32"
    font-family="sans-serif"
    fill="black"
    style="dominant-baseline: text-top;"
  >Allows full filesystem read and write access</text>
</svg>

<!-- Clip boundary visualization (not rendered to user, for audit reference):
     The dashed line at y=30 marks where the SVG clips:
       y= 0: top of em-box (text-top baseline reference point)
       y=22: approximate cap-height baseline (bottom of capital letters)
       y=30: SVG clip edge — ONLY 2px of descender region below cap-height visible
       y=32: bottom of em-box

     What the user sees in the 30px container:
       A 2px smear at the top of the container — no readable letterforms
       The cap-height body of "Allows full filesystem..." is entirely below the clip
-->

// Detection: check dominant-baseline computed value and compare
// rendered y-extent against container clip boundary
function detectTextTopClip(svgEl) {
  const containerRect = svgEl.getBoundingClientRect();
  const containerBottom = containerRect.top + containerRect.height;

  svgEl.querySelectorAll('text, tspan').forEach(textEl => {
    const cs = getComputedStyle(textEl);
    const db = cs.dominantBaseline;

    // Flag non-alphabetic, non-auto dominant-baseline values on consent text
    if (db !== 'auto' && db !== 'alphabetic') {
      console.warn(
        'Non-standard dominant-baseline on consent text:',
        db,
        '— text element:', textEl
      );
    }

    // Specifically flag text-top: shifts entire text block downward from declared y
    if (db === 'text-top') {
      const fontSize = parseFloat(cs.fontSize);
      const yAttr = parseFloat(textEl.getAttribute('y') || '0');

      // With text-top: em-box top at yAttr, bottom at yAttr + fontSize
      const emBoxBottom = yAttr + fontSize;
      const svgHeight = parseFloat(svgEl.getAttribute('height'));

      // Check if the readable glyph body (cap-height region) is clipped
      // Cap-height ≈ 0.7 × fontSize above the alphabetic baseline
      // With text-top, alphabetic baseline ≈ yAttr + 0.7 × fontSize
      const alphabeticBaseline = yAttr + fontSize * 0.7;
      if (alphabeticBaseline > svgHeight) {
        console.error(
          'CRITICAL SA-CSS-DB-001: dominant-baseline:text-top places alphabetic baseline at y='
          + alphabeticBaseline.toFixed(1)
          + ' which is below SVG height ' + svgHeight
          + ' — consent text body entirely clipped; getBoundingClientRect() still non-zero'
        );
      }
    }
  });
}

// Also check: getBoundingClientRect alone is insufficient
// The text node bounding box may extend below the visible area
// Use SVG getComputedTextLength() or check rendered character bounding boxes
function legibilityCheck(textEl, svgEl) {
  const svgHeight = parseFloat(svgEl.getAttribute('height') ||
    svgEl.getBoundingClientRect().height);
  const textRect = textEl.getBoundingClientRect();
  const svgRect = svgEl.getBoundingClientRect();

  // Compute overlap between text bounding box and SVG visible area
  const textTop    = textRect.top - svgRect.top;
  const textBottom = textRect.bottom - svgRect.top;
  const visibleTop    = Math.max(textTop, 0);
  const visibleBottom = Math.min(textBottom, svgHeight);
  const visibleHeight = Math.max(0, visibleBottom - visibleTop);
  const textHeight    = textRect.height;

  const visibleFraction = textHeight > 0 ? visibleHeight / textHeight : 0;
  if (visibleFraction < 0.5) {
    console.error(
      'Less than 50% of consent text height is within the visible SVG area.',
      'Visible fraction:', (visibleFraction * 100).toFixed(1) + '%',
      '— possible dominant-baseline clip attack'
    );
  }
}

CRITICAL — SA-CSS-DB-001: dominant-baseline: text-top shifts the text reference point to the em-box top, causing a 32px-font text element at y="0" to render its readable glyph body entirely below a 30px-tall clipped SVG container. Only ~2px of the antialiased glyph top edge appears in the visible region — illegible but non-empty. getBoundingClientRect() returns a non-zero bounding rect, defeating JavaScript emptiness checks. Detection requires reading the dominantBaseline computed value, computing the effective glyph render region relative to the container clip boundary, and checking the fraction of readable text height that falls within the visible area. SkillAudit flags dominant-baseline values other than auto or alphabetic on any SVG text element inside a consent-flagged area.

Attack 2: dominant-baseline: mathematical with alignment-baseline: mathematical on child <tspan> elements creates additive upward shifts clipping the first consent line (SA-CSS-DB-002)

The mathematical dominant baseline is defined by the SVG specification for use with mathematical formulae: it positions the baseline reference point at the center of operators like =, +, and − in TeX-style typesetting. In terms of physical position, this is approximately 0.5 × ex-height above the natural alphabetic baseline — for typical Latin fonts, the ex-height is roughly 50% of the em size, placing the mathematical baseline at approximately 0.25em above the alphabetic baseline. For a 14px font, this is a shift of approximately 0.25 × 14 = 3.5px upward relative to the declared y coordinate.

In isolation, 3.5px seems minor. The attack becomes effective through the alignment-baseline property on child <tspan> elements. The alignment-baseline: mathematical value positions each <tspan>’s baseline relative to the parent text element’s dominant baseline using the mathematical sub-alignment point. Because the dominant baseline is itself already shifted by ~0.25em upward, and the alignment-baseline on each tspan shifts the tspan’s baseline a further ~0.25em upward relative to the parent’s (already shifted) dominant baseline point, the total shift for a tspan is approximately 0.25em + 0.25em = 0.5em. For a consent panel with multiple tspan lines representing different permission items, each line is shifted up by 0.5em. The top line — typically the most critical “This plugin requests access to:” header — moves above the container’s top clipping boundary and disappears. Consent panels with small top padding (less than 5px) are especially vulnerable, as even a partial line clip removes the grammatical subject of the permission request.

<!-- SA-CSS-DB-002: dominant-baseline:mathematical on parent <text> +
     alignment-baseline:mathematical on child <tspan> elements
     Additive upward shift ≈ 0.5em total per tspan
     At font-size:14px: shift ≈ 7px upward per tspan
     Consent SVG with top padding 4px → first line clips above container top -->

<!-- Consent SVG: height 120px, 4px effective top padding for text region -->
<svg
  id="consent-text-svg"
  width="420"
  height="120"
  style="overflow: hidden; background: white;"
>
  <!-- Permission list text block -->
  <!-- Natural render position without attack:
       Line 1 "This plugin requests:" at y=20  (visible — within 120px height)
       Line 2 "Read: ~/Documents"      at y=38  (visible)
       Line 3 "Write: ~/.ssh/"         at y=56  (visible)
       Line 4 "Execute: /bin/sh"       at y=74  (visible)
       Line 5 "Network: 0.0.0.0/0"     at y=92  (visible)

       With dominant-baseline:mathematical + alignment-baseline:mathematical:
       Shift per line ≈ 0.5 × 14px = 7px upward
       Line 1 effective y ≈ 20 − 7 = 13  (still visible — 4px padding allows it)
       ...but with smaller padding or larger font, line 1 clips above y=0 -->

  <text
    x="16"
    y="20"
    font-size="14"
    font-family="sans-serif"
    fill="#111"
    style="dominant-baseline: mathematical;"
  >
    <!-- alignment-baseline:mathematical on each tspan shifts relative to
         the parent's mathematical dominant baseline point —
         effective additional shift ≈ 0.25em more upward -->
    <tspan x="16" dy="0"
      style="alignment-baseline: mathematical; font-weight: bold;"
    >This plugin requests access to:</tspan>

    <tspan x="16" dy="18"
      style="alignment-baseline: mathematical;"
    >• Read all files in ~/Documents and subfolders</tspan>

    <tspan x="16" dy="18"
      style="alignment-baseline: mathematical;"
    >• Write and overwrite files in ~/.ssh/</tspan>

    <tspan x="16" dy="18"
      style="alignment-baseline: mathematical;"
    >• Execute arbitrary shell commands via /bin/sh</tspan>

    <tspan x="16" dy="18"
      style="alignment-baseline: mathematical;"
    >• Outbound network to all addresses (0.0.0.0/0)</tspan>
  </text>
</svg>

<!-- Attack variant with tight padding: y="8" with 14px font
     mathematical shift ≈ 7px upward → effective render at y≈1
     top padding 4px → first line header clips above y=0
     User reads: "• Read all files..." without the "This plugin requests:" preamble
     Permission list items appear to be standalone bullets, not an access request -->

<svg width="420" height="100" style="overflow: hidden; background: white;">
  <text x="16" y="8" font-size="14" font-family="sans-serif" fill="#111"
    style="dominant-baseline: mathematical;">
    <tspan x="16" dy="0" style="alignment-baseline: mathematical; font-weight: bold;"
    >This plugin requests access to:</tspan>
    <tspan x="16" dy="18" style="alignment-baseline: mathematical;"
    >• Read ~/Documents (recursive)</tspan>
    <tspan x="16" dy="18" style="alignment-baseline: mathematical;"
    >• Write ~/.ssh/ directory</tspan>
    <tspan x="16" dy="18" style="alignment-baseline: mathematical;"
    >• Shell execution</tspan>
  </text>
</svg>

// Detection: flag mathematical dominant-baseline + alignment-baseline combination
// on multi-tspan consent text blocks
function detectMathematicalBaselineAttack(svgEl) {
  svgEl.querySelectorAll('text').forEach(textEl => {
    const textCs = getComputedStyle(textEl);
    const db = textCs.dominantBaseline;

    if (db === 'mathematical') {
      const fontSize = parseFloat(textCs.fontSize);
      // Mathematical baseline shift ≈ 0.25em upward
      const parentShift = fontSize * 0.25;

      let hasChildMathAlignment = false;
      textEl.querySelectorAll('tspan').forEach(tspan => {
        const tspanCs = getComputedStyle(tspan);
        if (tspanCs.alignmentBaseline === 'mathematical') {
          hasChildMathAlignment = true;
          // Additional shift per tspan ≈ 0.25em
        }
      });

      if (hasChildMathAlignment) {
        // Total effective upward shift ≈ 0.5em per tspan
        const totalShift = fontSize * 0.5;
        const yAttr = parseFloat(textEl.getAttribute('y') || '0');
        const effectiveFirstLineY = yAttr - totalShift;

        if (effectiveFirstLineY < 5) {
          console.error(
            'HIGH SA-CSS-DB-002: dominant-baseline:mathematical + alignment-baseline:mathematical',
            'Total upward shift:', totalShift.toFixed(1) + 'px',
            'First line effective y:', effectiveFirstLineY.toFixed(1),
            '— first consent line may be clipped above container top edge'
          );
        } else {
          console.warn(
            'SA-CSS-DB-002: mathematical baseline combination present.',
            'Upward shift:', totalShift.toFixed(1) + 'px',
            'First line y:', effectiveFirstLineY.toFixed(1)
          );
        }
      }
    }
  });
}

HIGH — SA-CSS-DB-002: dominant-baseline: mathematical on a parent <text> element combined with alignment-baseline: mathematical on child <tspan> elements creates an additive upward shift of approximately 0.5em per line relative to the declared y position. On a 14px-font consent panel with a top text position of y="8", the first line — typically the “This plugin requests access to:” preamble — shifts to an effective render position of approximately y=1, which clips above the container edge when top padding is 4px or less. Users read the bullet items without the preamble that establishes these as an access request, critically obscuring the consent framing. SkillAudit checks for dominant-baseline: mathematical and alignment-baseline: mathematical combinations on multi-tspan consent text structures and computes the additive shift against the container clip boundary.

Attack 3: dominant-baseline: central on nested <g> groups compounds vertical displacement — three-level nesting pushes text 12px upward at font-size: 16px (SA-CSS-DB-003)

SVG allows <g> group elements to carry presentation attributes including dominant-baseline. When a <text> element is nested inside multiple <g> elements, each with dominant-baseline: central, the baseline shifts compound. The central value positions the baseline at the geometric center of the em-box — approximately 0.5 × font-size above the natural alphabetic baseline. When a <g> element has dominant-baseline: central, child elements that do not explicitly override the property inherit the central value and apply it relative to the em-box center established by the parent’s dominant baseline.

The compound effect arises because each nesting level that specifies dominant-baseline: central re-applies the center-shift relative to the coordinate system established by the parent’s central baseline. In a three-level nesting structure — <g dominant-baseline="central"><g dominant-baseline="central"><text dominant-baseline="central"> — the total upward displacement is approximately 3 × 0.5 × fontSize / 2, or 0.75 × fontSize above the natural alphabetic baseline position. For font-size: 16px, this is 0.75 × 16 = 12px upward. A consent text element declared at y="20" renders with an effective top-of-glyph position at approximately y = 20 − 12 = 8. If the SVG container starts at y=0 with no top padding, the first 8 pixels of glyph body — including the cap-height tops of the first line — are present, but the baseline and lower half of the glyphs are shifted up and the effective text block extends above the declared starting position in a way that changes with each additional nesting level.

The attack is particularly effective in MCP server consent panels that use layered <g> elements for animation, theming, or component encapsulation purposes — legitimate structural reasons that provide cover for the multiple nesting levels required. A three-level group hierarchy is common in SVG component frameworks. Each level adds another central dominant-baseline specification, and the cumulative upward shift is not apparent from examining any individual element’s style in isolation.

<!-- SA-CSS-DB-003: dominant-baseline:central on three nested <g> elements
     Compound upward shift ≈ 3 × (0.5 × fontSize / 2) = 0.75 × fontSize
     At font-size:16px: effective upward shift ≈ 12px
     Text declared at y=20 renders with effective top at approximately y=8
     In a container starting at y=0, the cap-height tops appear near y=8,
     and if the container clips at y=0, the topmost pixels of glyphs are visible
     but the text is shifted up enough to cut off the bottom half of characters -->

<svg
  id="consent-nested-svg"
  width="420"
  height="80"
  style="overflow: hidden; background: white;"
>
  <!-- Level 1: outer g — dominant-baseline:central → shift ~0.5 × 16/2 = 4px up -->
  <g dominant-baseline="central">

    <!-- Level 2: middle g — dominant-baseline:central → additional ~4px up -->
    <g dominant-baseline="central">

      <!-- Level 3: inner g — dominant-baseline:central → additional ~4px up
           Total compound shift: ~12px upward from declared y position -->
      <g dominant-baseline="central">

        <!-- Consent text declared at y=20 with font-size:16
             Natural alphabetic render: baseline at y=20, cap-height top at y≈8.8
             With 12px compound central shift: baseline effectively at y≈8,
             cap-height top at approximately y≈−3 (partially above SVG viewport)
             Lower portion of glyphs visible; upper stroke endings clipped at y=0 -->
        <text
          x="12"
          y="20"
          font-size="16"
          font-family="sans-serif"
          fill="#111"
          dominant-baseline="central"
        >Grants shell execution permission to this plugin</text>

        <text
          x="12"
          y="42"
          font-size="16"
          font-family="sans-serif"
          fill="#111"
          dominant-baseline="central"
        >Approve to allow persistent background access</text>

      </g>
    </g>
  </g>
</svg>

// Detection: traverse the ancestor chain of each consent text element,
// counting dominant-baseline:central occurrences and accumulating shift
function detectCentralNestingAttack(svgEl) {
  svgEl.querySelectorAll('text').forEach(textEl => {
    let centralCount = 0;
    let fontSize = parseFloat(getComputedStyle(textEl).fontSize);

    // Walk ancestors up to the SVG container, collecting dominant-baseline values
    let node = textEl;
    while (node && node !== svgEl) {
      const db = getComputedStyle(node).dominantBaseline;
      if (db === 'central') {
        centralCount++;
      }
      node = node.parentElement;
    }

    if (centralCount >= 2) {
      // Approximate compound shift: each central level adds ~0.25em upward
      const estimatedShiftPx = centralCount * fontSize * 0.25;
      const yAttr = parseFloat(textEl.getAttribute('y') || '0');
      const effectiveY = yAttr - estimatedShiftPx;

      console.warn(
        'SA-CSS-DB-003: ' + centralCount + ' levels of dominant-baseline:central in ancestor chain.',
        'font-size:', fontSize + 'px,',
        'declared y:', yAttr + ',',
        'estimated compound upward shift:', estimatedShiftPx.toFixed(1) + 'px,',
        'effective render y:', effectiveY.toFixed(1)
      );

      if (centralCount >= 3) {
        console.error(
          'HIGH SA-CSS-DB-003: Three or more dominant-baseline:central nesting levels detected.',
          'Estimated shift:', estimatedShiftPx.toFixed(1) + 'px upward —',
          'consent text displacement significant; verify legibility within SVG bounds.'
        );
      }
    }
  });
}

// Also detect at the g-element level for early warning
function detectCentralOnGroups(svgEl) {
  let centralGroups = 0;
  svgEl.querySelectorAll('g').forEach(gEl => {
    const db = gEl.getAttribute('dominant-baseline') ||
               getComputedStyle(gEl).dominantBaseline;
    if (db === 'central') centralGroups++;
  });
  if (centralGroups >= 2) {
    console.warn(
      'SA-CSS-DB-003 (structural signal): ' + centralGroups +
      ' <g> elements with dominant-baseline:central found in consent area SVG.',
      'May compound into significant vertical displacement on nested text elements.'
    );
  }
}

HIGH — SA-CSS-DB-003: Three levels of dominant-baseline: central in a nested <g> hierarchy accumulate to approximately 0.75 × fontSize of upward vertical displacement — 12px for a 16px font. A consent text element declared at y="20" may have its upper stroke endings clipped above the SVG viewport top edge, and its overall position is shifted to a register that obscures the relationship between the declared y coordinate and the actual rendered position. The three-level group structure is structurally plausible in SVG component frameworks, making the attack difficult to distinguish from legitimate nesting without auditing the accumulated dominant-baseline value across the ancestor chain. SkillAudit traverses all ancestor elements of consent-area text nodes and flags two or more dominant-baseline: central occurrences in the ancestor chain as a compound displacement risk.

Attack 4: dominant-baseline: ideographic shifts permission-scope badge labels below the badge background rectangle, clipping text against overflow: hidden badge containers (SA-CSS-DB-004)

The ideographic dominant baseline is specified for CJK (Chinese, Japanese, Korean) ideographic text rendering, where the baseline is defined at the bottom of the ideographic em-square — below the descender line of Latin fonts. For Latin glyphs, the ideographic baseline sits approximately 0.12em–0.2em below the natural alphabetic baseline. This means an <text> element with dominant-baseline: ideographic and y="20" renders its glyph bodies with the bottom of the ideographic em-square at y=20 — shifting the visible glyph body downward by approximately 0.15em relative to the declared coordinate. For a font-size: 11px badge label, the downward shift is approximately 0.15 × 11 = 1.65px.

MCP consent panels commonly render permission scope names as compact tags or badge elements: small colored rectangles labeled with names like “filesystem”, “network”, “shell”, “clipboard”, “camera”. Each badge is an SVG <rect> background combined with a <text> label. The badge height is typically set to exactly 1em or a fixed pixel height that matches the intended text layout. When the badge <rect> height equals 1em at the same font size as the label, the rect precisely contains the expected glyph body from the cap-height top to the descender bottom. The ideographic baseline shift pushes the glyph body downward by 1.65px — causing the bottom of the descenders to extend 1.65px below the rect’s bottom edge. If the badge container uses overflow: hidden, the descenders are clipped. On labels like “filesystem” or “shell”, the descenders of letters such as ‘y’, ‘p’, and ‘g’ are cropped, degrading legibility. For taller-body characters the shift may also clip the bottom stroke of letters like ‘e’ and ‘s’.

The attack scales with the number of permission badges. MCP servers requesting many permissions render many badges simultaneously, each with its label clipped by the same ideographic baseline shift. The dominantBaseline computed CSS property is readable via getComputedStyle, making detection straightforward once auditors know to check it on text elements within badge containers.

<!-- SA-CSS-DB-004: dominant-baseline:ideographic on badge label text
     Ideographic baseline ≈ 0.12em–0.2em below alphabetic baseline for Latin fonts
     At font-size:11px: downward shift ≈ 1.65px
     Badge rect height = 1em = 11px
     With ideographic shift: glyph descenders extend 1.65px below rect bottom
     overflow:hidden on badge container clips the descender portions
     Affects letters: y, p, g, j, q (descenders) and lower body of e, s, a

     Rendered badge appearance: top half of text visually intact,
     descender letters appear truncated at their foot → degraded legibility
     getBoundingClientRect() on text node is still fully non-zero -->

<!-- Permission badges SVG for MCP consent panel -->
<svg
  id="permission-badges-svg"
  width="480"
  height="60"
  style="background: #f8f8f8;"
>
  <!-- Badge 1: "filesystem" -->
  <!-- Badge container group with overflow:hidden via clipPath -->
  <clipPath id="badge-clip-1">
    <rect x="8" y="12" width="80" height="11" />
  </clipPath>
  <rect x="8" y="12" width="80" height="11" rx="3" fill="#fee2e2" />
  <text
    x="12"
    y="23"
    font-size="11"
    font-family="sans-serif"
    fill="#991b1b"
    clip-path="url(#badge-clip-1)"
    style="dominant-baseline: ideographic;"
  >filesystem</text>
  <!-- ideographic baseline at y=23; Latin text shifts down ~1.65px
       Descenders of 'y' in filesystem extend to y ≈ 24.65
       Badge rect bottom is y=23 → text descenders clip by 1.65px -->

  <!-- Badge 2: "shell" -->
  <clipPath id="badge-clip-2">
    <rect x="96" y="12" width="42" height="11" />
  </clipPath>
  <rect x="96" y="12" width="42" height="11" rx="3" fill="#fef3c7" />
  <text
    x="100"
    y="23"
    font-size="11"
    font-family="sans-serif"
    fill="#92400e"
    clip-path="url(#badge-clip-2)"
    style="dominant-baseline: ideographic;"
  >shell</text>

  <!-- Badge 3: "network" -->
  <clipPath id="badge-clip-3">
    <rect x="146" y="12" width="58" height="11" />
  </clipPath>
  <rect x="146" y="12" width="58" height="11" rx="3" fill="#dbeafe" />
  <text
    x="150"
    y="23"
    font-size="11"
    font-family="sans-serif"
    fill="#1e40af"
    clip-path="url(#badge-clip-3)"
    style="dominant-baseline: ideographic;"
  >network</text>

  <!-- Badge 4: "clipboard" -->
  <clipPath id="badge-clip-4">
    <rect x="212" y="12" width="62" height="11" />
  </clipPath>
  <rect x="212" y="12" width="62" height="11" rx="3" fill="#f3e8ff" />
  <text
    x="216"
    y="23"
    font-size="11"
    font-family="sans-serif"
    fill="#6b21a8"
    clip-path="url(#badge-clip-4)"
    style="dominant-baseline: ideographic;"
  >clipboard</text>

  <!-- Badge 5: "camera" -->
  <clipPath id="badge-clip-5">
    <rect x="282" y="12" width="54" height="11" />
  </clipPath>
  <rect x="282" y="12" width="54" height="11" rx="3" fill="#dcfce7" />
  <text
    x="286"
    y="23"
    font-size="11"
    font-family="sans-serif"
    fill="#166534"
    clip-path="url(#badge-clip-5)"
    style="dominant-baseline: ideographic;"
  >camera</text>
</svg>

// Detection: getComputedStyle(textEl).dominantBaseline returns the CSS-resolved value
// Flag dominant-baseline values other than 'auto' or 'alphabetic' on badge label text

function auditBadgeLabels(consentPanelEl) {
  // Query all text elements inside badge containers (small text in small rects)
  const textElements = consentPanelEl.querySelectorAll('text');

  textElements.forEach(textEl => {
    // getComputedStyle exposes dominantBaseline as a CSS property
    const db = getComputedStyle(textEl).dominantBaseline;

    // 'auto' and 'alphabetic' are safe default values for Latin text
    // All other values indicate non-standard baseline positioning
    if (db !== 'auto' && db !== 'alphabetic') {
      const fontSize = parseFloat(getComputedStyle(textEl).fontSize);

      // Estimate downward shift for ideographic baseline on Latin text
      const ideographicShiftPx = db === 'ideographic' ? fontSize * 0.15 : 0;

      // Find the associated clipPath or overflow:hidden container
      const clipPathAttr = textEl.getAttribute('clip-path');

      if (db === 'ideographic') {
        console.error(
          'MEDIUM SA-CSS-DB-004: dominant-baseline:ideographic on badge label text.',
          'font-size:', fontSize + 'px,',
          'estimated downward shift:', ideographicShiftPx.toFixed(2) + 'px.',
          'If badge rect height equals font-size, descenders extend',
          ideographicShiftPx.toFixed(2) + 'px below rect bottom.',
          'clipPath:', clipPathAttr || 'none',
          '— partial descender clip degrades permission name legibility.',
          'Text node:', textEl.textContent.trim()
        );
      } else {
        console.warn(
          'SA-CSS-DB-004 variant: dominant-baseline:', db,
          'on consent badge label — non-alphabetic baseline on permission scope name.',
          'Text:', textEl.textContent.trim()
        );
      }
    }
  });
}

// Complementary: check if badge rect height matches font-size (tight fit)
// tight-fit badges are most vulnerable to ideographic descent clip
function checkBadgeTightFit(svgEl) {
  svgEl.querySelectorAll('text').forEach(textEl => {
    const db = getComputedStyle(textEl).dominantBaseline;
    if (db !== 'ideographic') return;

    const fontSize = parseFloat(getComputedStyle(textEl).fontSize);
    const textY = parseFloat(textEl.getAttribute('y') || '0');

    // Look for a sibling rect that might be the badge background
    const parent = textEl.parentElement;
    if (!parent) return;
    const siblingRects = parent.querySelectorAll('rect');
    siblingRects.forEach(rect => {
      const rectHeight = parseFloat(rect.getAttribute('height') || '0');
      const rectY = parseFloat(rect.getAttribute('y') || '0');
      const rectBottom = rectY + rectHeight;

      // Check if text y (ideographic baseline) is at or near rect bottom
      if (Math.abs(textY - rectBottom) < 2) {
        console.warn(
          'SA-CSS-DB-004: Badge rect bottom at', rectBottom,
          'matches ideographic baseline at', textY,
          '— font-size', fontSize + 'px descenders extend ~'
          + (fontSize * 0.15).toFixed(1) + 'px below rect — descender clip confirmed.'
        );
      }
    });
  });
}

MEDIUM — SA-CSS-DB-004: dominant-baseline: ideographic on Latin badge label text shifts glyph bodies approximately 0.15em downward relative to the declared y coordinate. At font-size: 11px, this is 1.65px. Badge elements whose <rect> height equals exactly 1em provide no margin for this shift — letters with descenders (‘y’, ‘p’, ‘g’) escape the badge boundary and are clipped by the badge’s clipPath, reducing legibility of permission scope names. The dominantBaseline CSS property is directly readable via getComputedStyle(textEl).dominantBaseline — detection is straightforward once auditors know to check this property. SkillAudit flags dominant-baseline values other than auto or alphabetic on any SVG text element in the consent-flagged region, and cross-references the shift magnitude against the element’s clip boundary geometry.

Summary table

AttackMechanismWhat it hidesSeverity
SA-CSS-DB-001: dominant-baseline: text-top — em-box top at declared y, text hangs below With text-top, the em-box top sits at the declared y coordinate; glyph body extends downward; 32px font at y=0 renders from y=0 to y=32; SVG height=30 shows only top 30px, leaving only ~2px of antialiased glyph top edge visible Entire readable consent text body below the clip boundary; getBoundingClientRect() returns non-zero rect; text appears as sub-pixel smear at container top Critical
SA-CSS-DB-002: dominant-baseline: mathematical + alignment-baseline: mathematical on <tspan> children — additive upward shift Parent mathematical dominant baseline shifts ~0.25em upward; alignment-baseline: mathematical on each <tspan> adds another ~0.25em; total ~0.5em (7px at 14px font) upward per tspan; first line exits top of consent panel Preamble line “This plugin requests access to:” clipped above container top; users see bullet items without the access-request framing, misreading the consent context High
SA-CSS-DB-003: dominant-baseline: central on three nested <g> elements — compound 12px upward shift at 16px font Each <g dominant-baseline="central"> level adds ~0.25em upward to child text rendering; three levels = ~0.75 × fontSize = 12px at 16px; text at y=20 renders with top portion at approximately y=8, clipping cap-height tops against y=0 container edge Upper strokes of consent text letterforms clipped above SVG viewport; text partially legible but character tops cut off; three-level group nesting appears structurally normal in SVG component frameworks High
SA-CSS-DB-004: dominant-baseline: ideographic shifts Latin badge labels ~0.15em below badge rect boundary Ideographic baseline for Latin text places reference point ~0.15em below alphabetic baseline; glyph body shifts downward 1.65px at 11px font; badge rects sized to exactly 1em height cannot accommodate the shift; descenders clip against badge clipPath Permission scope badge labels (“filesystem”, “shell”, “network”) have descenders clipped simultaneously across all badges; getComputedStyle(textEl).dominantBaseline returns "ideographic" — CSS-readable Medium

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-DB-001: dominant-baseline: text-top; font-size: 32px; y="0" inside SVG with height="30" overflow:hidden — text em-box top at y=0, body extends to y=32; only top 30px of 32px em-box visible; readable glyph body entirely below clip boundary at y=30; getBoundingClientRect() returns non-zero rect evading emptiness checks; detection requires computing alphabetic baseline position (y + 0.7 × fontSize for text-top) and verifying it falls within SVG height.
HIGH SA-CSS-DB-002: dominant-baseline: mathematical on <text> parent combined with alignment-baseline: mathematical on all <tspan> children — additive upward shift ~0.5em per tspan; at font-size:14px, first-line effective y shifts ~7px above declared position; preamble line “This plugin requests access to:” clips above container top in consent panels with top text position y ≤ 8px; users misread isolated bullet items as feature list rather than permission access request.
HIGH SA-CSS-DB-003: three-level <g dominant-baseline="central"> nesting with inner <text dominant-baseline="central"> — compound upward displacement ~0.75 × font-size; at font-size:16px, 12px upward shift moves text declared at y=20 to effective render position y≈8; cap-height tops clip against SVG y=0 boundary; detection requires traversing all ancestor elements and accumulating central dominant-baseline occurrences in the ancestor chain — single-element computed style inspection is insufficient.
MEDIUM SA-CSS-DB-004: dominant-baseline: ideographic on permission-scope badge label <text> elements with font-size:11px — Latin glyph body shifts ~1.65px below declared y coordinate; badge <rect> height sized to exactly 1em (11px) provides no margin; descenders of ‘y’, ‘p’, ‘g’ in permission names (“filesystem”, “clipboard”) clip against badge clipPath boundary; getComputedStyle(textEl).dominantBaseline returns "ideographic" — flag any non-alphabetic value on badge label text in consent areas.

Related: SVG dominant-baseline general security  |  SVG textLength consent attacks  |  text-indent consent attacks  |  word-spacing consent attacks

← Blog  |  Security Checklist