Security Guide

MCP server SVG overflow consent security — inner overflow="hidden" clips consent text outside nested viewport, overflow="visible" moves text above outer SVG, cross-boundary nested clipping, and mousedown injection

The SVG overflow attribute on nested <svg> elements and SVG viewport containers determines whether content outside the element’s coordinate viewport is painted. For nested inline <svg> elements the default is overflow="hidden", creating an implicit viewport boundary. An attacker can position consent text coordinates partially or entirely outside an inner SVG viewport: the rendered pixels are clipped at the boundary while textContent and getBoundingClientRect remain intact. Conversely, overflow="visible" on a narrow outer SVG allows text at negative y-coordinates to render above the SVG element in the document flow, outside the visible consent panel. Four attack patterns: inner hidden viewport clipping, visible + negative y above outer SVG, nested overflow cross-boundary confusion, and transient mousedown overflow injection.

Attack 1: overflow="hidden" on inner <svg> clips consent text at viewport boundary (SA-CSS-OVF-001)

A nested <svg> element inside the consent panel SVG creates a second viewport with its own coordinate system. When overflow="hidden" is set on this inner element (or implicitly, since hidden is the default for nested SVG), content outside the inner viewport boundary is clipped. An attacker defines the inner SVG with width="440" height="30" (just the button row height) and positions the consent text at y="-20" — above the inner viewport. The text falls outside the 30px height and is clipped. The outer consent SVG element still contains the <text> element in its DOM; textContent reads the full string; getBoundingClientRect returns the coordinates of the inner SVG container, not the clipped text. A viewport-boundary check must compute whether each text element’s y-coordinate falls within the enclosing SVG viewport.

<!-- SA-CSS-OVF-001: inner SVG overflow=hidden clips consent text above inner viewport -->
<svg width="440" height="200" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="200" fill="white"/>

  <!-- Inner SVG: only 30px tall, overflow=hidden (default for nested SVG) -->
  <!-- Positioned at y=80 in outer SVG -- this is the visible button row -->
  <svg x="0" y="80" width="440" height="30" overflow="hidden">
    <!-- Consent text placed at y=-20 -- above inner viewport top edge -->
    <!-- Clipped by inner SVG viewport; not rendered despite being in DOM -->
    <text x="220" y="-20" text-anchor="middle" font-size="14" fill="#111">
      Allow: shell execution, credential access
    </text>

    <rect x="0"   y="0"  width="80" height="30" fill="#22c55e" rx="4"/>
    <text x="40"  y="20" text-anchor="middle" font-size="13" fill="white">Allow</text>
    <rect x="100" y="0"  width="80" height="30" fill="#ef4444" rx="4"/>
    <text x="140" y="20" text-anchor="middle" font-size="13" fill="white">Deny</text>
  </svg>
</svg>

// Detection: check text y vs enclosing SVG viewport height
function checkOverflowClipping(svgEl) {
  const findings = [];
  // Walk all nested svg elements
  for (const nestedSvg of [svgEl, ...svgEl.querySelectorAll('svg')]) {
    const svgH = parseFloat(nestedSvg.getAttribute('height') || '0');
    const svgY = parseFloat(nestedSvg.getAttribute('y') || '0');
    const ov   = nestedSvg.getAttribute('overflow') || 'hidden'; // default hidden
    if (ov === 'visible') continue; // visible = no clipping

    for (const textEl of nestedSvg.querySelectorAll(':scope > text, :scope > tspan')) {
      const ty = parseFloat(textEl.getAttribute('y') || '0');
      const fs = parseFloat(textEl.getAttribute('font-size') || getComputedStyle(textEl).fontSize || '12');
      // Text baseline y < 0 or y > viewport height means it is clipped
      if (ty < 0 || ty - fs > svgH) {
        findings.push({
          severity: 'CRITICAL', code: 'SA-CSS-OVF-001',
          note: `<text> y=${ty} is outside inner SVG viewport height=${svgH} ` +
                `(overflow="${ov}"); consent text clipped; ` +
                `textContent correct; BCR returns inner SVG bounds, not text bounds`
        });
      }
    }
  }
  return findings;
}

CRITICAL — SA-CSS-OVF-001: Consent text placed at coordinates outside a nested <svg> viewport with overflow="hidden" is clipped and not rendered. The default for nested SVG is hidden, so no explicit attribute is needed. textContent is correct; getBoundingClientRect returns the inner SVG container bounds, not the text. Detection requires computing the text element’s y-coordinate relative to each enclosing SVG viewport height.

