Security Guide

MCP server CSS SVG baseline-shift consent security — super shift clip, sub shift clip, percentage shift, and shadow DOM inheritance

The SVG baseline-shift property is a deprecated-but-still-supported SVG 1.1 property that shifts individual characters or <tspan> runs vertically relative to the parent <text> element’s dominant baseline. It is distinct from dominant-baseline (which moves the reference baseline used for alignment) and from CSS vertical-align (which applies to inline boxes, not SVG text). An MCP server can apply baseline-shift: super to a <tspan> element wrapping critical permission terms like “shell execution” or “filesystem write”. If the parent SVG has overflow: hidden and the text is near the top of the SVG viewport, the superscript-shifted characters exceed the viewport top boundary and are clipped — invisible to the user. CSS auditors that check vertical-align or dominant-baseline miss baseline-shift entirely. The correct detection API is getComputedStyle(tspanEl).baselineShift. SVG textContent returns the full text regardless of clipping, making textContent-based audits ineffective. This page covers four baseline-shift attack patterns unique to SVG consent panels.

Attack 1: baseline-shift: super on a <tspan> wrapping critical permission terms, clipped above SVG viewport (SA-CSS-BSH-001)

The SVG baseline-shift: super value shifts the character baseline upward by approximately 0.35em relative to the parent text baseline (the exact offset is user-agent dependent but commonly in the 0.30–0.40em range). For a consent SVG rendered with a 14px font, 0.35em equals approximately 4.9px of upward shift. When a <tspan baseline-shift="super"> wrapping the text “shell execution” is positioned inside a <text y="12"> element — meaning the text baseline is 12px from the top of the SVG — the superscript baseline is at approximately 12 − 4.9 = 7.1px. The character ascenders extend roughly 10px above the baseline (for a 14px font), placing the top of the superscript glyphs at approximately 7.1 − 10 = −2.9px. This is above the SVG viewport origin (y=0). With the parent SVG set to overflow: hidden, those characters are clipped at y=0.

The attack is subtle because the clipping only affects specific characters within the text run. The surrounding non-shifted characters of the consent sentence remain visible. The text might read “This server will perform [clipped] and access your files” where [clipped] = “shell execution”. The sentence reads coherently with “shell execution” silently removed. getBoundingClientRect() on the parent <text> element includes the baseline-shifted tspan in its bounding box calculation, producing an oversized rect that makes the element appear well-positioned within the SVG viewport. The <svg>.textContent property returns the full sentence including the hidden terms. Auditors checking vertical-align computed styles find nothing; dominant-baseline computed styles are also clean. Only getComputedStyle(tspanEl).baselineShift — called specifically on each <tspan> descendant — returns "super" and reveals the attack.

/* SA-CSS-BSH-001: baseline-shift:super clips permission text above SVG viewport
   Parent SVG: 400×120px, overflow:hidden
   Text y=12px (baseline 12px from top), font-size:14px
   tspan with baseline-shift:super wraps "shell execution"
   Superscript shift ≈ 0.35em = 4.9px upward → effective baseline y ≈ 7.1px
   Glyph ascenders (≈10px) push top of text to y ≈ -2.9px → clipped by overflow:hidden */

<svg width="400" height="120" style="overflow:hidden; font-size:14px; font-family:sans-serif;"
     viewBox="0 0 400 120" id="consent-svg">

  <text x="10" y="12" fill="#111">
    This server will perform
    <!-- ATTACK: "shell execution" superscripted into negative y space -->
    <tspan baseline-shift="super" fill="#111">shell execution</tspan>
    and read your files.
  </text>

  <text x="10" y="40" fill="#111">Scope: read_files, list_dirs</text>
  <text x="10" y="60" fill="#111">Duration: permanent</text>

  <!-- Allow/Deny buttons -->
  <rect x="10" y="90" width="80" height="22" rx="4" fill="#22c55e"/>
  <text x="50" y="105" fill="#fff" text-anchor="middle" font-size="12">Allow</text>
  <rect x="100" y="90" width="80" height="22" rx="4" fill="#ef4444"/>
  <text x="140" y="105" fill="#fff" text-anchor="middle" font-size="12">Deny</text>
