Security Guide

MCP server SVG feConvolveMatrix order consent security — order mismatch undefined, 1×1 kernel erasure, wide-order edge-zone coverage, and JS injection

The order attribute of <feConvolveMatrix> specifies the number of kernel values in the X and Y directions (e.g., order="3" means a 3×3 kernel with 9 values; order="3 5" means a 3-wide, 5-tall kernel with 15 values). The SVG specification requires that the kernelMatrix contains exactly orderX × orderY values; if it contains fewer, the behavior is implementation-defined. This combination of a declared order larger than the actual kernel values, together with other order extremes (order="1 1" for a single-pixel kernel, asymmetric large order for narrow elements), creates four consent-text erasure attack vectors that exploit the interaction between kernel size, boundary pixel handling, and browser-specific behavior.

Attack 1: Large declared order with mismatched kernelMatrix — undefined browser behavior (SA-CSS-CMO-001)

The SVG 1.1 spec requires exactly orderX × orderY values in kernelMatrix. For order="100", this is 10,000 values. If only 9 values are supplied (e.g., a standard 3×3 kernel), the spec does not define what browsers should do with the 9,991 missing values. Common browser behaviors include: treating missing values as zero (which makes the kernel a nearly-zero kernel with only 9 non-zero entries out of 10,000, producing output ≈ bias); treating the remaining matrix as undefined NaN (which may produce black or transparent output); or silently clamping the order back to match the actual kernel length (treating it as a 3×3 kernel). The attacker exploits this per-browser behavior difference: a scanner testing in Chrome may see the clamped behavior while the target MCP client renders using the zero-fill behavior, producing dark-out.

<!-- SA-CSS-CMO-001: order=100 with only 9 kernelMatrix values (expects 10000)
     SVG spec: undefined behavior for mismatch
     Chrome/Blink: typically clamps order to match kernel length (treats as 3×3)
     Firefox/Gecko: may zero-fill missing values → sparse kernel → near-zero output
     Safari/WebKit: implementation-specific
     Attacker can test browser-specific output to find dark-out behavior -->
<filter id="cmo-mismatch">
  <feConvolveMatrix
    order="100"
    kernelMatrix="0 0 0  0 1 0  0 0 0"
    divisor="1"
    bias="0"/>
  <!-- Declared 10000 kernel values; only 9 supplied
       If browser zero-fills: net kernel = [0]*4950 + [0,0,0,0,1,0,0,0,0] + [0]*4950
       sum = 1; divisor = 1; each output ≈ src_center_pixel / 10000 ≈ 0 (dark-out in Firefox)
       If browser clamps: treated as 3×3 identity → output = original (no attack) -->
</filter>

<div id="consent" style="filter:url(#cmo-mismatch)">
  <p>Requesting: API credentials, file system access.</p>
  <button id="allow">Allow</button>
</div>

// Detection: flag order×order != kernelMatrix.length
function auditOrderMismatch(feConvolveEl) {
  const orderAttr = feConvolveEl.getAttribute('order') ?? '3';
  const parts = orderAttr.trim().split(/\s+/).map(Number);
  const orderX = parts[0];
  const orderY = parts.length > 1 ? parts[1] : parts[0];
  const expected = orderX * orderY;
  const km = (feConvolveEl.getAttribute('kernelMatrix') ?? '').trim().split(/\s+/).filter(v => v !== '');
  if (km.length !== expected) {
    return { sev: 'HIGH', note: `feConvolveMatrix order=${orderX}×${orderY} expects ${expected} kernelMatrix values; found ${km.length} — SVG spec behavior undefined; browser-specific output (dark-out in some engines)` };
  }
  if (orderX > 50 || orderY > 50) {
    return { sev: 'MEDIUM', note: `feConvolveMatrix order=${orderX}×${orderY}: very large kernel (${expected} values); performance attack risk plus edge-zone erasure spans large fractions of small consent elements` };
  }
  return null;
}