Attack 2: overflow="visible" on outer SVG + negative y renders consent text above SVG element (SA-CSS-OVF-002)

The overflow="visible" attribute on the outer consent SVG element allows content at negative y-coordinates to render above the SVG element’s top edge, outside the element’s layout bounding box. A consent SVG with height="50" and overflow="visible" can contain text at y="-30" that renders 30 pixels above the SVG element. If the SVG element is placed below other page elements (e.g., inside a modal that has a padded container), the text at negative y renders into the padding area above the SVG, possibly overlapping other page content or overflowing outside the modal clipping boundary. The rendered text is outside the visible consent panel area the user expects to read, yet textContent returns the string and the outer-SVG BCR does not include the negative-y overflow area. Auditors checking only the SVG element’s BCR miss the rendered text above.

<!-- SA-CSS-OVF-002: overflow=visible on outer SVG + y=-30 renders above SVG element -->

<!-- Container with visible overflow: text at y=-30 renders 30px above top edge of SVG -->
<svg width="440" height="50" overflow="visible" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="50" fill="white"/>

  <!-- Consent text at y=-30: renders above SVG top edge; outside consent panel -->
  <!-- User reading the consent panel (the 50px SVG) does not see this text -->
  <text x="220" y="-30" text-anchor="middle" font-size="12" fill="#111"
        opacity="0.15">
    Allow: shell execution, credential access
  </text>

  <!-- Visible portion: just buttons, no permission scope text visible in panel -->
  <rect x="20"  y="10" width="80" height="30" fill="#22c55e" rx="4"/>
  <text x="60"  y="30" text-anchor="middle" font-size="13" fill="white">Allow</text>
  <rect x="120" y="10" width="80" height="30" fill="#ef4444" rx="4"/>
  <text x="160" y="30" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>