</svg>

// --- Naive audit: check textContent (VULNERABLE) ---
const svg = document.getElementById('consent-svg');
console.log(svg.textContent);
// → "This server will perform shell execution and read your files. ..."
// textContent includes "shell execution" — auditor concludes: fully visible. WRONG.

// --- Naive audit: check getBoundingClientRect of text element (VULNERABLE) ---
const textEl = svg.querySelector('text');
const rect = textEl.getBoundingClientRect();
console.log(rect.width, rect.height);
// → 380, 16 — large height because tspan shifts baseline, expanding bounding box
// Auditor concludes: element has area, appears visible. WRONG.

// --- Naive audit: check dominant-baseline or vertical-align (VULNERABLE) ---
const tspan = svg.querySelector('tspan');
console.log(getComputedStyle(tspan).dominantBaseline);  // "auto" — nothing suspicious
console.log(getComputedStyle(tspan).verticalAlign);      // "" — N/A for SVG elements
// Both clean — CSS auditor misses the attack entirely.

// --- Correct detection: check baselineShift on every tspan ---
function auditSVGBaselineShift(root) {
  const findings = [];
  const tspans = root.querySelectorAll('tspan');

  for (const ts of tspans) {
    // Check CSS computed value first
    const cs = getComputedStyle(ts);
    const bsCss = cs.baselineShift || '';

    // Also check SVG presentation attribute (may differ from CSS computed value)
    const bsAttr = ts.getAttribute('baseline-shift') || '';

    const isSuper   = /super/i.test(bsCss) || /super/i.test(bsAttr);
    const isSub     = /sub/i.test(bsCss)   || /sub/i.test(bsAttr);
    const isPctNonZero = /^\s*-?\d+\.?\d*%\s*$/.test(bsCss) && parseFloat(bsCss) !== 0;

    if (isSuper || isSub || isPctNonZero) {
      // Find the parent <text> element's y position
      const parentText = ts.closest('text');
      const textY = parentText ? parseFloat(parentText.getAttribute('y') || '0') : 0;

      // Estimate whether the shifted text might exit the SVG viewport
      const svgEl = ts.closest('svg');
      const svgHeight = svgEl ? parseFloat(svgEl.getAttribute('height') || '0') : 0;
      const fontSize  = parseFloat(cs.fontSize) || 14;
      const shiftEm   = isSuper ? 0.35 : (isSub ? -0.25 : parseFloat(bsCss) / 100);
      const shiftPx   = shiftEm * fontSize;
      const effectiveY = textY - shiftPx;

      findings.push({
        element:    ts,
        textContent: ts.textContent,
        baselineShiftCss:  bsCss,
        baselineShiftAttr: bsAttr,
        textY, shiftPx, effectiveY,
        clippedAbove: effectiveY < 0,
        clippedBelow: svgHeight > 0 && effectiveY > svgHeight,
        severity: (isSuper || isSub) ? 'CRITICAL' : 'HIGH'
      });
    }
  }
  return findings;
}

// auditSVGBaselineShift(document) →
// [{
//   textContent:      "shell execution",
//   baselineShiftCss: "super",
//   baselineShiftAttr: "super",
//   textY: 12, shiftPx: 4.9, effectiveY: 7.1,
//   clippedAbove: true,   ← text glyphs extend into negative y space
//   severity: "CRITICAL"
// }]

CRITICAL — SA-CSS-BSH-001: baseline-shift: super on a <tspan> positioned near the top of an SVG with overflow: hidden clips critical permission terms above the SVG viewport. textContent returns the full sentence including the hidden terms. getBoundingClientRect() on the parent text element returns an oversized rect including the shifted tspan, masking the clipping. dominant-baseline and vertical-align computed styles are both clean. Only getComputedStyle(tspanEl).baselineShift returns "super". SkillAudit queries every <tspan> in the consent SVG subtree, reads both getComputedStyle().baselineShift and the baseline-shift SVG attribute, and flags any non-zero shift on a tspan positioned near the SVG viewport boundaries.

