← Blog

MCP server SVG feConvolveMatrix edgeMode consent security

Four SA-CSS-CVM consent attack patterns using feConvolveMatrix — the SVG 2D kernel convolution primitive — to render consent text unreadable via Gaussian blur, destroy legibility using Laplacian edge detection, create black border artifacts via edgeMode="none", and convert consent text to semi-transparent smear via preserveAlpha="false", while evading CSSOM contrast checks which cannot observe convolution filter outputs.

By SkillAudit  |  24 Oct 2026

The feConvolveMatrix SVG filter primitive applies a 2D discrete convolution to each pixel of the source image using a specified kernel. The output at each pixel is: result = (sum over kernel: kernelMatrix[i][j] × source[x+i][y+j]) / divisor + bias, clamped to [0,1]. Key attributes are: order (kernel dimensions), kernelMatrix (kernel coefficients as space-separated list of order×order values), divisor (output normalisation), bias (additive constant), edgeMode (“duplicate”/“wrap”/“none”), and preserveAlpha (whether the alpha channel bypasses the kernel). Unlike feColorMatrix (which is memoryless — each pixel transformed independently), feConvolveMatrix creates spatial coupling between pixels, making its effects on consent text readability fundamentally different: the attack is on legibility through spatial frequency manipulation rather than colour-channel manipulation.

Blur attacks evade pixel-level WCAG checks: WCAG contrast (WCAG 2.1 §1.4.3) measures the luminance ratio between the text colour and its background at a single representative point. After Gaussian blur, the text stroke centre pixels may still be dark (high contrast) while the surrounding glyph pixels contain a weighted mix of text and background luminance. A WCAG check sampled at the glyph centre would pass, but human readability fails because adjacent pixels contain mixed luminance that destroys the legibility of character shapes. Blur-based attacks require spatial analysis (effective stroke contrast over the entire glyph area), not single-sample contrast measurement.

feConvolveMatrix attack anatomy: blur, edge-detect, and border effects

/* feConvolveMatrix output formula for pixel at (x, y):
   result(x,y) = (Σ_{i,j} kernelMatrix[orderY-j-1][orderX-i-1] × src(x+i-targetX, y+j-targetY)) / divisor + bias

   For a GAUSSIAN BLUR (order=5, uniform approximate):
   kernelMatrix = "1 2 3 2 1  2 4 6 4 2  3 6 9 6 3  2 4 6 4 2  1 2 3 2 1"
   divisor = 81 (sum of all kernel values)
   For a dark text pixel (L≈0) surrounded by white pixels (L≈1):
     Centre pixel contribution: 9×0 = 0
     Adjacent pixels (L=1): (2+3+2 + 2+3+2 + 2+2+1+...) × 1.0 ≈ 72
     Total / 81 ≈ 0.89 (near-white)
   The formerly dark text pixel now has L≈0.89 → WCAG vs white: 1.05/0.94=1.12:1

   For LAPLACIAN EDGE DETECTION (order=3):
   kernelMatrix = "-1 -1 -1  -1 8 -1  -1 -1 -1"
   divisor = 1, bias = 0.5
   For a uniform-dark interior pixel (all neighbours also dark, L≈0):
     8×0 - 8×0 + 0.5 = 0.5 (mid-gray)  ← no edge → mid-gray background
   For a text-edge pixel (centre dark L=0, neighbours mix dark+white):
     8×0 - sum(dark+white neighbours)×1 + 0.5 → small non-zero output
   The solid filled text becomes a faint edge outline on mid-gray.
   Result: rendered text is barely visible edge artifacts.

   For edgeMode="none" with large kernel order=15:
   Any kernel position requiring a source pixel outside the filter region
   uses value 0 (transparent/black). For text near the edge of the filter
   region, many kernel positions sample 0, pulling the output toward black.
   Black rectangular bands appear at the filter region boundary. */

SA-CSS-CVM-001: Gaussian blur kernel — text legibility failure via stroke spreading