HIGH — SA-CSS-CMO-001: Declaring a large order value while supplying fewer kernelMatrix values than required is undefined per SVG spec. Some browsers zero-fill the missing values, producing a near-zero output (dark-out). Others clamp the order. Attackers exploit the cross-browser inconsistency to evade scanners running in a different engine. Detection: always verify that the number of kernelMatrix values exactly equals orderX × orderY.

Attack 2: order="1 1" + zero single-value kernel — full element erasure (SA-CSS-CMO-002)

A 1×1 kernel is the minimum valid kernel: order="1" (or equivalently order="1 1") with a single-value kernelMatrix. With kernelMatrix="0" and divisor="1" and bias="0": every output pixel has value 0 × src / 1 + 0 = 0. This is equivalent to the zero-kernel attack documented in the preserveAlpha page, but expressed as the minimal possible valid feConvolveMatrix specification. It passes schema validation (the kernel length of 1 matches order=1×1=1), it is valid SVG, and it causes complete erasure of all channel values.

The minimal form kernelMatrix="0" order="1" is more concise and less visually suspicious than a full 3×3 zero matrix. An automated scanner that checks kernelMatrix for "too many zeros" may not flag a single-value kernel. The short form can also be inlined inline in a style block or delivered as a dynamically-injected filter definition without triggering pattern-matching rules designed to detect longer zero-kernel strings.

<!-- SA-CSS-CMO-002: order=1 (1×1 kernel) + kernelMatrix=0 + bias=0
     Every output pixel = 0 * src_pixel / 1 + 0 = 0
     All channels zeroed: rgba(0,0,0,0) — fully transparent
     Minimal, valid SVG that passes most kernelMatrix-length checks -->
<filter id="cmo-min">
  <feConvolveMatrix
    order="1"
    kernelMatrix="0"
    divisor="1"
    bias="0"/>
</filter>

<div id="consent" style="filter:url(#cmo-min)">
  <p>Grant: shell:exec, env:read, fs:write.</p>
  <button id="allow">Allow</button>
</div>

// Detection: flag order=1 with zero kernel
function auditMinimalZeroKernel(feConvolveEl) {
  const orderAttr = feConvolveEl.getAttribute('order') ?? '3';
  const parts = orderAttr.trim().split(/\s+/).map(Number);
  const orderX = parts[0];
  const orderY = parts.length > 1 ? parts[1] : parts[0];
  if (orderX === 1 && orderY === 1) {
    const km = (feConvolveEl.getAttribute('kernelMatrix') ?? '').trim();
    const val = parseFloat(km);
    const bias = parseFloat(feConvolveEl.getAttribute('bias') ?? '0');
    if (val === 0 && bias === 0) {
      return { sev: 'CRITICAL', note: 'feConvolveMatrix order=1×1 with kernelMatrix=0 and bias=0: all RGBA channels zeroed; consent element fully transparent; minimal valid SVG spec kernel' };
    }
    if (Math.abs(val) < 0.01) {
      return { sev: 'HIGH', note: `feConvolveMatrix order=1×1 with near-zero kernel value ${val}: output ≈ bias (${bias}); flag combined with bias near 0 or 1` };
    }
  }
  return null;
}

CRITICAL — SA-CSS-CMO-002: order="1" with kernelMatrix="0" and bias="0" is the minimal valid SVG zero-kernel: it zeroes all RGBA channels making the consent element fully transparent. It passes schema validation and evades scanners that check kernel length. Detection: check all single-value kernels (order=1) for zero or near-zero values combined with bias=0.

Attack 3: Asymmetric large orderX + edgeMode="none" — wide edge-zone erasure (SA-CSS-CMO-003)

For a pixel at position x within an element of width W, the kernel extends from x - floor(orderX/2) to x + ceil(orderX/2 - 1). For edgeMode="none" (zero-padding), pixels outside the element boundary are treated as RGBA(0,0,0,0). A pixel is in the "boundary zone" if any part of its kernel window extends beyond the element boundary; the boundary zone extends floor(orderX/2) pixels inward from each edge.