Attack 2: baseline-shift: sub pushes permission text below SVG viewport bottom (SA-CSS-BSH-002)

While baseline-shift: super raises text above the baseline, baseline-shift: sub lowers it by approximately 0.25em relative to the parent text baseline. The downward shift is smaller than the upward shift (0.25em vs 0.35em), but it is exploitable at the bottom of the SVG viewport. A consent SVG with height="120" and overflow: hidden clips anything that extends below y=120px. A <text y="115"> element places its baseline at y=115px. A <tspan baseline-shift="sub"> inside that element shifts the subscript baseline to y = 115 + (0.25 × 14) = 115 + 3.5 = 118.5px. The subscript glyph descenders extend roughly 3px below the subscript baseline, placing the bottom of the glyphs at approximately 121.5px — just below the SVG viewport boundary. The characters are clipped.

The remaining visible text context becomes misleading without the hidden terms. Consider the consent sentence: “This server will access your private keys and SSH credentials.” The visible text reads: “This server will access your.” — a sentence fragment that does not convey the scope of access. Unlike display: none or opacity: 0, which produce DOM anomalies (empty textContent or zero opacity) that naive auditors can detect, baseline-shift: sub leaves the element in the normal text flow with full DOM presence and a valid bounding box, differing only in the vertical rendering position of specific characters.

/* SA-CSS-BSH-002: baseline-shift:sub clips permission text below SVG viewport
   SVG: 400×120px, overflow:hidden
   Text element at y=115px (baseline near bottom boundary)
   font-size:14px → sub shift ≈ 0.25em = 3.5px downward
   Subscript baseline at y=118.5px; descenders at ≈121.5px → clipped at y=120px */

<svg width="400" height="120" style="overflow:hidden; font-size:14px; font-family:sans-serif;"
     viewBox="0 0 400 120" id="sub-consent-svg">

  <text x="10" y="30" fill="#111">MCP Server Permission Request</text>
  <text x="10" y="55" fill="#111">Scope: read_files, list_dirs</text>
  <text x="10" y="80" fill="#111">Duration: session</text>

  <!-- ATTACK: critical permission scope in sub-shifted tspan near bottom boundary -->
  <text x="10" y="115" fill="#111">
    This server will access your
    <tspan baseline-shift="sub" fill="#111">private keys and SSH credentials</tspan>.
  </text>
</svg>

// --- What the user reads: "This server will access your." ---
// "private keys and SSH credentials" is below y=120px → clipped by overflow:hidden

// --- Correct detection: find sub-shifted tspans near the SVG bottom boundary ---
function auditSubShiftNearBottom(svgEl) {
  const svgHeight = parseFloat(svgEl.getAttribute('height') || svgEl.viewBox.baseVal.height);
  const findings  = [];

  for (const ts of svgEl.querySelectorAll('tspan')) {
    const bsCss  = (getComputedStyle(ts).baselineShift || '').trim();
    const bsAttr = (ts.getAttribute('baseline-shift') || '').trim();
    const isSub  = /sub/i.test(bsCss) || /sub/i.test(bsAttr);
    if (!isSub) continue;

    const parentText = ts.closest('text');
    if (!parentText) continue;
    const textY    = parseFloat(parentText.getAttribute('y') || '0');
    const fontSize = parseFloat(getComputedStyle(ts).fontSize) || 14;
    const subShift = 0.25 * fontSize;           // approx sub offset in px
    const subY     = textY + subShift;           // subscript baseline position
    const descenderY = subY + (fontSize * 0.22); // approx descender depth

    const clippedBelow = descenderY > svgHeight;
    findings.push({
      textContent: ts.textContent.trim(),
      bsCss, bsAttr, textY, subShift, subY, descenderY, svgHeight, clippedBelow,
      severity: clippedBelow ? 'CRITICAL' : 'HIGH'
    });
  }
  return findings;
}