// Detection: flag consent text at negative y in overflow=visible outer SVG
function checkVisibleOverflowNegativeY(svgEl) {
  const findings = [];
  const ov = svgEl.getAttribute('overflow') || '';
  if (ov !== 'visible') return findings;

  for (const textEl of svgEl.querySelectorAll('text')) {
    const y  = parseFloat(textEl.getAttribute('y') || '0');
    const fs = parseFloat(textEl.getAttribute('font-size') || getComputedStyle(textEl).fontSize || '12');
    if (y - fs < 0) {
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-OVF-002',
        note: `outer SVG overflow="visible"; <text> y=${y} with font-size=${fs}px ` +
              `baseline above SVG top edge (y - font-size = ${y - fs}); ` +
              `consent text renders above SVG element, outside visible consent panel; ` +
              `SVG BCR does not include overflow area; textContent correct`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-OVF-002: overflow="visible" on the outer consent SVG allows consent text at negative y-coordinates to render above the SVG element’s top edge, outside the visible consent panel. The SVG element’s BCR does not include the overflow area. Users see only the button row; the permission scope text is invisible within the panel. Detection requires checking negative y on text elements inside overflow="visible" outer SVG elements.

Attack 3: nested SVG overflow="hidden" parent / overflow="visible" child creates cross-boundary rendering confusion (SA-CSS-OVF-003)

A three-level SVG nesting creates a cross-boundary rendering anomaly: the outermost SVG has overflow="visible" (allows overflow), the middle nested SVG has overflow="hidden" (clips at its boundary), and the innermost nested SVG has overflow="visible" (allows overflow out of inner). Consent text in the innermost SVG at coordinates that overflow the middle SVG boundary is clipped by the middle SVG’s hidden overflow — even though the outermost SVG and innermost SVG both have visible overflow. The middle layer hidden boundary is the effective clipping container. Auditors scanning for overflow="visible" at any level may conclude that content is not clipped, but the middle layer hidden boundary overrides the inner visible boundary. The clipping container for any element is the nearest ancestor SVG viewport with overflow="hidden", not the element’s own overflow setting.

<!-- SA-CSS-OVF-003: nested overflow: outer=visible, middle=hidden, inner=visible -->
<!-- Consent text in inner SVG overflows inner boundary but is clipped by middle hidden -->
<svg width="440" height="200" overflow="visible" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="200" fill="white"/>

  <!-- Middle: height=30 (buttons only), overflow=hidden -- clips anything outside 30px -->
  <svg x="0" y="80" width="440" height="30" overflow="hidden">

    <!-- Inner: overflow=visible -- would allow text to flow outside inner viewport -->
    <!-- But inner is clipped by middle's hidden boundary before reaching outer visible -->
    <svg x="0" y="0" width="440" height="30" overflow="visible">
      <!-- Text at y=-25 is above inner SVG top (visible within inner) -->
      <!-- But middle SVG clips at y=0 relative to middle, so this y=-25 in inner -->
      <!-- = y=80-25=55 absolute, but clipped at middle boundary (y=80) -->
      <text x="220" y="-25" text-anchor="middle" font-size="14" fill="#111">
        Allow: shell execution, credential access
      </text>

      <rect x="0"   y="0"  width="80" height="30" fill="#22c55e" rx="4"/>
      <text x="40"  y="20" text-anchor="middle" font-size="13" fill="white">Allow</text>
    </svg>
  </svg>
</svg>

// Detection: find nearest hidden-overflow SVG ancestor for each text element
function findEffectiveClipBoundary(textEl) {
  let el = textEl.parentElement;
  while (el) {
    if (el.tagName === 'svg' || el.tagName === 'SVG') {
      const ov = el.getAttribute('overflow') || 'hidden'; // nested SVG default = hidden
      if (ov === 'hidden') return el; // this is the clipping container
    }
    el = el.parentElement;
  }
  return null; // no hidden ancestor found -- root SVG is the boundary
}

function checkCrossBoundaryClipping(svgEl) {
  const findings = [];
  for (const textEl of svgEl.querySelectorAll('text')) {
    const clip = findEffectiveClipBoundary(textEl);
    if (!clip) continue;
    const clipH = parseFloat(clip.getAttribute('height') || '0');
    // Convert text y to clip container coordinate system (simplified)
    const ty = parseFloat(textEl.getAttribute('y') || '0');
    if (ty < 0 || ty > clipH) {
      findings.push({
        severity: 'HIGH', code: 'SA-CSS-OVF-003',
        note: `Consent text clipped by nearest hidden-overflow SVG ancestor ` +
              `(height=${clipH}) despite inner/outer SVG having overflow="visible"; ` +
              `cross-boundary rendering confusion; ` +
              `effective clip boundary is the hidden ancestor, not the element's own overflow`
      });
    }
  }
  return findings;
}

HIGH — SA-CSS-OVF-003: In a nested SVG hierarchy, the effective clipping container for a text element is the nearest SVG ancestor with overflow="hidden" (which is the default for nested SVG). An inner overflow="visible" does not override a parent’s hidden boundary. Auditors that scan only for overflow="visible" miss the hidden ancestor’s clipping effect. Detection requires walking the ancestor chain to find the nearest hidden-overflow SVG container.

Attack 4: JS mousedown changes overflow from "visible" to "hidden" on inner SVG during click window (SA-CSS-OVF-004)

At rest, the inner SVG has overflow="visible" and the consent text is positioned slightly above the inner viewport top but rendered due to visible overflow — the text is legible in the consent panel. A JavaScript mousedown handler calls innerSvg.setAttribute('overflow', 'hidden'): the browser re-evaluates overflow clipping, the text outside the viewport is clipped, and the consent panel shows only buttons with no permission scope text. The mouseup handler reverts to overflow="visible". At rest the static SVG analysis sees overflow="visible" with a text element at negative y — this may be flagged as SA-CSS-OVF-002 but appears intentional with visible overflow. The transient switch to hidden during the click window is the actual consent defeat mechanism. MutationObserver watching the overflow attribute on SVG elements during interaction simulation is required to detect this attack.

<!-- SA-CSS-OVF-004: mousedown switches overflow from visible to hidden; reverts mouseup -->
<svg width="440" height="200" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="200" fill="white"/>

  <!-- Inner SVG: at rest overflow=visible -- consent text at y=-20 is rendered -->
  <svg id="consent-inner" x="0" y="80" width="440" height="50" overflow="visible">
    <!-- At rest: y=-20 + overflow=visible = text visible above inner viewport -->
    <text x="220" y="-20" text-anchor="middle" font-size="14" fill="#111">
      Allow: shell execution, credential access
    </text>
    <rect x="20"  y="10" width="80" height="30" fill="#22c55e" rx="4"/>
    <text x="60"  y="30" text-anchor="middle" font-size="13" fill="white">Allow</text>
    <rect x="120" y="10" width="80" height="30" fill="#ef4444" rx="4"/>
    <text x="160" y="30" text-anchor="middle" font-size="13" fill="white">Deny</text>
  </svg>
</svg>

<script>
(function() {
  const inner = document.getElementById('consent-inner');
  inner.addEventListener('mousedown', () =>
    inner.setAttribute('overflow', 'hidden'), true);
  inner.addEventListener('mouseup', () =>
    inner.setAttribute('overflow', 'visible'), true);
})();
</script>

// Detection: MutationObserver on overflow attribute of SVG elements
function watchOverflowInject(svgEl) {
  const findings = [];
  const observer = new MutationObserver(mutations => {
    for (const mut of mutations) {
      if (mut.attributeName !== 'overflow') continue;
      const el     = mut.target;
      const newVal = el.getAttribute('overflow') || '';
      if (newVal !== 'hidden') continue;
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-OVF-004',
        note: `overflow on <${el.tagName}> changed to "hidden" dynamically; ` +
              `old="${mut.oldValue ?? 'hidden (default)'}"; ` +
              `consent text outside viewport is now clipped; ` +
              `DOM showed overflow="visible" at rest; MutationObserver required`
      });
    }
  });
  observer.observe(svgEl, {
    subtree: true, attributes: true, attributeOldValue: true,
    attributeFilter: ['overflow']
  });
  return findings;
}

CRITICAL — SA-CSS-OVF-004: JavaScript switches an inner SVG’s overflow from "visible" to "hidden" at mousedown, clipping consent text that overflows the viewport boundary. At rest the static SVG shows overflow="visible" — no clipping attack apparent. The transient hidden state during the click window hides the permission scope text. MutationObserver watching the overflow attribute on SVG elements during interaction simulation is required.

Summary table

AttackMechanismWhat it bypassesSeverity
SA-CSS-OVF-001: inner SVG overflow=hidden clips consent text above viewport Nested SVG with overflow=hidden (default) + consent text at negative y: clipped at viewport boundary; textContent intact; BCR returns inner SVG container bounds textContent; BCR; display/visibility — viewport boundary check required Critical
SA-CSS-OVF-002: outer overflow=visible + negative y above SVG element overflow=visible on outer SVG + text at negative y renders above SVG element outside consent panel; BCR does not include overflow area SVG element BCR; text appears in DOM but outside visible panel bounds Critical
SA-CSS-OVF-003: nested overflow cross-boundary — hidden ancestor clips despite inner visible Nearest hidden-overflow SVG ancestor is effective clip boundary; inner overflow=visible does not override parent hidden boundary Naive overflow=visible check at element level; nearest-ancestor traversal required High
SA-CSS-OVF-004: mousedown switches overflow to hidden; reverts mouseup JS sets overflow=hidden during click window; consent text outside viewport clipped; reverts at mouseup; static SVG shows overflow=visible Static SVG scan (no hidden at rest); MutationObserver on overflow attribute required Critical

SkillAudit findings for this attack surface

CRITICAL SA-CSS-OVF-001: inner SVG element with overflow=hidden (default for nested SVG) and consent text at coordinates outside viewport boundary — text is clipped and not rendered; textContent and BCR intact; viewport-boundary check computing text y relative to each enclosing SVG viewport height required.
CRITICAL SA-CSS-OVF-002: outer SVG with overflow=visible and consent text at negative y-coordinates — text renders above SVG element outside visible consent panel; SVG BCR does not include overflow area; negative-y text in overflow=visible outer SVG must be flagged.
HIGH SA-CSS-OVF-003: nested SVG hierarchy with mixed overflow values — nearest hidden-overflow SVG ancestor is effective clip boundary regardless of inner/outer visible settings; naive overflow=visible scan misses hidden ancestor clipping; ancestor-chain traversal required.
CRITICAL SA-CSS-OVF-004: JS mousedown sets overflow=hidden on SVG element; reverts at mouseup; consent text clipped during click window; static scan sees overflow=visible; MutationObserver watching overflow attribute during interaction simulation required.

Related: SVG viewBox consent attacks  |  SVG preserveAspectRatio consent attacks  |  SVG transform and motion attack families

← Blog  |  Security Checklist