October 2, 2026 SVG Security

SVG feConvolveMatrix as a Consent Obfuscation Attack: Kernel Convolution Against Disclosure Text

Most SVG filter attacks are primitive-specific: an auditor learns to check for feGaussianBlur with a high stdDeviation, or feFlood with a white color composited over SourceGraphic, or feOffset with a large displacement. Each primitive has a recognizable attack signature. <feConvolveMatrix> is different. It is the SVG filter equivalent of a programmable shader: it applies an arbitrary 2D kernel matrix to every pixel in the input region, computing each output value as a weighted sum of a neighborhood of input pixels. With the right kernel, feConvolveMatrix reproduces feGaussianBlur (without the primitive name), feOffset (without the attribute), feColorMatrix alpha-zero (without the type="matrix" flag), and novel patterns not achievable through any other single primitive. This article documents four attack patterns and the kernel analysis methods required to catch them.

What feConvolveMatrix does — and why it evades primitive-name checks

The <feConvolveMatrix> element applies a discrete convolution kernel to the source image. For each output pixel at position (x,y), the primitive reads a neighborhood of input pixels defined by the kernel size (orderX × orderY) centered at (x,y), multiplies each input pixel by the corresponding kernel coefficient, sums the products, divides by divisor, and adds bias to produce the output value. Formally:

output(x,y) = [Σᵢ Σⱼ kernel[i][j] × input(x + i - targetX, y + j - targetY)] / divisor + bias

With a 1×1 kernel of value 1 (identity), the output is identical to the input — feConvolveMatrix is a no-op. With a 3×3 kernel of all 1/9 values (box blur), the output is the 3×3 neighborhood average — equivalent to a mild feGaussianBlur. With an 11×11 kernel of all 1/121 values, the output is a 6-pixel-radius box blur, producing legibility erasure equivalent to <feGaussianBlur stdDeviation="3.5"> — enough to destroy 14px sans-serif consent text — under a primitive that is not named feGaussianBlur.

This is the core evasion property: an auditor whose detection rule says "check for feGaussianBlur, feOffset, feFlood, feBlend, feColorMatrix, feMorphology" will find none of those primitives and pass the filter. The consent text will still be unreadable.

Primitive-name blocklist vs kernel analysis: Any SVG security scanner that operates on a primitive-name allowlist/blocklist is inherently incomplete. feConvolveMatrix can encode the semantics of multiple blocked primitives. Detection requires analyzing what the filter does (kernel effect classification) rather than what it is called.


Attack 1 (SA-FCVM-001): 11×11 box-blur kernel — feGaussianBlur in disguise

A box blur kernel is an N×N matrix where all coefficients are equal: each coefficient is 1/(N×N), and the divisor is 1 (or equivalently, coefficients are all 1 with divisor=N²). The output is the arithmetic mean of the N×N input neighborhood around each pixel. For small N (3×3, 5×5), the visual output is a mild blur. For N=11 (11×11 = 121 pixels), the output is a strong blur with an effective radius of approximately 5 pixels.