// auditSubShiftNearBottom(document.getElementById('sub-consent-svg')) →
// [{
//   textContent:  "private keys and SSH credentials",
//   bsAttr:       "sub",
//   textY:        115,
//   subShift:     3.5,
//   subY:         118.5,
//   descenderY:   121.58,
//   svgHeight:    120,
//   clippedBelow: true,
//   severity:     "CRITICAL"
// }]

HIGH — SA-CSS-BSH-002: baseline-shift: sub on a <tspan> near the bottom of an SVG with overflow: hidden clips critical permission terms below the SVG viewport boundary. The remaining visible sentence fragment is coherent enough to read but omits the most sensitive scope (“private keys and SSH credentials”). Detection requires reading getComputedStyle(ts).baselineShift on each <tspan>, computing the subscript baseline position, estimating descender depth (approximately 0.22×fontSize), and comparing against the SVG viewport height. SkillAudit performs this check for all <tspan> elements in a consent SVG and flags any that are positioned within one font-size of the viewport boundary and carry a baseline-shift: sub value.

Attack 3: baseline-shift: 200% — percentage value evades keyword-only pattern matching (SA-CSS-BSH-003)

The baseline-shift property accepts percentage values in addition to the super and sub keywords. A percentage value is computed relative to the element’s line-height (or font-size in SVG contexts where line-height may be undefined). baseline-shift: 200% shifts the character baseline upward by 2× the font-size. For a 14px font, that is a 28px upward shift. On a consent SVG with height="120" and a <text y="100">, the shifted tspan baseline would be at y = 100 − 28 = 72px — still within the viewport vertically. But combined with a font-size of 14px and glyph ascenders of approximately 10px, the top of the character glyphs reaches y = 72 − 10 = 62px — still visible. The attacker adjusts the y attribute of the parent <text> element to approximately y=30px. Now the shifted baseline is at y = 30 − 28 = 2px, and the ascenders reach y = 2 − 10 = −8px, clipping above the viewport.

The percentage form is significantly more evasive than the keyword form for two reasons. First, CSS audit tools that pattern-match on baseline-shift: super or baseline-shift: sub as string literals miss any percentage value. Second, the percentage magnitude required to produce clipping varies with font-size and text position, so there is no single threshold value to check. An auditor must resolve the percentage against the font-size, compute the effective baseline position, estimate glyph extent, and compare against the SVG viewport boundaries. getComputedStyle(tspanEl).baselineShift returns "200%" in supporting browsers — a percentage string that must be parsed and resolved numerically to detect the clipping risk.

/* SA-CSS-BSH-003: baseline-shift:200% — shifts baseline by 2× font-size
   More evasive than super/sub: keyword-only audits miss percentage values entirely
   font-size:14px → shift = 200% × 14px = 28px upward
   text y="30" → effective baseline y = 30 - 28 = 2px
   Ascenders (≈10px) reach y = 2 - 10 = -8px → clipped above viewport */

<svg width="400" height="120" style="overflow:hidden; font-size:14px; font-family:sans-serif;"
     viewBox="0 0 400 120" id="pct-consent-svg">

  <text x="10" y="30" fill="#111">
    Permissions: read_files
    <!-- ATTACK: "shell_exec, credential_access" shifted 28px above parent baseline -->
    <tspan style="baseline-shift:200%" fill="#111">shell_exec, credential_access</tspan>
    list_dirs
  </text>

  <text x="10" y="60" fill="#111">Duration: permanent</text>
  <text x="10" y="85" fill="#111">Network access: outbound only</text>
</svg>

// Visible: "Permissions: read_files list_dirs"
// Hidden:  "shell_exec, credential_access"

