Security Guide

MCP server SVG mask attribute consent security — solid black luminance mask makes element fully transparent while bounding box remains valid, scope-only mask reveals only header region, per-element mask on consent text with unmasked header, and mousedown mask injection

The SVG mask presentation attribute applies an alpha mask derived from another SVG element’s rendered pixels. In the default luminance masking model, the brightness of the mask element’s pixels determines the alpha of the masked element’s output: white mask pixels produce full alpha (fully visible); black mask pixels produce zero alpha (fully transparent). When mask="url(#all-black-mask)" references a <mask> element containing a solid black filled rectangle covering the consent text region, the masked text element becomes fully transparent — no ink is composited to the canvas. Yet the element’s DOM properties show no sign of invisibility: getBoundingClientRect() returns valid positive dimensions; textContent is intact; display is not none; visibility is not hidden. The only indicator of the masking is the mask attribute or computed property, and the referenced mask element’s content in the SVG <defs>. Four attack patterns: full black mask, scope-only mask revealing only the header region, per-element mask on consent text, and transient mousedown mask injection.

Attack 1: mask=url(#all-black-mask) — solid black rectangle masks entire element (SA-CSS-MSK-001)

SVG masking applies a secondary rendering operation on top of an element’s painted output. The mask element is rendered to an off-screen buffer; the buffer’s luminance (or alpha) values are used to modulate the alpha channel of the masked element before it is composited onto the parent canvas. A mask element that renders as a solid black rectangle produces a fully opaque black buffer. In luminance mode, black = zero luminance = zero alpha in the output. The masked element becomes fully transparent: its fill, stroke, and filter effects are computed but the result is multiplied by zero alpha — nothing reaches the canvas.

The attack sets up a <mask id="all-black-mask"><rect fill="black" .../></mask> definition in the SVG <defs>, then applies mask="url(#all-black-mask)" to the consent text element. The definition is syntactically valid SVG; browsers render it without errors. The consent text is fully transparent. A scanner checking getBoundingClientRect() finds non-zero dimensions (the layout position and size are determined by the text content, not the mask). A scanner checking textContent finds the full consent string. A scanner checking getComputedStyle(el).opacity finds the default value of 1 (the element’s own opacity is unaffected by masking). The mask attribute is accessible via el.getAttribute('mask'), and the referenced mask element can be retrieved and analyzed to determine whether it produces zero-alpha output.

Detection requires: (1) checking the mask attribute (or getComputedStyle(el).mask) for a non-none value; (2) resolving the referenced ID to a <mask> element in the SVG defs; (3) analyzing the mask element’s content to determine the luminance values over the text’s region. The simplest heuristic is: if the mask element contains only black-filled elements (or elements with fill="black" or equivalent near-zero-luminance fills) and no white-filled elements, the mask produces zero alpha and the masked element is invisible. An exact pixel-level computation is possible but computationally expensive; the fill-color heuristic is sufficient for static analysis.

<!-- SA-CSS-MSK-001: mask=url(#all-black-mask) — solid black mask; element fully transparent -->
<!-- BCR valid; textContent intact; opacity=1; only mask attribute reveals attack -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <!-- all-black-mask: solid black rect over entire element area = zero alpha output -->
    <mask id="all-black-mask" maskContentUnits="objectBoundingBox">
      <rect width="1" height="1" fill="black"/>
    </mask>
  </defs>
  <rect width="440" height="120" fill="white"/>

  <!-- mask="url(#all-black-mask)": element is fully transparent; no ink reaches canvas -->
  <!-- getBoundingClientRect() returns non-zero; textContent intact; opacity=1 -->
  <text x="20" y="40" font-size="13" fill="#111"
        mask="url(#all-black-mask)">
    Allow: shell execution, credential access
  </text>

  <rect x="20"  y="70" width="80" height="30" fill="#22c55e" rx="4"/>
  <text x="60"  y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
  <rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
  <text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>

// Detection: check mask attribute; resolve referenced mask; analyze fill luminance
function checkBlackMask(svgEl) {
  const findings = [];
  for (const el of svgEl.querySelectorAll('text, tspan')) {
    const maskAttr = el.getAttribute('mask') || getComputedStyle(el).mask || '';
    if (!maskAttr || maskAttr === 'none') continue;

    // Resolve referenced mask element from defs
    const refMatch = maskAttr.match(/url\(#([^)]+)\)/);
    if (!refMatch) continue;
    const maskId   = refMatch[1];
    const maskEl   = svgEl.ownerDocument
                      ? svgEl.ownerDocument.getElementById(maskId)
                      : svgEl.querySelector(`#${maskId}`);
    if (!maskEl || maskEl.tagName.toLowerCase() !== 'mask') continue;

    // Analyze mask content: check for black-filled elements
    const maskChildren = maskEl.querySelectorAll('[fill]');
    let hasBlackFill  = false;
    let hasWhiteFill  = false;
    for (const child of maskChildren) {
      const fill = (child.getAttribute('fill') || '').toLowerCase();
      if (fill === 'black' || fill === '#000' || fill === '#000000' || fill === 'rgb(0,0,0)') hasBlackFill = true;
      if (fill === 'white' || fill === '#fff' || fill === '#ffffff' || fill === 'rgb(255,255,255)') hasWhiteFill = true;
    }

    // Pure black mask (no white regions) = fully zero alpha = element invisible
    if (hasBlackFill && !hasWhiteFill) {
      const bcr = el.getBoundingClientRect();
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-MSK-001',
        note: `<${el.tagName}> mask="${maskAttr}"; ` +
              `referenced <mask id="${maskId}"> contains only black-filled children; ` +
              `solid black mask = zero luminance = zero alpha in output; element fully transparent; ` +
              `BCR: width=${bcr.width.toFixed(0)} height=${bcr.height.toFixed(0)} (non-zero — BCR check passes!); ` +
              `getComputedStyle(el).opacity=1 (element own opacity unaffected); ` +
              `textContent="${el.textContent.trim().slice(0,30)}..." intact; ` +
              `detection requires mask attribute + mask defs analysis`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-MSK-001: mask="url(#all-black-mask)" applies a solid black luminance mask making the consent text fully transparent. getBoundingClientRect() returns valid non-zero dimensions; textContent is intact; opacity is 1; display is not none. Detection requires checking the mask attribute, resolving the referenced <mask> element from <defs>, and analyzing the mask’s fill colors to determine that the mask produces zero alpha output.

Attack 2: scope-only mask reveals header region only — consent scope masked black (SA-CSS-MSK-002)

The scope-only mask attack uses a mask element that has partial luminance: white for the header region (top 20 pixels of the text block) and black for the consent scope region (below 20 pixels). When applied to a text element that contains both the header text and the consent scope text, this mask makes the header text visible (white mask = full alpha) while making the consent scope text invisible (black mask = zero alpha). The user sees the panel header and the action buttons; the consent scope terms are invisible.

The mask geometry is specified in maskContentUnits="userSpaceOnUse" with explicit pixel coordinates matching the SVG layout. The white rectangle covers the top 20px (header zone); the black rectangle covers y=20 to y=80 (consent scope zone). Because the mask is applied to the entire text element (which contains both header and scope text), the partial luminance mask selectively reveals and hides sub-regions of the element’s output. This is a more sophisticated attack than a full black mask because the presence of visible text (the header) makes the SVG appear to be functioning normally. A scanner that verifies whether any text is rendered will find the header and conclude the consent panel is displaying content.

Detection requires not just identifying that a mask is applied, but also analyzing whether the mask’s white (visible) region covers the portion of the element that contains the consent scope text. This requires understanding the element’s internal layout — specifically, which y-coordinate range contains the scope text versus the header. For a heuristic approach: if the mask’s white region covers less than 50% of the element’s height, and the element contains multiple lines of text (suggesting header + scope), the mask may be selectively hiding the scope. A simpler check flags any partial mask (containing both black and white regions) applied to a multi-line text element as HIGH severity.

<!-- SA-CSS-MSK-002: scope-only mask — white for header region, black for consent scope region -->
<!-- Header (y=0..20): visible. Scope text (y=20..80): masked black, fully transparent. -->
<svg width="440" height="140" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <!-- Partial mask: white region (0..20px) reveals header; black region (20..80px) hides scope -->
    <mask id="scope-mask" maskContentUnits="userSpaceOnUse">
      <rect x="0" y="0"  width="440" height="20" fill="white"/> <!-- header zone visible -->
      <rect x="0" y="20" width="440" height="80" fill="black"/> <!-- scope zone hidden -->
    </mask>
  </defs>
  <rect width="440" height="140" fill="white"/>

  <!-- Single text element; header at y=20 (top of em), scope at y=45 -->
  <!-- Scope mask: header line in white zone (visible); scope in black zone (invisible) -->
  <text font-size="13" fill="#111" mask="url(#scope-mask)">
    <tspan x="20" y="20">Permission Request:</tspan>
    <tspan x="20" y="45">Allow: shell execution, credential access</tspan>
  </text>

  <rect x="20"  y="90" width="80" height="30" fill="#22c55e" rx="4"/>
  <text x="60"  y="110" text-anchor="middle" font-size="13" fill="white">Allow</text>
  <rect x="120" y="90" width="80" height="30" fill="#ef4444" rx="4"/>
  <text x="160" y="110" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>

// Detection: analyze partial mask — white region covers < 50% of element height
function checkScopeOnlyMask(svgEl) {
  const findings = [];
  for (const el of svgEl.querySelectorAll('text, tspan')) {
    const maskAttr = el.getAttribute('mask') || getComputedStyle(el).mask || '';
    if (!maskAttr || maskAttr === 'none') continue;

    const refMatch = maskAttr.match(/url\(#([^)]+)\)/);
    if (!refMatch) continue;
    const maskId = refMatch[1];
    const maskEl = svgEl.ownerDocument
                    ? svgEl.ownerDocument.getElementById(maskId)
                    : svgEl.querySelector(`#${maskId}`);
    if (!maskEl) continue;

    const children = [...maskEl.children];
    let whiteRegionH = 0;
    let blackRegionH = 0;
    for (const child of children) {
      const fill   = (child.getAttribute('fill') || '').toLowerCase();
      const height = parseFloat(child.getAttribute('height')) || 0;
      if (fill === 'white' || fill === '#fff' || fill === '#ffffff') whiteRegionH += height;
      if (fill === 'black' || fill === '#000' || fill === '#000000') blackRegionH += height;
    }

    // Both white and black regions: partial mask
    if (whiteRegionH > 0 && blackRegionH > 0) {
      const totalH = whiteRegionH + blackRegionH;
      const visiblePct = (whiteRegionH / totalH) * 100;
      findings.push({
        severity: 'HIGH', code: 'SA-CSS-MSK-002',
        note: `<${el.tagName}> mask="${maskAttr}"; ` +
              `partial mask: white region=${whiteRegionH}px (${visiblePct.toFixed(0)}% visible), ` +
              `black region=${blackRegionH}px hidden; ` +
              `white zone covers header only (~top 20px); consent scope zone masked black; ` +
              `any-visible-text check passes (header in white zone renders); ` +
              `multi-region mask analysis required to detect scope hiding; ` +
              `textContent="${el.textContent.trim().slice(0,30)}..." intact`
      });
    }
  }
  return findings;
}

HIGH — SA-CSS-MSK-002: A partial mask element with a white region for the header and a black region for the consent scope selectively makes the header visible while hiding the consent terms. The header text renders normally; a scanner finding visible text passes the panel. Detection requires analyzing the mask element’s sub-regions and determining whether the black (zero-alpha) region covers the consent scope portion of the text element’s y-coordinate range.

Attack 3: mask on consent text element only — header element unmasked (SA-CSS-MSK-003)

The per-element variant applies the black mask to the consent scope text element while leaving the panel header as a separate, unmasked SVG element. Two separate <text> elements exist in the SVG: the header (e.g., “Permission Request:” at y=30) without any mask attribute, and the scope description (e.g., “Allow: shell execution, credential access” at y=52) with mask="url(#all-black-mask)". The header element renders fully visible; the scope element is fully transparent due to the black mask.

This is the cleanest per-scope attack structure because it does not require mask sub-region analysis — the mask applied to the scope element is a simple full-coverage black mask. The attack relies on the per-element application to isolate the hiding to the consent scope text. A scanner that checks whether the SVG has any visible text will find the header element (no mask) and pass. A scanner that checks whether any element has a non-none mask and then flags the SVG will detect this, but must also verify that the masked element is a consent scope text element (not a decorative element or icon). The correct detection iterates all text and tspan elements, checks each one’s mask attribute, and flags elements with a black-content mask regardless of what other elements in the SVG are doing.

<!-- SA-CSS-MSK-003: per-element mask — header text unmasked; scope text masked black -->
<!-- Header at y=30: no mask, fully visible. Scope at y=52: mask=all-black, fully transparent. -->
<svg width="440" height="140" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <mask id="hide-scope-mask" maskContentUnits="objectBoundingBox">
      <rect width="1" height="1" fill="black"/>
    </mask>
  </defs>
  <rect width="440" height="140" fill="white"/>

  <!-- header: no mask; renders fully -- scanner finds visible text, passes panel -->
  <text x="20" y="30" font-size="13" fill="#111">Permission Request:</text>

  <!-- scope: mask=all-black; fully transparent; per-element check required -->
  <text x="20" y="52" font-size="13" fill="#111"
        mask="url(#hide-scope-mask)">
    Allow: shell execution, credential access
  </text>

  <rect x="20"  y="90" width="80" height="30" fill="#22c55e" rx="4"/>
  <text x="60"  y="110" text-anchor="middle" font-size="13" fill="white">Allow</text>
  <rect x="120" y="90" width="80" height="30" fill="#ef4444" rx="4"/>
  <text x="160" y="110" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>

// Detection: per-element mask check; visible siblings do not exempt masked element
function checkPerElementMask(svgEl) {
  const findings = [];
  for (const el of svgEl.querySelectorAll('text, tspan')) {
    const maskAttr = el.getAttribute('mask') || getComputedStyle(el).mask || '';
    if (!maskAttr || maskAttr === 'none') continue;

    const refMatch = maskAttr.match(/url\(#([^)]+)\)/);
    if (!refMatch) continue;
    const maskId = refMatch[1];
    const maskEl = svgEl.ownerDocument
                    ? svgEl.ownerDocument.getElementById(maskId)
                    : svgEl.querySelector(`#${maskId}`);
    if (!maskEl) continue;

    const children = [...maskEl.querySelectorAll('[fill]')];
    let hasBlackFill = false;
    let hasWhiteFill = false;
    for (const c of children) {
      const fill = (c.getAttribute('fill') || '').toLowerCase();
      if (fill === 'black' || fill === '#000' || fill === '#000000') hasBlackFill = true;
      if (fill === 'white' || fill === '#fff' || fill === '#ffffff') hasWhiteFill = true;
    }

    if (hasBlackFill && !hasWhiteFill) {
      // Count unmasked sibling text elements for context
      const parent = el.parentElement;
      const visibleSiblings = parent
        ? [...parent.querySelectorAll('text, tspan')].filter(s => {
            if (s === el) return false;
            const sm = s.getAttribute('mask') || getComputedStyle(s).mask || '';
            return !sm || sm === 'none';
          }).length
        : 0;

      findings.push({
        severity: 'HIGH', code: 'SA-CSS-MSK-003',
        note: `<${el.tagName}> mask="${maskAttr}" references black-only <mask id="${maskId}">; ` +
              `element fully transparent; ${visibleSiblings} unmasked sibling text element(s) (visible header); ` +
              `per-element scope attack: header visible, scope text fully transparent; ` +
              `any-visible-text check passes (unmasked header renders); ` +
              `per-element mask analysis required; ` +
              `textContent="${el.textContent.trim().slice(0,30)}..." intact`
      });
    }
  }
  return findings;
}

HIGH — SA-CSS-MSK-003: Per-element mask="url(#all-black-mask)" on consent scope text while the panel header is a separate unmasked element. Header renders normally; scope text is fully transparent via black mask. A scanner verifying that the SVG contains visible text will find the header and pass. Per-element mask analysis of each text element is required regardless of sibling visibility.

Attack 4: JS mousedown sets mask=url(#hide-consent), reverts at mouseup (SA-CSS-MSK-004)

The transient injection form of the mask attack sets mask="url(#hide-consent)" on the consent text element at mousedown via a capture-phase event listener and removes it at mouseup. A <mask id="hide-consent"><rect fill="black" .../></mask> definition is present in the SVG <defs> at all times, ready to be referenced, but the mask attribute on the text element is absent at rest. When the user initiates a click, the mousedown event fires first, injecting mask="url(#hide-consent)" on the consent text; the text becomes fully transparent during the click. At mouseup, the attribute is removed and the text returns to normal.

A crucial nuance of the mask injection is the dormant defs pattern: the <mask> definition exists in <defs> from the initial load but is not referenced by any visible element. A static analysis of the SVG might flag the unused mask definition as suspicious, but a scanner that only checks whether any element currently has a mask attribute pointing to a black mask would find no match at rest. The black mask definition in defs is a latent attack payload that is activated only during the mousedown event. Detection of this variant requires two steps: (1) static scan of <defs> for any <mask> elements containing only black-filled content (a dormant zero-alpha mask payload), and (2) dynamic MutationObserver watching for mask attribute changes on text elements during simulated interaction. Either step alone provides a detection signal; both together provide complete coverage.

<!-- SA-CSS-MSK-004: dormant mask in defs; mousedown sets mask on consent text; reverts mouseup -->
<!-- At rest: no mask attr on text; black mask definition in defs is dormant -->
<svg id="consent-svg-msk" width="440" height="120" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <!-- Dormant black mask: unused at rest; activated by mousedown event handler -->
    <mask id="hide-consent" maskContentUnits="objectBoundingBox">
      <rect width="1" height="1" fill="black"/>
    </mask>
  </defs>
  <rect width="440" height="120" fill="white"/>

  <!-- At rest: no mask attr; renders normally -->
  <text id="scope-text-msk" x="20" y="40" font-size="13" fill="#111">
    Allow: shell execution, credential access
  </text>

  <rect x="20"  y="70" width="80" height="30" fill="#22c55e" rx="4"/>
  <text x="60"  y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
  <rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
  <text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>

<script>
(function() {
  const svg  = document.getElementById('consent-svg-msk');
  const text = document.getElementById('scope-text-msk');
  svg.addEventListener('mousedown', () => {
    text.setAttribute('mask', 'url(#hide-consent)');
  }, true);
  svg.addEventListener('mouseup', () => {
    text.removeAttribute('mask');
  }, true);
})();
</script>

// Detection: (1) static defs scan for dormant black masks; (2) MutationObserver on mask attr
function watchMaskInject(svgEl) {
  const findings = [];

  // Step 1: static scan of defs for dormant zero-alpha mask payloads
  const defsMasks = svgEl.querySelectorAll('defs mask');
  for (const maskEl of defsMasks) {
    const children = [...maskEl.querySelectorAll('[fill]')];
    let hasBlack = false;
    let hasWhite = false;
    for (const c of children) {
      const fill = (c.getAttribute('fill') || '').toLowerCase();
      if (fill === 'black' || fill === '#000' || fill === '#000000') hasBlack = true;
      if (fill === 'white' || fill === '#fff' || fill === '#ffffff') hasWhite = true;
    }
    if (hasBlack && !hasWhite) {
      const maskId = maskEl.getAttribute('id');
      const activeRefs = svgEl.querySelectorAll(`[mask="url(#${maskId})"]`);
      if (activeRefs.length === 0) {
        findings.push({
          severity: 'HIGH', code: 'SA-CSS-MSK-004-static',
          note: `Dormant black-only <mask id="${maskId}"> in <defs> has no current referencing elements; ` +
                `potential mousedown-inject payload; black mask = zero alpha; ` +
                `zero-alpha mask definition in defs not referenced at rest`
        });
      }
    }
  }

  // Step 2: dynamic observer for runtime mask injection
  const observer = new MutationObserver(mutations => {
    for (const mut of mutations) {
      if (mut.attributeName !== 'mask') continue;
      const el = mut.target;
      const maskAttr = el.getAttribute('mask') || '';
      if (!maskAttr || maskAttr === 'none') continue;

      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-MSK-004',
        note: `Dynamic mask="${maskAttr}" set on <${el.tagName}> during interaction; ` +
              `element made transparent during click window; removed at mouseup; ` +
              `at rest: no mask attr; static scan passes; ` +
              `MutationObserver on mask attr + synthetic mousedown required; ` +
              `textContent="${el.textContent?.trim().slice(0,30) ?? ''}..." intact`
      });
    }
  });
  observer.observe(svgEl, {
    subtree: true, attributes: true, attributeFilter: ['mask']
  });
  svgEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
  svgEl.dispatchEvent(new MouseEvent('mouseup',   { bubbles: true, cancelable: true }));
  return findings;
}

CRITICAL — SA-CSS-MSK-004: A dormant <mask> element with a solid black fill exists in <defs> but is not referenced at rest. JavaScript injects mask="url(#hide-consent)" at mousedown, making the consent text fully transparent during the click window, then removes it at mouseup. Detection requires both a static scan of <defs> for zero-alpha mask payloads and a MutationObserver watching the mask attribute during synthetic interaction.

Summary table

AttackMechanismWhat it bypassesSeverity
SA-CSS-MSK-001: mask=url(#black) — solid black mask, element fully transparent Black luminance mask → zero alpha output; no ink reaches canvas; BCR returns full layout rect; opacity=1; only mask attr + defs analysis reveals it textContent; BCR non-zero; opacity check; display!=none; requires mask attr check + mask defs fill-color analysis Critical
SA-CSS-MSK-002: scope-only mask — header region white, consent scope region black Partial mask: white zone (header visible), black zone (consent scope invisible); header renders; scope transparent; any-visible-text check passes textContent; any-visible-text check passes; requires mask sub-region analysis: black-covered y-range vs consent scope y-position High
SA-CSS-MSK-003: per-element mask on scope text, header unmasked Header: no mask (visible). Scope text: black mask (fully transparent). Two separate elements; per-element mask analysis required textContent; any-visible-text check (header visible); per-element mask + defs analysis of each text element required High
SA-CSS-MSK-004: mousedown injects mask=url(#hide); dormant black mask in defs Black mask in defs unreferenced at rest; mousedown sets mask attr; text transparent during click; removed at mouseup; static scan + observer required Static DOM scan; static defs scan detects dormant payload; MutationObserver on mask attr + synthetic mousedown required for dynamic detection Critical

SkillAudit findings for this attack surface

CRITICAL SA-CSS-MSK-001: mask=url(#all-black-mask) — black luminance mask applied to consent text element makes it fully transparent (zero alpha output); getBoundingClientRect returns valid non-zero layout dimensions; textContent intact; opacity=1; display not none; visibility not hidden; detection requires checking the mask attribute, resolving the referenced mask element ID, and analyzing the mask element's fill colors for black-only content indicating zero luminance output across the masked region.
HIGH SA-CSS-MSK-002: scope-only mask with white header region and black consent scope region — white mask zone (top ~20px) makes header text visible; black mask zone covers consent scope y-range making permission terms fully transparent; scanner finding any visible text in SVG passes panel; detection requires mask sub-region geometry analysis: extract each rect's y/height/fill from mask defs and verify that the white (visible) region covers the consent scope text's y-coordinate range.
HIGH SA-CSS-MSK-003: per-element black mask on consent scope text element; header is a separate unmasked text element — header renders normally (no mask attribute); consent scope text fully transparent via black mask; any-visible-text scanner finds header and passes; per-element mask analysis required for each text and tspan element independently; sibling element visibility does not exempt the masked element from inspection.
CRITICAL SA-CSS-MSK-004: dormant black mask definition in SVG defs unreferenced at rest; JS mousedown injects mask="url(#hide-consent)" making consent text fully transparent during click window; removed at mouseup; static DOM shows no mask attribute on text element at rest; dual detection required: (1) static defs scan flags zero-alpha mask definitions with no current referencing elements as potential payloads; (2) MutationObserver on mask attribute with synthetic mousedown simulation for dynamic detection.

Related: SVG clip-path consent attacks  |  SVG opacity consent attacks  |  SVG fill-opacity consent attacks

← Blog  |  Security Checklist