Security Guide
MCP server SVG divisor consent security — feConvolveMatrix divisor=0 undefined, large-divisor dark-out, un-normalized white-out, and JS injection
The divisor attribute of <feConvolveMatrix> is the denominator applied to the kernel convolution sum before the result is stored as an output channel value: output = kernel_sum / divisor + bias. The SVG specification notes that the default value of divisor is the sum of all kernel matrix values, ensuring proper normalization. When divisor=0, the spec declares the behavior undefined, producing inconsistent results across browsers. An extremely large divisor effectively collapses all output to near-zero (dark-out), while divisor=1 on a kernel with a coefficient sum greater than 1 amplifies output past the clamp ceiling, forcing all channels to maximum white (white-out).
Attack 1: divisor="0" — undefined SVG spec behavior (SA-CSS-DIV-001)
The SVG 1.1 specification section on feConvolveMatrix states: “If the sum of all of the kernel matrix values is 0 and divisor is not specified, then divisor shall be set to 1. ... If divisor is equal to 0, the behavior is undefined.” Browsers handle this differently: Chromium-based browsers typically produce zero output (all channels = 0, dark-out); Firefox may produce zero or saturate; Safari may use a fallback value. An attacker who knows the target MCP client's rendering engine can select divisor="0" knowing it produces zero output (dark-out) in that specific browser, while the filter validates correctly in a different browser-based scanner that treats the undefined behavior differently.
The cross-browser inconsistency is the attack surface: a security scanner running in Chrome may observe zero output and flag it, but a scanner running as a headless Node process that evaluates the attribute string without rendering may not catch the undefined-behavior implication. The attacker relies on the scanner's static analysis code treating divisor="0" as simply a suspicious value rather than tracking the runtime rendering path in the target browser.
<!-- SA-CSS-DIV-001: divisor=0 — SVG spec: behavior undefined
Chromium: outputs zero (all channels → black)
Firefox: implementation-specific (usually zero or NaN propagation)
Attacker targets specific MCP client browser engine
Scanner may evaluate string "0" without rendering the filter -->
<filter id="div-zero">
<feConvolveMatrix
order="3"
kernelMatrix="0 0 0 0 1 0 0 0 0"
divisor="0"
bias="0"/>
<!-- Identity kernel but divisor=0: output undefined; dark-out in Chromium -->
</filter>
<div id="consent" style="filter:url(#div-zero)">
<p>Requesting: shell exec, filesystem write.</p>
<button id="allow">Allow</button>
</div>
// Detection: always flag divisor=0 regardless of kernel
function auditDivisorZero(feConvolveEl) {
const div = parseFloat(feConvolveEl.getAttribute('divisor') ?? 'NaN');
if (!isNaN(div) && div === 0) {
return { sev: 'HIGH', note: 'feConvolveMatrix divisor=0: SVG spec undefined behavior; Chromium produces zero-output (dark-out); output is browser-engine-dependent and inconsistent across security scanners' };
}
// Also check for near-zero (avoids the 0 check)
if (!isNaN(div) && Math.abs(div) < 1e-6) {
return { sev: 'HIGH', note: `feConvolveMatrix divisor=${div}: near-zero divisor; kernel_sum/divisor amplified to extremes; SVG spec behavior undefined for divisor=0` };
}
return null;
}
HIGH — SA-CSS-DIV-001: divisor="0" is undefined per SVG spec. Chromium-based MCP clients produce zero output (consent text invisible on dark backgrounds). The inconsistency between browser behaviors and static scanner evaluation is the attack surface. Detection: always flag divisor=0 or near-zero on feConvolveMatrix regardless of kernel content.
Attack 2: Very large divisor — dark-out via near-zero output (SA-CSS-DIV-002)
With an extremely large divisor value (e.g., divisor="1000000"), the output formula becomes: output = kernel_sum / 1000000 + bias. For any realistic kernel sum in the range [−9, 9] for a 3×3 kernel with values bounded by 1, the convolution result divided by 1,000,000 produces values in the range [−0.000009, +0.000009] — effectively zero. With bias="0", all output channels are approximately 0.000009, which renders as pure black in integer precision (0/255 after quantization to 8-bit).
The consent text becomes invisible against a dark background. The attack is subtle: a large divisor could appear to be a custom normalization choice (e.g., normalizing to a very small range for a subtle artistic effect). Static analysis that only checks for extreme divisor values may not flag a value like 1000 (which still produces outputs in [−0.009, +0.009] — essentially black). The threshold below which the output is imperceptible depends on the display contrast, but any divisor above approximately 50× the kernel sum ensures near-zero output.
<!-- SA-CSS-DIV-002: Very large divisor — output ≈ 0
kernel_sum(3×3 ones) = 9; divisor=1000000
output = 9 * src_avg / 1000000 ≈ 0 (imperceptible)
bias=0 → solid dark output; consent text invisible on dark UIs -->
<filter id="div-large">
<feConvolveMatrix
order="3"
kernelMatrix="1 1 1 1 1 1 1 1 1"
divisor="1000000"
bias="0"/>
<!-- Looks like an averaging blur with "unusual" normalization -->
</filter>
<div id="consent" style="filter:url(#div-large)">
<p>Allow: write to ~/.config, export $HOME.</p>
<button id="allow">Allow</button>
</div>
// Detection: flag divisor that reduces output to near-zero
function auditLargeDivisor(feConvolveEl) {
const div = parseFloat(feConvolveEl.getAttribute('divisor') ?? '0');
const bias = parseFloat(feConvolveEl.getAttribute('bias') ?? '0');
if (isNaN(div) || div === 0) return null;
const km = (feConvolveEl.getAttribute('kernelMatrix') ?? '').split(/\s+/).map(Number);
const kmax = km.reduce((a, b) => a + Math.abs(b), 0); // absolute sum
if (kmax === 0) return null; // zero kernel handled separately
const maxOutput = kmax / Math.abs(div) + Math.abs(bias);
if (maxOutput < 0.05 && Math.abs(bias) < 0.05) {
return { sev: 'HIGH', note: `feConvolveMatrix divisor=${div} with kernel abs-sum=${kmax}: max output ≈ ${maxOutput.toFixed(4)} — near-zero dark-out; consent text invisible at normal display contrast` };
}
return null;
}
HIGH — SA-CSS-DIV-002: Extremely large divisor values reduce all channel outputs to near-zero regardless of the kernel, producing a dark-out that hides consent text on dark-themed clients. Detection: compute the maximum possible output as |kernel_abs_sum| / |divisor| + |bias|; flag if below a visibility threshold (approximately 0.05).
Attack 3: divisor="1" + un-normalized kernel — white-out via clamp (SA-CSS-DIV-003)
The SVG specification default for divisor is the sum of all kernel matrix values — this ensures a normalized convolution where the output has the same mean as the input (e.g., a 3×3 box blur with all coefficients = 1 and divisor = 9). When divisor="1" is explicitly set with a kernel whose sum is substantially greater than 1, the output is amplified: output = kernel_sum × src_avg / 1. For a 3×3 all-ones kernel (sum = 9), the output is approximately 9 × src_avg — for any non-black pixel, this exceeds 1.0 and is clamped to 1.0. The entire consent element becomes white (or near-white) when rendered.
This is a white-out via overflow-and-clamp. The attack uses what appears to be a standard blur kernel (common, legitimate-looking coefficient values) but with an explicit divisor="1" that bypasses the default normalization. The divisor is a small, non-suspicious-looking integer; the kernel coefficients are all small positive integers; only the combination with divisor="1" creates the problematic output. Scanners that check divisor in isolation (looking for zero or extreme values) miss this attack entirely.
<!-- SA-CSS-DIV-003: divisor=1 + kernel sum=9 — output = 9 * src → clamp to 1.0 (white)
All-ones 3×3 kernel: sum = 9. divisor=1 (not the default 9).
For any pixel with src_R = 0.1 (dark text): output_R = 9 * 0.1 / 1 = 0.9 → not white
For any pixel with src_R = 0.2: output_R = 9 * 0.2 = 1.8 → clamped to 1.0 (white)
At src_R ≥ 0.112, output clamps. Light text and mid-tones white-out. -->
<filter id="div-amp">
<feConvolveMatrix
order="3"
kernelMatrix="1 1 1 1 1 1 1 1 1"
divisor="1"
bias="0"/>
<!-- "blur" kernel but with divisor=1 instead of 9: amplify × 9, clamp → white -->
</filter>
<div id="consent" style="filter:url(#div-amp)">
<p>Granting: network access, secrets vault read.</p>
<button id="allow">Allow</button>
</div>
// Detection: check ratio of kernel sum to divisor
function auditDivisorAmplification(feConvolveEl) {
const div = parseFloat(feConvolveEl.getAttribute('divisor') ?? '0');
if (isNaN(div) || div === 0) return null;
const km = (feConvolveEl.getAttribute('kernelMatrix') ?? '').split(/\s+/).map(Number);
const ksum = km.reduce((a, b) => a + b, 0);
const amplification = ksum / div;
// If amplification * typical_src_value > 1, output clamps to 1.0 (white)
// For src ≥ 0.3 (medium grey): amplification > 3.3 → clamp
if (amplification > 3) {
return { sev: 'HIGH', note: `feConvolveMatrix divisor=${div} with kernel sum=${ksum}: amplification ratio = ${amplification.toFixed(1)}×; most non-dark pixels clamp to 1.0 (white); consent text white-out on light UIs` };
}
return null;
}
HIGH — SA-CSS-DIV-003: divisor="1" with a kernel whose coefficient sum is substantially greater than 1 (e.g., a 3×3 all-ones kernel with sum=9) amplifies pixel values so they clamp to 1.0 (white). Consent text becomes invisible on light-themed MCP clients. Detection: compute the amplification ratio kernel_sum / divisor and flag if greater than approximately 3.
Attack 4: JS mousedown injects divisor change — click-time amplification (SA-CSS-DIV-004)
JavaScript changes the divisor attribute to a value near zero (e.g., "0.001") at mousedown capture phase. The initial state uses a normalized divisor (e.g., divisor="9" for a 3×3 all-ones kernel) — consent text is correctly rendered as a slightly blurred but visible version of the original. On mousedown, the divisor is changed from "9" to "0.001": the amplification becomes 9/0.001 = 9000, which drives all channels to 1.0 (white-out) regardless of input. The click fires on the now-invisible consent element.
<!-- SA-CSS-DIV-004: JS mousedown changes divisor from 9 (normalized) to 0.001
Amplification at click time: 9 / 0.001 = 9000 → all channels clamp to 1.0 (white)
Initial state: divisor=9 → normal blur appearance (legitimate)
At mousedown: divisor=0.001 → white-out; "Allow" button invisible and clicked -->
<filter id="div-dynamic">
<feConvolveMatrix id="div-cm"
order="3"
kernelMatrix="1 1 1 1 1 1 1 1 1"
divisor="9"
bias="0"/>
</filter>
<div id="consent" style="filter:url(#div-dynamic)">
<p>Allow access: read private keys, export credentials.</p>
<button id="allow">Allow</button>
</div>
<script>
document.getElementById('consent').addEventListener('mousedown', () => {
document.getElementById('div-cm').setAttribute('divisor', '0.001');
}, { capture: true });
</script>
// Detection: MutationObserver on divisor attribute
const cmEl = document.getElementById('div-cm');
new MutationObserver(muts => {
for (const m of muts) {
if (m.attributeName === 'divisor') {
const div = parseFloat(cmEl.getAttribute('divisor') ?? '1');
if (Math.abs(div) < 0.01 || div === 0) {
console.warn('[SA-CSS-DIV-004] divisor changed to near-zero:', div, '— amplification extreme; white-out likely');
}
const km = (cmEl.getAttribute('kernelMatrix') ?? '').split(/\s+/).map(Number);
const ksum = km.reduce((a, b) => a + b, 0);
if (Math.abs(div) > 0 && Math.abs(ksum / div) > 10) {
console.warn('[SA-CSS-DIV-004] divisor changed to amplifying value; ksum/div =', (ksum/div).toFixed(0));
}
}
}
}).observe(cmEl, { attributes: true, attributeFilter: ['divisor', 'kernelMatrix', 'bias'] });
HIGH — SA-CSS-DIV-004: JS mousedown injection of a near-zero divisor (e.g., 0.001) amplifies the kernel output by a factor of thousands, clamping all channels to white and erasing the consent text at click time. Initial divisor is a legitimate normalized value. Detection requires a MutationObserver on divisor for all <feConvolveMatrix> elements on consent-containing ancestors, installed before any pointer events.
Summary table
| Attack | divisor | Mechanism | Effect | Severity |
|---|---|---|---|---|
| SA-CSS-DIV-001 | 0 |
SVG spec undefined; Chromium produces zero output; cross-engine inconsistency | Dark-out (Chromium) or inconsistent | High |
| SA-CSS-DIV-002 | Very large (e.g., 1000000) | kernel_sum / divisor ≈ 0; all channels → black; bias=0 | Dark-out on any background | High |
| SA-CSS-DIV-003 | 1 (with sum-9 kernel) |
Amplification × 9; pixel values clamp to 1.0 (white) for all non-black inputs | White-out on light backgrounds | High |
| SA-CSS-DIV-004 | JS mousedown → 0.001 | Amplification ×9000 at click time; all channels white; initial state normalized | White-out at interaction | High |
Defences
- Always flag
divisor="0"— this is undefined per SVG spec and has no legitimate use case in consent UI. The browser-inconsistency between scanner environments and target MCP clients makes it an unreliable evasion technique, but it still constitutes a consent-text erasure risk on Chromium-based clients. - Compute the amplification ratio — evaluate
|kernel_sum / divisor|and flag values substantially above 1.0. For any positive-sum kernel, an amplification ratio above 3 will white-out mid-tone or light pixels. The threshold depends on background contrast, but 3× is a conservative flag threshold. - Check for near-zero effective output — evaluate
|kernel_abs_sum / divisor + |bias||to determine the maximum possible channel output. If the maximum output is below 0.05 (imperceptible), flag as a dark-out attack. - MutationObserver before interaction — monitor the
divisorattribute for all<feConvolveMatrix>elements on consent-containing ancestors. The DIV-004 pattern changes a valid divisor to near-zero at mousedown; detection requires observing the change before the click event fires.
SkillAudit findings for this attack surface
feConvolveMatrix divisor="0" — SVG spec undefined behavior; Chromium produces zero-output dark-out; cross-engine inconsistency exploited for scanner evasion; always flag divisor=0 or near-zero.feConvolveMatrix with very large divisor — kernel_sum/divisor ≈ 0; with bias=0 all channels approach zero (dark-out); detection: compute max output = |kernel_abs_sum| / |divisor| and flag if below 0.05.feConvolveMatrix divisor="1" with un-normalized kernel (sum > 3) — amplification clamps mid-tone and light pixels to 1.0 (white-out); detection: check kernel_sum / divisor ratio; flag if above 3.feConvolveMatrix — amplification becomes extreme at click time; initial state normalized (legitimate); MutationObserver on divisor attribute before pointer events required.