A 9×9 uniform Gaussian-approximate kernel with divisor=81 spreads each text stroke over a 9-pixel radius. Dark text characters (stroke width 1–3px at typical 16px body text) have their dark pixels mixed with the surrounding white background. At the glyph boundary, the pixel luminance is approximately 0.5 (equal mix of stroke and background). The glyph interior retains dark pixels at the exact glyph centre, but the stroke is so narrow at body text sizes that the entire glyph becomes an anti-aliased blur. Text becomes unreadable. WCAG single-point sampling at the geometric centre of a letter may still show high contrast (the exact centre of an ‘O’ outline is white, not dark), but the perceptual effect is complete illegibility. This attack uses feConvolveMatrix instead of feGaussianBlur to avoid detection rules specifically targeting the blur primitive.

<filter id="cvm001">
  <feConvolveMatrix order="9"
    kernelMatrix="1 1 1 1 1 1 1 1 1
                  1 2 2 2 2 2 2 2 1
                  1 2 3 3 3 3 3 2 1
                  1 2 3 4 4 4 3 2 1
                  1 2 3 4 5 4 3 2 1
                  1 2 3 4 4 4 3 2 1
                  1 2 3 3 3 3 3 2 1
                  1 2 2 2 2 2 2 2 1
                  1 1 1 1 1 1 1 1 1"
    divisor="196"
    edgeMode="duplicate"/>
</filter>
<!-- Approximate 9×9 Gaussian blur via convolution kernel.
     divisor=196 normalises the kernel (sum of all values).
     Effect: dark text stroke pixels blend into surrounding white background.
     At 16px body text: character stroke ≈ 2px wide. After blur radius=4px:
     stroke luminance at edge pixels ≈ 0.7-0.9 (near-white).
     Text becomes unreadable smear on white background.
     getComputedStyle().color still returns original dark color — CSSOM blind spot.
     Scanner rules targeting feGaussianBlur do NOT catch feConvolveMatrix blur. -->

function detectConvolveBlur(filterEl) {
  for (const m of filterEl.querySelectorAll('feConvolveMatrix')) {
    const vals = (m.getAttribute('kernelMatrix') || '').trim().split(/\s+/).map(Number);
    const div = parseFloat(m.getAttribute('divisor') || '1');
    // Check if kernel is low-pass (blur): all coefficients non-negative and sum≈divisor
    const allNonNeg = vals.every(v => v >= 0);
    const kernelSum = vals.reduce((a,b)=>a+b, 0);
    const order = parseInt(m.getAttribute('order') || '3');
    if (allNonNeg && Math.abs(kernelSum/div - 1.0) < 0.01 && order >= 5) {
      return { risk: 'HIGH', type: 'convolution-blur', order };
    }
    // Check if high-pass (edge-detect/sharpen): centre positive, surround negative
    const centre = Math.floor(vals.length / 2);
    if (vals[centre] > 4 && vals.filter(v=>v<0).length > vals.length/2) {
      return { risk: 'CRITICAL', type: 'edge-detection' };
    }
  }
  return null;
}

SA-CSS-CVM-002: Laplacian edge-detection kernel — solid text to faint outline artifacts

The Laplacian operator kernel kernelMatrix="-1 -1 -1  -1 8 -1  -1 -1 -1" with divisor="1" and bias="0.5" is a high-pass filter that removes low-frequency (uniform) regions and amplifies high-frequency (edge) regions. In the consent text context: solid dark text interiors (all neighbouring pixels are also dark) produce output ≈ 0 + bias = 0.5 (uniform mid-gray). Text edge pixels (dark pixel surrounded by white neighbours) produce: 8×dark − sum(white)×1 + 0.5 ≈ 8×0 − 8×1 + 0.5 = −7.5 → 0 (clamped). The result: white background → mid-gray (0.5); dark text interior → mid-gray (0.5); text edges → near-zero (dark outline). Text is rendered as faint dark outlines on a uniform mid-gray background — legibility collapses because the character shapes are only 1-pixel-wide outlines with no filled stroke.