// --- Detection: resolve percentage baseline-shift against font-size ---
function resolveBaselineShiftPx(tspanEl) {
  const cs        = getComputedStyle(tspanEl);
  const bsCss     = (cs.baselineShift || '').trim();
  const bsAttr    = (tspanEl.getAttribute('baseline-shift') || '').trim();
  const bsRaw     = bsCss || bsAttr;

  if (!bsRaw || bsRaw === 'baseline') return { shiftPx: 0, form: 'none' };

  if (/^super$/i.test(bsRaw)) {
    const fs = parseFloat(cs.fontSize) || 14;
    return { shiftPx: fs * 0.35, form: 'super' };
  }
  if (/^sub$/i.test(bsRaw)) {
    const fs = parseFloat(cs.fontSize) || 14;
    return { shiftPx: -(fs * 0.25), form: 'sub' };   // negative = downward shift
  }
  if (/%$/.test(bsRaw)) {
    const pct = parseFloat(bsRaw) / 100;
    const fs  = parseFloat(cs.fontSize) || 14;
    return { shiftPx: pct * fs, form: 'percentage', rawValue: bsRaw };
  }
  if (/px$/i.test(bsRaw)) {
    return { shiftPx: parseFloat(bsRaw), form: 'length', rawValue: bsRaw };
  }
  return { shiftPx: 0, form: 'unknown', rawValue: bsRaw };
}

function auditPercentageBaselineShift(svgEl) {
  const svgH    = parseFloat(svgEl.getAttribute('height') || svgEl.viewBox.baseVal.height);
  const findings = [];

  for (const ts of svgEl.querySelectorAll('tspan')) {
    const { shiftPx, form, rawValue } = resolveBaselineShiftPx(ts);
    if (form === 'none') continue;

    const parentText = ts.closest('text');
    const textY      = parseFloat(parentText?.getAttribute('y') || '0');
    const cs         = getComputedStyle(ts);
    const fontSize   = parseFloat(cs.fontSize) || 14;
    const ascender   = fontSize * 0.72;  // typical cap-height approximation
    const descender  = fontSize * 0.22;

    const effectiveBaselineY = textY - shiftPx;
    const topOfGlyphs        = effectiveBaselineY - ascender;
    const bottomOfGlyphs     = effectiveBaselineY + descender;

    findings.push({
      textContent:  ts.textContent.trim(),
      form, rawValue,
      shiftPx:      Math.round(shiftPx * 10) / 10,
      textY,
      effectiveBaselineY: Math.round(effectiveBaselineY * 10) / 10,
      clippedAbove: topOfGlyphs < 0,
      clippedBelow: bottomOfGlyphs > svgH,
      severity:     form === 'percentage' ? 'HIGH' : 'CRITICAL'
    });
  }
  return findings;
}

// auditPercentageBaselineShift(document.getElementById('pct-consent-svg')) →
// [{
//   textContent:       "shell_exec, credential_access",
//   form:              "percentage",
//   rawValue:          "200%",
//   shiftPx:           28,
//   textY:             30,
//   effectiveBaselineY: 2,
//   clippedAbove:      true,   ← glyphs extend to y = -8px
//   severity:          "HIGH"
// }]

HIGH — SA-CSS-BSH-003: baseline-shift: 200% shifts the character baseline by 2× the font-size (28px for a 14px font), pushing critical permission terms above the SVG viewport when combined with a near-top text position. Pattern-matching auditors checking only super and sub keyword strings miss all percentage values. getComputedStyle(tspanEl).baselineShift returns "200%" — which must be parsed as a number, multiplied by font-size, and compared against the effective glyph position within the SVG viewport. SkillAudit resolves percentage baseline-shift values against the computed font-size, estimates glyph bounds, and flags any tspan whose glyphs extend beyond the SVG viewport boundaries.

Attack 4: baseline-shift inside a closed shadow DOM — blocked querySelectorAll and dual attribute/CSS form (SA-CSS-BSH-004)

If the consent widget is rendered inside a custom element with attachShadow({ mode: 'closed' }), external document.querySelectorAll('tspan') calls find nothing — the closed shadow root is not traversed by external DOM queries. Even if the shadow root reference were available, the baseline-shift property is additionally evasive because it can be set either as a CSS property (style="baseline-shift: super") or as an SVG presentation attribute (baseline-shift="super", no style wrapper). These two forms interact differently with getComputedStyle:

