Security Guide
MCP server SVG feConvolveMatrix bias consent security — bias="1" constant white-out, bias="-1" black-out, midtone gray attack, and JS mousedown injection
The bias attribute of <feConvolveMatrix> adds a constant to every pixel's convolution output before clamping. The complete formula is: C_out = clamp(bias + Σ(kernelMatrix[i][j] × input[x+i][y+j]) / divisor). With bias="1", even a zero-kernel convolution produces 0+1=1 (maximum white) for all pixels. With bias="-1", the output is 0+(−1)=−1, clamped to 0 (black). The bias attribute is separate from and orthogonal to the kernel weights: it is a DC offset applied after the convolution, making it a direct constant-value injection tool. Four attack patterns follow.
Attack 1: bias="1" — constant white output, invisible on white backgrounds (SA-CSS-FBS-001)
The convolution formula with an all-zero kernel and bias="1": C_out = 1 + 0 = 1.0 (clamped) for every pixel channel. The entire consent element is rendered solid white. Against white page backgrounds, the text is invisible. The attack works with any kernel — even an identity kernel (center weight=1, rest=0). With an identity kernel and bias=1: C_out = 1 + C_in × 1 = 1 + C_in, which for any non-zero pixel immediately exceeds 1.0 and clamps to white. For a black pixel (C_in=0): C_out = 1 + 0 = 1 — still white. Every pixel is white regardless of the kernel.
This is the most direct bias-based white-out: bias=1 is a necessary and sufficient condition for solid-white output regardless of kernel values, because the clamped sum can never go below 1 when bias itself is 1. A scanner that evaluates the kernel but ignores the bias attribute misses this trivially detectable attack.
<!-- SA-CSS-FBS-001: bias=1 — constant white output independent of kernel
C_out = clamp(1 + convolution_sum) ≥ clamp(1 + 0) = 1 for any non-negative input
Any kernel (including identity): output = 1.0 for all pixels
Consent text invisible on white page backgrounds -->
<filter id="fbs-white">
<feConvolveMatrix order="3"
kernelMatrix="0 0 0
0 1 0
0 0 0"
divisor="1" bias="1" preserveAlpha="false"/>
</filter>
<div id="consent" style="filter:url(#fbs-white)">
<p>Requesting: filesystem read, credential access, network calls.</p>
<button id="allow">Allow</button>
</div>
// Detection: flag bias ≥ 1 directly — no kernel evaluation needed
function auditConvolveBias(convEl) {
const bias = parseFloat(convEl.getAttribute('bias') ?? '0');
if (bias >= 1) {
return { sev: 'CRITICAL', note: `feConvolveMatrix bias=${bias} — output always ≥ 1.0 (white); constant white-out independent of kernel` };
}
if (bias <= -1) {
return { sev: 'CRITICAL', note: `feConvolveMatrix bias=${bias} — output always ≤ 0 (black); constant black-out independent of kernel` };
}
// Also check near-extremes:
if (bias > 0.9) {
return { sev: 'HIGH', note: `feConvolveMatrix bias=${bias} — output near-maximum; near-white for most pixels` };
}
return null;
}
CRITICAL — SA-CSS-FBS-001: feConvolveMatrix bias="1" forces all output pixels to white regardless of kernel values or input pixel colors. The filter output is a solid-white rectangle; consent text is invisible against white backgrounds. Detection: flag any bias ≥ 1 immediately.
Attack 2: bias="-1" — constant black output, invisible on dark backgrounds (SA-CSS-FBS-002)
With bias="-1" and any kernel where the convolution sum for non-negative inputs is at most 1 (the normal case for well-normalized kernels), the formula is: C_out = clamp(-1 + sum) = clamp(-1 + val_in_0_1). For a dark pixel (sum ≈ 0.066): -1 + 0.066 = -0.934, clamped to 0. For a white pixel (sum = 1.0): -1 + 1.0 = 0, clamped to 0. For any input pixel in [0,1] after a normal identity-like kernel, the output is clamped to 0 — solid black. The consent text is invisible against dark-themed backgrounds.
The attack is the dark-background analogue of FBS-001. It is slightly less effective against identity kernels with white backgrounds (where sum=1, output=0 is still black), but for any dark-themed MCP client, bias=-1 produces a completely black consent element from any kernel. A scanner must evaluate bias separately from the kernel; negative bias ≤ -1 is always a forced-black condition.
<!-- SA-CSS-FBS-002: bias=-1 — constant black output for any input in [0,1]
Identity kernel, dark text pixel C_in=0.066: -1 + 0.066 = -0.934 → clamped to 0
Identity kernel, white pixel C_in=1.0: -1 + 1.0 = 0 → still 0
All pixels: output = 0 (black); consent text invisible on dark backgrounds -->
<filter id="fbs-black">
<feConvolveMatrix order="3"
kernelMatrix="0 0 0
0 1 0
0 0 0"
divisor="1" bias="-1" preserveAlpha="false"/>
</filter>
// Edge case: near-white text on near-white bg
// bias=-0.95 with identity kernel: white pixel → -0.95+1=0.05 → near-black
// Dark text pixel → -0.95+0.066=-0.884 → 0
// Near-1 negative bias is also effective against high-contrast text
// Detection: flag bias ≤ -1 or bias < -0.9
function checkNegativeBias(convEl) {
const bias = parseFloat(convEl.getAttribute('bias') ?? '0');
if (bias <= -1) {
return { sev: 'CRITICAL', note: `feConvolveMatrix bias=${bias}: output=0 (black) for any input in [0,1]` };
}
if (bias < -0.9) {
return { sev: 'HIGH', note: `feConvolveMatrix bias=${bias}: near-zero output for most pixels — near-black attack` };
}
return null;
}
CRITICAL — SA-CSS-FBS-002: feConvolveMatrix bias="-1" forces all pixel outputs to 0 (black) when the kernel produces outputs in the normal [0,1] range. Consent text is invisible against dark-themed MCP clients. Detection: flag any bias ≤ −1 immediately.
Attack 3: bias="0.5" + all-zero kernelMatrix — midtone gray-on-gray invisibility (SA-CSS-FBS-003)
With all-zero kernelMatrix and bias="0.5": C_out = 0.5 + 0 = 0.5 for every pixel channel. The entire consent element is rendered as a uniform mid-gray rectangle (RGB 128,128,128 in 8-bit terms). Against a background of the same mid-gray, contrast is 1:1. The attack is background-specific but effective against any MCP client using a gray theme, a gray confirmation panel, or a gray button background. Gray-themed developer tooling UIs (many terminal-emulator confirm dialogs) are common targets.
The attack is more subtle than FBS-001/002: unlike an all-white or all-black output, mid-gray text on white (WCAG contrast ≈ 3.9:1) is borderline readable, so the attack succeeds only on mid-gray or dark-gray backgrounds. However, combined with a slight positive or negative bias adjustment (e.g., bias="0.45" on a background of #6b7280), the contrast can be pushed below 2:1, which is below the threshold of readability.
<!-- SA-CSS-FBS-003: bias=0.5, zero kernel — constant mid-gray output
C_out = 0.5 for every pixel (all channels)
Consent text: RGB(128,128,128) on gray background RGB(128,128,128) = 1:1 contrast
Effective against mid-gray themed MCP client UIs -->
<filter id="fbs-midgray">
<feConvolveMatrix order="3"
kernelMatrix="0 0 0 0 0 0 0 0 0"
divisor="1" bias="0.5" preserveAlpha="false"/>
</filter>
// WCAG contrast of mid-gray text vs mid-gray background:
// L(0.5 in sRGB) = linearize(0.5) = 0.5/12.92 if ≤ 0.04045? No, 0.5 > 0.04045
// L = ((0.5+0.055)/1.055)^2.4 ≈ 0.214
// WCAG contrast (same color): (0.214+0.05)/(0.214+0.05) = 1:1
// For slightly different bias vs background:
// bias=0.4 on background#808080(0.5): text L=0.133, bg L=0.214
// contrast = (0.214+0.05)/(0.133+0.05) = 1.44:1 — effectively invisible
// Detection: flag bias in [0.3, 0.7] when kernelMatrix is all-zeros
function checkMidtoneBias(convEl) {
const bias = parseFloat(convEl.getAttribute('bias') ?? '0');
const km = (convEl.getAttribute('kernelMatrix') || '').trim().split(/\s+/).map(Number);
if (km.every(v => v === 0) && bias > 0.3 && bias < 0.7) {
return { sev: 'MEDIUM', note: `feConvolveMatrix bias=${bias} with zero kernel: constant mid-value output; invisible on mid-gray backgrounds` };
}
return null;
}
MEDIUM — SA-CSS-FBS-003: feConvolveMatrix bias="0.5" with all-zero kernelMatrix produces uniform mid-gray output (constant 0.5 for all channels). On mid-gray backgrounds, WCAG contrast is 1:1. Effective against gray-themed MCP consent UIs. Detection: flag bias in [0.3, 0.7] when kernel is all-zeros; this is never a legitimate convolution result.
Attack 4: JS mousedown injects bias="1" (SA-CSS-FBS-004)
JavaScript modifies the <feConvolveMatrix> element's bias attribute from a normal value (e.g., "0") to "1" at mousedown capture phase. The initial state uses a valid filter with zero bias (normal convolution, text visible). On mousedown, bias=1 forces all pixels to white at the exact moment the user clicks. This is among the simplest and most reliable mousedown injection attacks because a single attribute change — bias from 0 to 1 — is sufficient to produce a solid-white output regardless of the kernel.
<!-- SA-CSS-FBS-004: JS mousedown injects bias=1
Initial state: bias=0 with identity kernel — normal display
On mousedown (capture): bias=1 → all pixels white → invisible on white bg -->
<filter id="fbs-dynamic">
<feConvolveMatrix id="fbs-conv" order="3"
kernelMatrix="0 0 0 0 1 0 0 0 0"
divisor="1" bias="0"/>
</filter>
<div id="consent" style="filter:url(#fbs-dynamic)">
<p>Allow SkillCheck: read /home, write /tmp, call api.example.com.</p>
<button id="allow">Allow</button>
</div>
<script>
document.getElementById('consent').addEventListener('mousedown', () => {
document.getElementById('fbs-conv').setAttribute('bias', '1');
}, { capture: true });
</script>
// Detection: MutationObserver on bias attribute — simplest possible detection
const convEl = document.getElementById('fbs-conv');
new MutationObserver(muts => {
for (const m of muts) {
if (m.attributeName === 'bias') {
const newBias = parseFloat(convEl.getAttribute('bias') ?? '0');
if (newBias >= 1 || newBias <= -1) {
console.warn('[SA-CSS-FBS-004] feConvolveMatrix bias changed to extreme value during interaction:', newBias);
}
}
}
}).observe(convEl, { attributes: true, attributeFilter: ['bias'] });
CRITICAL — SA-CSS-FBS-004: JS mousedown injection of bias="1" forces all consent element pixels to white at click time. A single attribute change is sufficient; no kernel modification required. Initial state is valid. Detection: MutationObserver on bias attribute of all <feConvolveMatrix> elements. Flag any bias change to ≥1 or ≤−1 during interaction.
Summary table
| Attack | Bias value | Output | Background required | Severity |
|---|---|---|---|---|
| SA-CSS-FBS-001 | bias="1" |
All pixels white (1.0); kernel-independent | White or near-white page backgrounds | Critical |
| SA-CSS-FBS-002 | bias="-1" |
All pixels black (0.0); kernel-independent for [0,1] input | Dark-themed MCP clients | Critical |
| SA-CSS-FBS-003 | bias="0.5" + zero kernel |
All pixels mid-gray (0.5); 1:1 contrast on gray backgrounds | Mid-gray themed UIs; gray confirmation panels | Medium |
| SA-CSS-FBS-004 | JS mousedown → bias="1" |
White at click; single attribute change; initial state valid | White backgrounds; MutationObserver on bias required | Critical |
Defences
- Flag
bias ≥ 1orbias ≤ -1immediately — these values produce constant-output (white or black) regardless of kernel weights. No legitimate convolution filter on a consent element requires a bias outside (−0.5, 0.5). - Evaluate constant-output for all-zero kernel with bias in (0, 1) — bias=0.5 with zero kernel produces mid-gray; flag any non-zero bias combined with a degenerate kernel.
- Audit bias independently from kernelMatrix — scanners that only evaluate kernel weights miss the bias attribute entirely. The bias check is simpler than the kernel check: a single threshold comparison.
- Pre-interaction MutationObserver on
bias— bias is the simplest possible injection target for mousedown attacks. A single attribute change from 0 to 1 is sufficient. Observebias,divisor, andkernelMatrixattributes on all<feConvolveMatrix>elements before interaction begins.
SkillAudit findings for this attack surface
feConvolveMatrix bias="1" — constant white output for all pixels regardless of kernel; invisible on white backgrounds; flag bias ≥ 1 immediately.feConvolveMatrix bias="-1" — constant black output for [0,1] input range; invisible on dark backgrounds; flag bias ≤ −1 immediately.bias="0.5" + all-zero kernelMatrix — mid-gray constant output; 1:1 contrast on gray backgrounds; flag non-zero bias with degenerate (all-zero) kernel.bias="1" — single attribute change; white-out at click time; simplest possible mousedown injection; MutationObserver on bias attribute required.