<filter id="cvm002">
  <feConvolveMatrix order="3"
    kernelMatrix="-1 -1 -1
                  -1  8 -1
                  -1 -1 -1"
    divisor="1"
    bias="0.5"
    edgeMode="duplicate"/>
</filter>
<!-- Laplacian edge-detection (high-pass filter):
     Output = centre×8 - sum(8 neighbours)×1 + 0.5, clamped to [0,1]

     White background pixel (all neighbours white, L=1.0):
       8×1.0 - 8×1.0 + 0.5 = 0 + 0.5 = 0.5  (mid-gray)
     Dark interior text pixel (all neighbours dark, L=0.017):
       8×0.017 - 8×0.017 + 0.5 = 0 + 0.5 = 0.5  (mid-gray)
     Text edge pixel (centre dark L=0.017, 6 of 8 neighbours white L=1.0):
       8×0.017 - (6×1.0 + 2×0.017) + 0.5 = 0.136 - 6.034 + 0.5 = -5.398 → 0  (dark)

     Result: background=mid-gray, text interior=mid-gray, text edges=dark outline.
     Consent text → 1px dark outlines on gray background. Unreadable at body text sizes.
     WCAG for outline pixels: 1.05/(0+0.05)=21:1 but only 1px wide — legibility failure.
     WCAG for background area: 1.05/(0.5+0.05)=1.91:1 (fail for any filled region). -->

SA-CSS-CVM-003: edgeMode=“none” with large kernel — black border bars at filter region boundary

With edgeMode="none", any kernel position that extends outside the source image region uses the value 0 (black/transparent). For a large kernel (order=15), pixels within 7 pixels of the filter region boundary have many kernel positions landing outside the source, pulling their output toward the 0 baseline. In a consent panel where the filter region is tight around the element bounding box (the SVG default: x=−10%, y=−10%, width=120%, height=120%), text positioned near the element boundary is within 7 pixels of the filter region edge. Those text pixels get blended with many “black” out-of-region values, producing a dark smear around the text. The practical attack: a consent dialog where text is positioned near the containing div’s top or left edge will have its first lines obscured by black smear bands.

<filter id="cvm003" x="-5%" y="-5%" width="110%" height="110%">
  <feConvolveMatrix order="15"
    kernelMatrix="1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"
    divisor="225"
    edgeMode="none"/>
</filter>
<!-- edgeMode="none": kernel positions outside source region → value=0 (black).
     For a 15×15 kernel (radius=7 pixels), pixels within 7px of the filter
     region boundary have at least some kernel positions sampling "0".
     7px margin pulls output luminance toward 0 (darker/blacker).
     At the filter region edge: output ≈ (fraction_in_region) × L_source.
     A pixel at the exact boundary has 50% kernel positions outside → output≈0.5×L.
     For dark text at boundary: already dark → output≈0.5×0 = 0 (still dark, OK).
     For white background at boundary: output≈0.5×1 = 0.5 → dark gray border bar.
     The dark gray border appears on the white background adjacent to text. -->

SA-CSS-CVM-004: JS mousedown identity kernel → Gaussian blur at click time

The initial kernel is the identity (a single 1.0 in the centre, zeros elsewhere). The consent panel renders sharply with correct contrast. On mousedown, the kernel is replaced with the 9×9 Gaussian blur. Text instantly becomes unreadable at the moment the user taps the confirmation. Static analysis sees only the identity kernel and reports no finding. Unlike feGaussianBlur (which has a simple stdDeviation attribute), feConvolveMatrix requires the full kernel matrix to be replaced, but a single setAttribute("kernelMatrix", ...) call with a precomputed Gaussian kernel string achieves the same effect atomically.

<svg style="position:absolute;width:0;height:0">
  <defs>
    <filter id="cvm004">
      <feConvolveMatrix id="cvm004K" order="3"
        kernelMatrix="0 0 0  0 1 0  0 0 0"
        divisor="1"/>
    </filter>
  </defs>