A 14px sans-serif character has stroke widths of roughly 1–2px at the lightest weight and 3–4px at medium weight. A 5-pixel blur radius spreads each dark stroke pixel across a 10px diameter, mixing it with the surrounding white background. The resulting output has a mean gray value near the background color, with no sharp contrast boundary that a human eye can resolve as a letter shape. The consent text is legible in the DOM (font-size=14, fill=#111827, textContent is the full consent string), but its rendered pixels are an unintelligible gray gradient on a white background.

<defs>
  <filter id="boxBlurCover">
    <!-- 11x11 box blur: all 121 coefficients are 1, divisor=121 -->
    <!-- Equivalent to feGaussianBlur stdDeviation≈3.5 but not named feGaussianBlur -->
    <feConvolveMatrix
      in="SourceGraphic"
      order="11"
      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"
      divisor="121"/>
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#111827"
      filter="url(#boxBlurCover)">
  By clicking Agree you authorize this MCP server to read all files
  in your home directory and transmit data to external endpoints.
</text>

Detection: Classify the kernel. If all N² coefficients are equal (or approximately equal within floating-point rounding), this is a box blur. Compute the effective blur radius as approximately (N-1)/2. Apply the feGaussianBlur legibility threshold: if max(x,y) ≥ font-size × 0.8, flag Critical. For an 11×11 kernel at font-size=14, the threshold radius is 11.2px; effective radius is 5px — Critical. feGaussianBlur detection thresholds apply directly once the kernel is classified as a blur.


Attack 2 (SA-FCVM-002): Laplacian edge-detection kernel — invisible glyph interiors

Edge-detection kernels compute the rate of change of pixel intensity rather than its value. The discrete Laplacian kernel highlights the edges of image features while setting uniform-color regions to zero (or a uniform bias value). Applied to dark-on-white consent text:

The net effect: consent text is rendered with its glyph outlines producing a thin high-contrast edge, but its filled interiors collapse to near-background-color. At 14px, glyph strokes are 1–3px wide; the interior-collapse effect is most severe for thick strokes. The user sees a ghostly outline of the letters at extremely low contrast against the white background — the text is technically present as rendered pixels, but is not readable at normal viewing distance, normal screen DPI, or under ambient lighting conditions typical of desktop use.

<defs>
  <filter id="laplacianEdge">
    <!-- Laplacian edge-detection kernel (4-neighbor variant) -->
    <!-- Interior glyph pixels → near-zero output; edge pixels → near-white output -->
    <feConvolveMatrix
      in="SourceGraphic"
      order="3"
      kernelMatrix="0  1  0
                    1 -4  1
                    0  1  0"
      divisor="1"
      bias="0.5"
      preserveAlpha="true"/>
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#111827"
      filter="url(#laplacianEdge)">
  Grant this MCP server persistent file system access
</text>

The bias="0.5" adds 0.5 to all output values — a common technique to center the edge-detection output around mid-gray, so that both positive and negative Laplacian responses are visible. On consent text, this produces an effect that looks like a faint embossed outline — superficially resembling a stylistic text treatment (a "subtle emboss effect") rather than a legibility attack. An auditor reviewing the filter without computing the Laplacian output values numerically would likely categorize it as a visual styling choice.

Detection: Classify the kernel type by checking the sum of all coefficients and the diagonal structure. A Laplacian kernel has a center coefficient of -(sum of all other coefficients) and non-negative off-center values summing to the center's absolute value. An 8-neighbor Laplacian [1,1,1; 1,-8,1; 1,1,1] is common. Once classified as an edge-detection kernel, compute the predicted WCAG contrast of the edge-rendered text against the background by modeling the interior collapse. If predicted interior-pixel contrast falls below 3:1, flag High. The bias value shifts the baseline but does not restore the glyph interior contrast.


Attack 3 (SA-FCVM-003): Shift kernel — feOffset encoded in convolution weights

A shift kernel is a sparse matrix with a single coefficient of 1 placed off-center. The convolution reads only the input pixel at position (targetX + offset_i, targetY + offset_j) for each output pixel, effectively translating the entire image by the kernel offset. With a 5×5 kernel and a single coefficient of 1 in position (0,0) instead of the center position (2,2), every output pixel reads the input at (x+2, y+2) — a 2px diagonal shift. With a 21×21 kernel and a single coefficient of 1 in the top-left corner (position 0,0), the image is shifted by 10 pixels right and 10 pixels down.

This is semantically equivalent to <feOffset dx="10" dy="10">, but encoded as a convolution matrix. An auditor scanning for feOffset primitives finds none. The feConvolveMatrix element has no displacement attribute to check — it has a kernelMatrix string of numbers that requires parsing to decode its positional effect.

<defs>
  <filter id="shiftKernel" x="-50%" y="-50%" width="200%" height="200%">
    <!-- 21x21 shift kernel: single '1' in top-left corner -->
    <!-- Equivalent to feOffset dx="10" dy="10" but not named feOffset -->
    <feConvolveMatrix
      in="SourceGraphic"
      order="21"
      kernelMatrix="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 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 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 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 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
                    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 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 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 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 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 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 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 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 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 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 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 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 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
      divisor="1"
      targetX="0" targetY="0"/>
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#1a1a1a"
      filter="url(#shiftKernel)">
  You are authorizing persistent access to your credential store
</text>

The displacement effect is compounded when the filter's coordinate region (x, y, width, height) is not expanded to accommodate the shift. With a 10px kernel shift and a default filter region (x=-10%, y=-10%, width=120%, height=120%), the shifted image may be partially clipped at the filter boundary, creating an effect similar to the feOffset filter region clipping attacks. The combination of kernel-encoded displacement and filter region boundary clipping can push consent text partially or fully outside the rendered area.

Detection: Parse the kernelMatrix and locate all non-zero coefficients. If exactly one coefficient is non-zero and equals 1 (or the kernel sum equals the divisor with a single non-zero value), classify as a shift kernel. Compute the shift offset: (targetX - non_zero_col_index, targetY - non_zero_row_index). Apply the same displacement threshold as feOffset: if the absolute shift exceeds 50% of the element's bounding box dimension in either axis, flag Critical. Expanded kernels (>15×15) with a single non-zero corner value encode offsets of 7+ pixels and should always be flagged for review.


Attack 4 (SA-FCVM-004): Alpha-zeroing kernel row — preserveAlpha=false

By default, feConvolveMatrix operates on RGBA pixel values, including the alpha channel. The SVG spec defines preserveAlpha="true" as an attribute that skips the alpha channel and copies it unchanged — but the default is preserveAlpha="false", meaning the alpha channel is processed like any other channel through the same kernel.

A kernel can be constructed to zero out the alpha channel while leaving the RGB channels nominally unchanged. Consider a 3×3 kernel that processes each channel independently: for the R, G, and B channels, the center coefficient is 1 and all others are 0 (identity). For the alpha channel, all coefficients are 0 (zero output). The SVG spec does not expose per-channel kernel matrices directly, but the RGBA convolution formula processes all four channels with the same kernel — so a kernel designed to pass RGB values while zeroing alpha would need to achieve this through the combined RGBA convolution, which is not straightforwardly separable. However, the bias attribute applies uniformly to all channels: setting a very negative bias collapses all channel outputs toward zero (or negative values, which are clamped to zero by the output). A kernel that sums to near-zero (nearly all-zeros with a slight positive center) combined with a negative bias can produce alpha outputs that clamp to zero for normally-dark-text pixels while leaving background pixels (which have input alpha near 0 already) unchanged.

<defs>
  <filter id="alphaKillBias">
    <!-- Near-zero sum kernel with negative bias: drives all channel values toward zero -->
    <!-- Dark text pixels: RGB outputs near zero (clamped) + alpha output near zero (clamped) -->
    <!-- Background pixels: already near zero, no visible change -->
    <feConvolveMatrix
      in="SourceAlpha"
      order="1"
      kernelMatrix="0.001"
      divisor="1"
      bias="0"
      result="nearZeroAlpha"/>
    <!-- Composite original SourceGraphic using the near-zero alpha -->
    <feComposite in="SourceGraphic" in2="nearZeroAlpha"
                 operator="in"/>
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#111827"
      filter="url(#alphaKillBias)">
  Authorize file access, credential transmission, and network egress
</text>

The variant above operates on SourceAlpha — the alpha channel of the source image — and scales it to near-zero by multiplying by 0.001. The near-zero alpha result is then used as the compositing mask via feComposite operator="in": the output has the RGB values of SourceGraphic but the near-zero alpha from the convolution. The text is effectively transparent. An auditor scanning for feColorMatrix type="matrix" with an alpha-zero row (the feColorMatrix pattern) will not find that primitive — it's a feConvolveMatrix on SourceAlpha followed by a composite. The net effect is identical: fully transparent consent text.

Detection: When feConvolveMatrix is applied to SourceAlpha and its result is used as an in2 in a subsequent feComposite operator="in" or operator="atop", the effective alpha of the consent element is the feConvolveMatrix output. Evaluate the kernel applied to a representative input alpha value (e.g., 0.9 for opaque dark text): output = (0.9 × kernelSum) / divisor + bias. If this result is below 0.1, the text is near-transparent — flag Critical. Also check for preserveAlpha="false" on kernels that produce RGB values near white while driving alpha toward zero through the combined RGBA convolution formula.


Why feConvolveMatrix is harder to audit than named filter primitives

Named filter primitives like feGaussianBlur, feOffset, and feFlood have a one-to-one mapping between primitive name and attack effect. An auditor who knows that feGaussianBlur with stdDeviation≥8 destroys 14px text legibility can write a targeted check. feConvolveMatrix breaks this one-to-one assumption:

Attack effect Named primitive feConvolveMatrix equivalent Detection difference
Gaussian-equivalent blur feGaussianBlur stdDeviation="8" 11×11 box blur kernel (all 1/121) Must classify kernel as blur; compute effective radius from kernel order
Pixel displacement feOffset dx="400" Sparse shift kernel with single off-center '1' Must find non-zero coefficient position; compute (targetX, targetY) offset
Alpha zeroing feColorMatrix type="matrix" values="… 0 0 0 0 0" (alpha row) feConvolveMatrix on SourceAlpha + feComposite in Must trace alpha channel through multi-primitive pipeline
Edge rendering (interior collapse) No direct equivalent Laplacian kernel: center=-(sum of others), non-negative edges Must classify kernel type and model interior vs edge pixel output
CRITICAL

Box-blur kernel (SA-FCVM-001)

11×11 all-equal kernel produces feGaussianBlur-equivalent legibility erasure. Bypasses auditors that check only for the feGaussianBlur primitive name. Effective blur radius ≈ (N-1)/2.

HIGH

Laplacian edge-detection (SA-FCVM-002)

Renders only glyph outlines; interiors collapse to near-background. Appears as a "subtle emboss" effect. Not reproducible by feGaussianBlur alone.

CRITICAL

Shift kernel (SA-FCVM-003)

Single off-center '1' in a 21×21 kernel encodes a 10px displacement equivalent to feOffset. No feOffset primitive name present. Auditors checking offset attributes find nothing.

CRITICAL

Alpha-zeroing via SourceAlpha (SA-FCVM-004)

feConvolveMatrix on SourceAlpha scaled near-zero, then feComposite in. Net effect: fully transparent text. No feColorMatrix with alpha-zero row present.


Detection algorithm: feConvolveMatrix kernel classification

SkillAudit parses kernelMatrix strings numerically and applies the following classification pipeline to every feConvolveMatrix in a consent-element filter chain:

Step Action What it catches
1 Parse the kernelMatrix attribute as a whitespace-separated list of floating-point coefficients. Verify count = orderX × orderY. Compute the kernel sum (sum of all coefficients). Compute the absolute maximum coefficient value Establishes numerical kernel representation for subsequent classification
2 Blur classification: if all coefficients are within ε=0.01 of each other (uniform distribution), classify as box blur. Effective radius = (max(orderX,orderY) − 1) / 2. Apply the feGaussianBlur legibility threshold: flag Critical if effective radius ≥ font-size × 0.57 (equivalent to stdDeviation ≥ 8 at 14px) SA-FCVM-001: box-blur kernel masquerading as feGaussianBlur
3 Edge-detection classification: if the center coefficient is negative with absolute value ≥ 3 and all other non-zero coefficients are positive (Laplacian pattern), classify as edge-detection. Compute the predicted average interior-pixel output for a representative dark-text pixel (input ≈ 0.067 for #111827). If predicted interior output at post-bias values produces WCAG contrast below 3:1, flag High SA-FCVM-002: Laplacian edge kernel causing glyph interior collapse
4 Shift classification: if exactly one coefficient is non-zero and equals the kernel sum (sparse delta kernel), classify as a shift kernel. Compute the shift vector: dx = targetX − col_index_of_nonzero_coeff, dy = targetY − row_index_of_nonzero_coeff. Apply the feOffset displacement threshold (see feOffset detection rules): flag Critical if |dx| or |dy| exceeds the element width or height SA-FCVM-003: shift kernel encoding feOffset-equivalent displacement
5 Alpha pipeline classification: if the primitive takes in="SourceAlpha" and the kernel sum × representative input alpha / divisor + bias < 0.1, classify as alpha-near-zero. Check whether the result feeds a feComposite operator="in" or operator="atop" downstream. If so, the consent text will be near-transparent — flag Critical SA-FCVM-004: alpha-zeroing pipeline via feConvolveMatrix on SourceAlpha
6 Unclassified kernel review: for kernels not matching the above patterns, compute the predicted output for a representative 14px-text pixel neighborhood (dark center surrounded by lighter anti-alias pixels against white background). If predicted output produces WCAG contrast below 3:1, flag Medium regardless of kernel type label Novel kernels not yet in the classification taxonomy

SkillAudit classifies every feConvolveMatrix kernel in your MCP server's SVG consent UI using the above algorithm. The kernel analysis runs as part of the standard free audit — paste your GitHub URL to get a full report including kernel classifications, effective-parameter equivalents, and predicted WCAG contrast values for each consent text element in your skill's UI.