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 value | Out-of-bounds pixel handling | Attack surface |
|---|---|---|
none | Returns RGBA(0,0,0,0) — zero alpha, zero color | Boundary pixels become transparent; large kernel extends erasure zone to cover consent text |
duplicate | Extends the nearest in-bounds edge pixel | Distorts consent text near boundaries; edge-pixel color smear may obscure glyphs at element edges |
wrap | Wraps to the pixel from the opposite boundary | Injects 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
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
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
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
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
| Control | How 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.