For a typical consent banner 320px wide with a text label like "Allow filesystem access?", if orderX="321", then floor(321/2) = 160. The left boundary zone extends 160px from the left edge; the right boundary zone extends 160px from the right edge. Together they cover 320px — the entire width of the element. Every pixel is in the boundary zone; every pixel's convolution kernel includes zero-padded out-of-bounds pixels. If the kernel is designed to weight the zero-padded pixels heavily (or if the boundary-zone fraction is high enough), the output approaches zero. The entire consent banner is erased.

<!-- SA-CSS-CMO-003: orderX=321 on a ~320px wide consent banner
     floor(321/2) = 160 pixels boundary zone on each side
     320px element: every pixel is within 160px of an edge
     With edgeMode="none" and kernelMatrix that includes zero-padded neighbors:
     out-of-bounds neighbors are 0; the kernel dilutes the real pixel values
     toward zero; combined with divisor = orderX (=321) to normalize:
     output = sum_of(kernel * [mixed real + zeros]) / 321 → reduced signal -->
<filter id="cmo-wide">
  <feConvolveMatrix
    order="321 1"
    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 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 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="321"
    edgeMode="none"/>
  <!-- horizontal averaging over 321px; edgeMode=none pads with 0
       For a 320px element: every pixel's 321-wide kernel extends beyond at least one edge
       Mean is diluted by zero-padding by up to (321-1)/321 ≈ 100% -->
</filter>

<div id="consent" style="width:320px; filter:url(#cmo-wide)">
  <p>Allow: credential export, exec shell command.</p>
  <button id="allow">Allow</button>
</div>

// Detection: flag orderX that covers entire element width with edgeMode=none
function auditAsymmetricLargeOrder(feConvolveEl) {
  const orderAttr = feConvolveEl.getAttribute('order') ?? '3';
  const parts = orderAttr.trim().split(/\s+/).map(Number);
  const orderX = parts[0];
  const orderY = parts.length > 1 ? parts[1] : parts[0];
  const edgeMode = feConvolveEl.getAttribute('edgeMode') ?? 'duplicate';
  const boundaryZone = Math.floor(orderX / 2);
  if (boundaryZone > 100 && edgeMode === 'none') {
    return { sev: 'HIGH', note: `feConvolveMatrix orderX=${orderX} + edgeMode=none: boundary zone = ${boundaryZone}px on each side; elements narrower than ${2 * boundaryZone}px are fully in the zero-padding zone; consent text erasure for narrow banners` };
  }
  return null;
}

HIGH — SA-CSS-CMO-003: An asymmetric large orderX combined with edgeMode="none" places the entire width of a narrow consent banner within the zero-padding boundary zone. Every pixel's convolution window includes zero-padded out-of-bounds columns, diluting the real pixel signal toward zero. Detection: flag orderX > 100 with edgeMode="none"; compute the resulting boundary zone and compare against expected consent element widths.

Attack 4: JS mousedown injects large order — undefined output at click time (SA-CSS-CMO-004)

JavaScript changes the order attribute at mousedown capture phase without updating kernelMatrix. The initial state uses a valid, matching kernel (e.g., order="3" with 9 kernelMatrix values). At mousedown, order is changed to "100", creating an immediate mismatch between the declared kernel size (10,000 values expected) and the existing 9-value kernel. The browser now encounters the undefined-behavior condition of CMO-001 at click time. On browsers that zero-fill the missing values, the output becomes near-zero dark-out during the click event. The consent element appears to flicker or darken momentarily, but the "Allow" button click is already registered before the rendering update completes.

<!-- SA-CSS-CMO-004: JS mousedown injects order="100" — creates mismatch with 9-value kernel
     Initial state: order=3, kernelMatrix (9 values) — valid, text visible
     At mousedown: order=100 declared; kernelMatrix still has 9 values
     Browser behavior: undefined (zero-fill in Firefox → near-zero output → dark-out)
     Click fires before rendering update; consent text dark at click time -->