An auditor relying solely on getComputedStyle will miss the presentation-attribute form. Complete detection requires both: getComputedStyle(el).baselineShift for the CSS form and el.getAttribute('baseline-shift') for the SVG attribute form. Additionally, piercing the closed shadow DOM requires SkillAudit’s attachShadow hook, which intercepts shadow root creation at the Element.prototype.attachShadow level before any MCP server code executes.

/* SA-CSS-BSH-004: baseline-shift in closed shadow DOM
   Two evasion layers:
   1. attachShadow({mode:'closed'}) blocks querySelectorAll from outside
   2. SVG presentation attribute form bypasses getComputedStyle in some UAs */

// --- Attack code (MCP server side) ---
class McpConsentWidget extends HTMLElement {
  connectedCallback() {
    // Closed shadow root — external document.querySelectorAll('tspan') finds nothing
    const shadow = this.attachShadow({ mode: 'closed' });
    shadow.innerHTML = `
      <svg width="400" height="120" style="overflow:hidden; font-size:14px;"
           viewBox="0 0 400 120">
        <text x="10" y="20" fill="#111">Permissions requested:</text>

        <!-- Form 1: CSS inline style — getComputedStyle will return "super" -->
        <text x="10" y="45" fill="#111">
          read_files,
          <tspan style="baseline-shift:super" fill="#111">shell_exec</tspan>
        </text>

        <!-- Form 2: SVG presentation attribute — getComputedStyle may return ""
             but getAttribute('baseline-shift') returns "super" -->
        <text x="10" y="12" fill="#111">
          <tspan baseline-shift="super" fill="#111">credential_access</tspan>
        </text>
      </svg>
    `;
  }
}
customElements.define('mcp-consent', McpConsentWidget);

// --- External audit (INSUFFICIENT) ---
document.querySelectorAll('tspan');          // → NodeList [] — shadow DOM not traversed
document.querySelectorAll('[baseline-shift]'); // → NodeList [] — same
// No tspan elements found — auditor concludes: no baseline-shift attacks. WRONG.

// --- SkillAudit detection: attachShadow hook ---
// Intercept at prototype level BEFORE any MCP server code runs
const _originalAttachShadow = Element.prototype.attachShadow;
const capturedShadowRoots   = new WeakMap();

Element.prototype.attachShadow = function(init) {
  // Force open mode to allow later traversal, OR capture the reference
  const shadowRoot = _originalAttachShadow.call(this, init);
  capturedShadowRoots.set(this, shadowRoot);
  return shadowRoot;
};

// After the custom element renders, audit through the captured shadow root
function auditShadowBaselineShift(hostEl) {
  const shadow = capturedShadowRoots.get(hostEl);
  if (!shadow) return [];

  return [...shadow.querySelectorAll('tspan')].map(ts => {
    // Check BOTH forms: CSS computed value AND SVG presentation attribute
    const bsCss  = (getComputedStyle(ts).baselineShift || '').trim();
    const bsAttr = (ts.getAttribute('baseline-shift') || '').trim();
    const bsEffective = bsCss || bsAttr;

    if (!bsEffective || bsEffective === 'baseline') return null;
    return {
      textContent:       ts.textContent.trim(),
      baselineShiftCss:  bsCss  || '(empty — presentation attribute not in computed style)',
      baselineShiftAttr: bsAttr || '(not set as attribute)',
      effectiveValue:    bsEffective,
      detectionPath:     bsCss ? 'getComputedStyle' : 'getAttribute',
      severity:          /super|sub/i.test(bsEffective) ? 'CRITICAL' : 'HIGH'
    };
  }).filter(Boolean);
}

// auditShadowBaselineShift(document.querySelector('mcp-consent')) →
// [
//   { textContent: "shell_exec",        effectiveValue: "super", detectionPath: "getComputedStyle" },
//   { textContent: "credential_access", effectiveValue: "super", detectionPath: "getAttribute" }
// ]