</svg>
<div style="filter:url(#cvm004)">
  Grant this MCP server persistent background access.
  <button id="allow">Allow</button>
</div>
<script>
const GAUSSIAN_9X9 =
  "1 1 1 1 1 1 1 1 1  1 2 2 2 2 2 2 2 1  1 2 3 3 3 3 3 2 1  " +
  "1 2 3 4 4 4 3 2 1  1 2 3 4 5 4 3 2 1  1 2 3 4 4 4 3 2 1  " +
  "1 2 3 3 3 3 3 2 1  1 2 2 2 2 2 2 2 1  1 1 1 1 1 1 1 1 1";
document.getElementById("allow").addEventListener("mousedown", () => {
  const k = document.getElementById("cvm004K");
  k.setAttribute("order", "9");
  k.setAttribute("kernelMatrix", GAUSSIAN_9X9);
  k.setAttribute("divisor", "196");
  // Text instantly becomes an unreadable blur at click-time.
  // getComputedStyle().color still returns original dark text color.
});
</script>

Attack summary

IDKernel typeedgeModeEffect on consent textWCAG impactSeverity
SA-CSS-CVM-001 9×9 Gaussian blur duplicate Stroke spreads, text unreadable Edge pixels L≈0.7–0.9; text illegible before WCAG fails High
SA-CSS-CVM-002 3×3 Laplacian edge-detect duplicate Solid text → 1px edge outlines on mid-gray Background: 1.91:1 (fail); outlines illegible Critical
SA-CSS-CVM-003 15×15 uniform blur none (black border) Dark gray bars at filter region boundary obscure text Layout-dependent; text near boundary obscured Medium
SA-CSS-CVM-004 Identity→9×9 blur (mousedown) duplicate Correct at load; unreadable at click — Critical

Defences

SkillAudit findings for this attack surface

HIGH SA-CSS-CVM-001: feConvolveMatrix with order=9 Gaussian-approximate all-positive kernel (divisor=sum of kernel values) on consent element — 9-pixel blur radius spreads text stroke into white background; at 16px body text (stroke≈2px), glyph edge pixels L≈0.7–0.9 after blur; human legibility fails before WCAG single-point threshold; evades feGaussianBlur-specific detection rules because primitive type is feConvolveMatrix.
CRITICAL SA-CSS-CVM-002: feConvolveMatrix Laplacian edge-detection kernel (order=3, kernelMatrix='-1 -1 -1 -1 8 -1 -1 -1 -1', divisor=1, bias=0.5) on consent element — removes filled text, converts to 1px edge outlines; background and text interior both become mid-gray (L=0.5); WCAG for background area: 1.91:1 (fail AA); outlines are 1-pixel-wide and illegible at body text sizes; high-pass kernel signature (centre positive, surround negative) detectable via kernel coefficient analysis.
MEDIUM SA-CSS-CVM-003: feConvolveMatrix with large kernel (order≥15) and edgeMode='none' combined with tight filter region — pixels near filter region boundary (within kernel-radius pixels) sample 0 (black/transparent) for out-of-region positions; dark gray smear bands appear at filter region margins; consent text positioned near element edge is obscured; severity depends on layout: text near element boundary is CRITICAL, text in element interior may be unaffected.
CRITICAL SA-CSS-CVM-004: JS mousedown event replaces identity kernel (order=3, kernelMatrix='0 0 0 0 1 0 0 0 0') with 9×9 Gaussian blur kernel; text legibility destroyed at confirmation moment; static DOM: identity — classified as benign; MutationObserver on feConvolveMatrix kernelMatrix and order attributes required; re-classify kernel type on every attribute mutation; block confirmation if kernel transitions from identity to blur or edge-detection.

Related: feColorMatrix consent security (general)  |  feColorMatrix alpha-column injection (SA-CSS-CZA)  |  feColorMatrix mixed alpha+RGB (SA-CSS-CMA)

← Blog  |  Security Checklist