MCP server feConvolveMatrix edgeMode security

The SVG feConvolveMatrix filter primitive applies a kernel convolution to an image. For pixels within (kernelSize−1)/2 pixels of the element boundary, the kernel extends beyond the source image region. The edgeMode attribute determines how these out-of-bounds samples are handled — and three of its four behaviors create distinct consent text attack surfaces.

feConvolveMatrix edgeMode behaviors

edgeMode valueOut-of-bounds pixel handlingAttack surface
noneReturns RGBA(0,0,0,0) — zero alpha, zero colorBoundary pixels become transparent; large kernel extends erasure zone to cover consent text
duplicateExtends the nearest in-bounds edge pixelDistorts consent text near boundaries; edge-pixel color smear may obscure glyphs at element edges
wrapWraps to the pixel from the opposite boundaryInjects foreign content from the element's opposite edge into the boundary zone; privacy concern for cross-origin compositing

Finding SA-FECM-001: edgeMode=none with large kernel erases boundary zones

CriticalA feConvolveMatrix with edgeMode="none" and a large order (e.g., order="31 31") creates a boundary erasure zone of (31−1)/2 = 15 pixels around every edge of the filtered element. For a consent label element that is 28px tall (two lines of 14px text), the top and bottom 15-pixel zones together cover the entire element height — all pixels are within the boundary erasure zone and receive zero alpha. The consent text is fully transparent. The kernel values themselves (the kernelMatrix attribute) do not matter — the boundary treatment applies regardless of kernel content.
<filter id="erase-filter">
  <feConvolveMatrix
    order="31 31"
    kernelMatrix="0 0 0 ... (961 values, all zeros)"
    edgeMode="none"/>
</filter>

Detection gap: An auditor that checks only whether feConvolveMatrix is present but does not correlate edgeMode value with order and element dimensions will miss this attack. A kernel with all-zero kernelMatrix is itself an attack (zero output regardless of edgeMode), but an attacker using edgeMode=none can use any kernel values — a benign-looking identity kernel 0 0 0 0 1 0 0 0 0 with order="31 31" still erases 15px boundaries even though the 3×3 identity kernel would be safe.

Finding SA-FECM-002: wide kernel erases narrow consent label

HighA feConvolveMatrix with order="51 1" (51 columns, 1 row) and edgeMode="none" creates a horizontal boundary erasure zone of (51−1)/2 = 25 pixels on each side of the element. For a consent label that is narrower than 50px (e.g., a checkbox label in a compact form), the left and right erasure zones together exceed the element width — the entire element is within the boundary zone and all pixels receive zero alpha. Vertical consent text (writing-mode: vertical-lr) is identically vulnerable with a 1×51 kernel.
<filter id="narrow-erase">
  <!-- 51-column kernel: horizontal boundary zone = 25px each side -->
  <feConvolveMatrix
    order="51 1"
    kernelMatrix="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
    edgeMode="none"/>
</filter>

Finding SA-FECM-003: edgeMode=wrap injects opposite-edge content

MediumWith edgeMode="wrap", pixels in the boundary zone sample from the opposite edge of the source image. For a consent element that contains sensitive disclosure text on the left half and a logo or decorative image on the right half, an attacker can calibrate the kernel order so the right-edge wrap zone covers the left half of the element. The disclosure text pixels are replaced with samples from the right (decorative) edge, making the consent text unreadable while the element still passes bounding-rect and non-empty-content checks.
<filter id="wrap-inject">
  <!-- kernel order = (element-width * 2) + 1 ensures wrap zone covers full element -->
  <feConvolveMatrix
    order="201 1"
    kernelMatrix="... (201 values)"
    edgeMode="wrap"/>
</filter>

Finding SA-FECM-004: iterative feConvolveMatrix accumulates boundary erasure

HighMultiple feConvolveMatrix primitives chained in series with edgeMode="none" accumulate boundary erasure across passes. Each pass extends the zero-alpha region inward by (order−1)/2 pixels on each side. Three passes with order="5 5" (2px boundary zone each) produce a 6px cumulative erasure zone — equivalent to a single pass with order="13 13". This distributes the attack signal across multiple primitives, each with a small and "safe-looking" kernel order, defeating auditors that check individual kernel sizes against a threshold.
<filter id="iterative-erase">
  <feConvolveMatrix order="5 5" kernelMatrix="..." edgeMode="none" result="p1"/>
  <feConvolveMatrix order="5 5" kernelMatrix="..." edgeMode="none" in="p1" result="p2"/>
  <feConvolveMatrix order="5 5" kernelMatrix="..." edgeMode="none" in="p2"/>
</filter>

Detection: correlating edgeMode with order and element dimensions

function checkFeConvolveMatrixEdgeMode(filterEl, consentEl) {
  const risks = [];
  const rect = consentEl.getBoundingClientRect();
  const primitives = filterEl.querySelectorAll('feConvolveMatrix');

  let cumulativeBoundaryX = 0;
  let cumulativeBoundaryY = 0;

  for (const prim of primitives) {
    const edgeMode = prim.getAttribute('edgeMode') || 'duplicate';
    const orderAttr = prim.getAttribute('order') || '3';
    const [ox, oy] = orderAttr.includes(' ')
      ? orderAttr.split(/\s+/).map(Number)
      : [Number(orderAttr), Number(orderAttr)];

    if (edgeMode === 'none') {
      cumulativeBoundaryX += (ox - 1) / 2;
      cumulativeBoundaryY += (oy - 1) / 2;
    }

    if (edgeMode === 'wrap') {
      risks.push({ finding: 'SA-FECM-003', primitive: prim, severity: 'medium' });
    }
  }

  // Check if accumulated boundary zone covers element
  if (cumulativeBoundaryX * 2 >= rect.width || cumulativeBoundaryY * 2 >= rect.height) {
    risks.push({
      finding: cumulativeBoundaryX * 2 >= rect.width ? 'SA-FECM-002' : 'SA-FECM-001',
      severity: 'critical',
      cumulativeBoundaryX,
      cumulativeBoundaryY,
      elementWidth: rect.width,
      elementHeight: rect.height
    });
  } else if (cumulativeBoundaryX > 10 || cumulativeBoundaryY > 10) {
    risks.push({ finding: 'SA-FECM-001', severity: 'high',
      note: 'Large boundary zone may clip consent text near edges' });
  }

  return risks;
}

Remediation

ControlHow it helps
For any feConvolveMatrix on a consent element, calculate cumulative boundary zone = sum of (orderN − 1) / 2 across all chained edgeMode=none passes; flag if boundary zone ≥ 50% of element dimension in either axis Catches both single large-kernel attacks and the iterative small-kernel evasion. The 50% threshold is conservative — any boundary zone covering a full consent text line (typically 14–18px) at the top or bottom of the element is sufficient to erase a line.
Flag edgeMode="wrap" on any filter applied to consent elements regardless of kernel content; wrap behavior is rarely needed for legitimate consent form styling edgeMode=wrap has no legitimate consent-form use case. Its presence on a consent element filter is an unconditional flag regardless of the kernel's otherwise benign values.
Do not apply feConvolveMatrix to consent text elements in MCP server code. Use a parent wrapper element for any visual convolution effects and exclude the consent text element from the filter region explicitly. Defense-in-depth: avoiding feConvolveMatrix on consent text entirely eliminates all four attack vectors. Parent-level filtering with an explicit filter region that excludes the consent text subtree provides structural separation.

SkillAudit checks feConvolveMatrix edgeMode values, correlates kernel order with consent element dimensions, and detects iterative chained feConvolveMatrix accumulation patterns. Run a free audit on your MCP server GitHub URL.