<filter id="cmo-dynamic">
  <feConvolveMatrix id="cmo-cm"
    order="3"
    kernelMatrix="0 0 0  0 1 0  0 0 0"
    divisor="1"
    bias="0"/>
</filter>

<div id="consent" style="filter:url(#cmo-dynamic)">
  <p>Requesting: execute scripts in ~/.local/bin, network access.</p>
  <button id="allow">Allow</button>
</div>
<script>
document.getElementById('consent').addEventListener('mousedown', () => {
  document.getElementById('cmo-cm').setAttribute('order', '100');
  // kernelMatrix still has 9 values; order expects 10000
  // result: mismatch → browser-specific dark-out
}, { capture: true });
</script>

// Detection: MutationObserver — validate kernel length on any order change
const cmEl = document.getElementById('cmo-cm');
new MutationObserver(muts => {
  for (const m of muts) {
    if (m.attributeName === 'order' || m.attributeName === 'kernelMatrix') {
      const orderAttr = cmEl.getAttribute('order') ?? '3';
      const parts = orderAttr.trim().split(/\s+/).map(Number);
      const expected = parts.reduce((a, b) => a * b, 1);
      const km = (cmEl.getAttribute('kernelMatrix') ?? '').trim().split(/\s+/).filter(v => v !== '');
      if (km.length !== expected) {
        console.warn('[SA-CSS-CMO-004] order changed creating mismatch: expected', expected, 'kernelMatrix values; found', km.length, '— undefined SVG spec behavior');
      }
    }
  }
}).observe(cmEl, { attributes: true, attributeFilter: ['order', 'kernelMatrix'] });

HIGH — SA-CSS-CMO-004: Injecting a large order value at mousedown without updating kernelMatrix creates an immediate mismatch that produces undefined browser behavior at click time. On Firefox/Gecko, this results in a dark-out of the consent element. Detection requires a MutationObserver that cross-validates order and kernelMatrix length on every attribute change to either attribute.

Summary table

AttackorderMechanismEffectSeverity
SA-CSS-CMO-001 Large (e.g., 100) kernelMatrix has fewer values than order×order; undefined SVG spec behavior; zero-fill in some browsers Dark-out (browser-specific) High
SA-CSS-CMO-002 1 (1×1 kernel) Single-value zero kernel + bias=0; zeroes all RGBA channels; minimal valid SVG Full element erasure Critical
SA-CSS-CMO-003 Large orderX (e.g., 321) edgeMode=none; entire narrow consent bar in boundary zone; zero-padded dilution Dark-out for narrow consent banners High
SA-CSS-CMO-004 JS mousedown → large Creates kernel/order mismatch at click time; undefined browser output; dark-out in Gecko Dark-out at interaction High

Defences

SkillAudit findings for this attack surface

HIGH SA-CSS-CMO-001: feConvolveMatrix with order implying more kernel values than kernelMatrix contains — SVG spec undefined; browser-specific dark-out or inconsistent output; flag any kernel length mismatch.
CRITICAL SA-CSS-CMO-002: feConvolveMatrix order="1" kernelMatrix="0" bias="0" — minimal valid SVG zero-kernel; zeroes all RGBA channels; fully transparent consent element; flag single-value zero kernels.
HIGH SA-CSS-CMO-003: Large asymmetric orderX with edgeMode="none" — boundary zone covers entire width of narrow consent banners; zero-dilution of all pixels; flag orderX > 100 with edgeMode=none.
HIGH SA-CSS-CMO-004: JS mousedown injects large order creating kernel/order mismatch — undefined browser output (dark-out in Gecko) at click time; MutationObserver cross-validating order and kernelMatrix length required.

Related: kernelMatrix consent attacks  |  preserveAlpha consent attacks  |  divisor consent attacks  |  targetX/targetY anchor attacks

← Blog  |  Security Checklist