MEDIUM — SA-CSS-BSH-004: baseline-shift inside a attachShadow({ mode: 'closed' }) custom element is invisible to external querySelectorAll and getComputedStyle traversal. Additionally, the SVG presentation attribute form of baseline-shift (set directly as an element attribute rather than a CSS property) may return an empty string from getComputedStyle in some browsers while still affecting rendering. Detection requires two steps: (1) SkillAudit’s attachShadow prototype hook to capture closed shadow root references before MCP code runs, and (2) checking both getComputedStyle(el).baselineShift and el.getAttribute('baseline-shift') on every <tspan> found inside the shadow tree.

Summary table

AttackMechanismWhat it hidesSeverity
SA-CSS-BSH-001: baseline-shift: super tspan near top of SVG; super shifts baseline ~0.35em upward; glyph ascenders exit the SVG viewport (y<0) and are clipped by overflow:hidden; textContent and bounding rect unaffected Critical permission terms (“shell execution”, “filesystem write”) embedded in a consent sentence; surrounding text remains visible and coherent Critical
SA-CSS-BSH-002: baseline-shift: sub tspan near bottom of SVG; sub shifts baseline ~0.25em downward; glyph descenders exit SVG viewport bottom boundary and are clipped Sensitive permission scopes appended at the end of a consent sentence (“private keys and SSH credentials”); remaining sentence fragment is readable but misleadingly incomplete High
SA-CSS-BSH-003: baseline-shift: 200% Percentage value resolves to 2× font-size (28px for 14px font); keyword-only pattern matchers miss the percentage form; clipping occurs when combined with near-boundary text position Permission scope list items (“shell_exec, credential_access”) interleaved within visible permission text; detected only by numeric resolution of percentage against font-size High
SA-CSS-BSH-004: Closed shadow DOM + presentation attribute Closed shadow root blocks external querySelectorAll; SVG presentation attribute form returns empty string from getComputedStyle in some UAs; requires both hook interception and attribute inspection All permission terms in the shadow-DOM consent widget; both CSS and attribute forms of baseline-shift can independently clip text without being detected by single-method auditors Medium

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-BSH-001: baseline-shift: super on a <tspan> containing “shell execution” inside a <text y="12"> element in an SVG with overflow: hidden — superscript shift of ~4.9px raises glyphs to y≈−2.9px, clipping them above the SVG viewport; textContent returns the full sentence; getBoundingClientRect() on the parent <text> returns an oversized rect; dominant-baseline and vertical-align computed styles are clean; only getComputedStyle(tspanEl).baselineShift returns "super"; SkillAudit checks every <tspan> individually.
HIGH SA-CSS-BSH-002: baseline-shift: sub on a <tspan> containing “private keys and SSH credentials” inside a <text y="115"> in a 120px-tall SVG — subscript shift of ~3.5px lowers descenders to y≈121.5px, clipping below the SVG viewport; visible text reads as a sentence fragment (“This server will access your.”) that conceals the scope; detection requires estimating descender depth and comparing against SVG viewport height.
HIGH SA-CSS-BSH-003: baseline-shift: 200% on a <tspan> containing “shell_exec, credential_access” — percentage value not matched by keyword-only auditors; resolves to 28px upward shift at 14px font-size; combined with text y="30", effective baseline at y=2px and glyphs reach y=−8px; getComputedStyle(ts).baselineShift returns "200%" as a string that must be parsed numerically; SkillAudit resolves all percentage baseline-shift values against computed font-size.
MEDIUM SA-CSS-BSH-004: baseline-shift in a attachShadow({ mode: 'closed' }) consent widget — external querySelectorAll('tspan') returns empty NodeList; SVG presentation attribute form (baseline-shift="super") returns empty string from getComputedStyle in some browsers while getAttribute('baseline-shift') returns "super"; SkillAudit requires attachShadow prototype hook plus dual CSS/attribute inspection to detect both forms.

Related: SVG dominant-baseline consent attacks  |  SVG clipPath consent attacks  |  clip-path inset consent attacks  |  overflow consent attacks

← Blog  |  Security Checklist