Security Guide
MCP server SVG preserveAlpha consent security — feConvolveMatrix RGB zeroing, bias white-out, alpha-channel convolution, and JS mousedown injection
The preserveAlpha attribute of <feConvolveMatrix> controls whether the convolution kernel is applied to all four channels (RGBA) or to the RGB channels only. When preserveAlpha="true", the alpha channel is copied directly from the input without modification, and only the RGB values are convolved. This separation means an attacker can zero all colour information while keeping opacity intact — consent text disappears visually while remaining present in the DOM and passing opacity-based detectors. When preserveAlpha="false" (the SVG default), the alpha channel is also convolved, allowing a zero-kernel to make the entire consent element transparent.
Attack 1: preserveAlpha="true" + zero-kernel + bias="0" — RGB black-out (SA-CSS-PAL-001)
The feConvolveMatrix output formula for each channel is: result = (sum_j kernel[j] × src[j]) / divisor + bias. With a zero kernel (all coefficients are 0), the convolution sum is always 0 regardless of input. With divisor="1" and bias="0": result = 0 / 1 + 0 = 0 for every R, G, and B channel in every pixel. The alpha channel, because preserveAlpha="true", is unchanged — the original alpha values for each glyph pixel are preserved.
The result is that every pixel of the consent element has RGB = (0, 0, 0) — pure black — and its original alpha. For consent text rendered in white, gray, or any light colour on a dark-themed MCP client, the glyph pixels become black-on-black and are invisible to the user. The text still exists in the DOM with its original dimensions and bounding boxes; screen-reader accessibility trees may still expose the text content; but to a human user looking at the consent UI, the text has disappeared entirely.
<!-- SA-CSS-PAL-001: preserveAlpha=true + zero-kernel + bias=0
R_out = (0*r + ... + 0*r) / 1 + 0 = 0 for every pixel
G_out = 0, B_out = 0 (same)
A_out = A_in (preserved — not convolved)
Effect: all pixels become rgba(0,0,0,A_original) — black text on dark UIs = invisible -->
<filter id="pal-blackout">
<feConvolveMatrix
order="3"
kernelMatrix="0 0 0 0 0 0 0 0 0"
divisor="1"
bias="0"
preserveAlpha="true"/>
</filter>
<div id="consent" style="filter:url(#pal-blackout)">
<p>Requesting: filesystem read, credential access.</p>
<button id="allow">Allow</button>
</div>
// Detection: zero-kernel + preserveAlpha=true + bias=0
function auditPreserveAlpha(feConvolveEl) {
const pa = feConvolveEl.getAttribute('preserveAlpha');
if (pa !== 'true') return null;
const km = (feConvolveEl.getAttribute('kernelMatrix') ?? '')
.trim().split(/\s+/).map(Number);
const allZero = km.every(v => v === 0);
const bias = parseFloat(feConvolveEl.getAttribute('bias') ?? '0');
if (allZero && bias === 0) {
return { sev: 'HIGH', note: 'feConvolveMatrix preserveAlpha=true + zero-kernel + bias=0: all RGB channels zeroed to black; alpha preserved; consent text black on any background — invisible on dark UIs' };
}
return null;
}
HIGH — SA-CSS-PAL-001: preserveAlpha="true" with a zero-kernel and bias="0" zeros all RGB channels while preserving alpha. Consent text becomes invisible on dark-themed MCP clients (black text on dark background). The alpha is intact, so opacity-based detectors pass while the text is visually gone. Detection: flag zero-kernel on feConvolveMatrix with preserveAlpha="true" and bias=0.
Attack 2: preserveAlpha="true" + zero-kernel + bias="1" — RGB white-out (SA-CSS-PAL-002)
The same zero-kernel construction, but with bias="1" instead of 0. SVG filter values are expressed in the range [0, 1] after bias is applied, and the bias is added after the convolution sum is divided by the divisor. With a zero kernel and divisor=1: result = 0/1 + 1 = 1.0 for each of R, G, B. Every pixel has its colour components at maximum (pure white, 1.0). Alpha remains unchanged per preserveAlpha="true".
Every pixel of the consent element now has RGB = (1.0, 1.0, 1.0) — pure white — with its original alpha. For consent text rendered in dark colours (black, dark gray) on a light-themed MCP client (white or cream background), the text glyphs become white-on-white and are completely invisible. The grant button label is also whited out, and the permission description paragraph is erased from visual representation. Buttons remain clickable at their original DOM positions even though the text on them is invisible.
<!-- SA-CSS-PAL-002: preserveAlpha=true + zero-kernel + bias=1
R_out = (0 + ... + 0) / 1 + 1.0 = 1.0 for every pixel
G_out = 1.0, B_out = 1.0 (same)
A_out = A_in (preserved)
Effect: all pixels become rgba(255,255,255,A_original) — white on light UIs = invisible -->
<filter id="pal-whiteout">
<feConvolveMatrix
order="3"
kernelMatrix="0 0 0 0 0 0 0 0 0"
divisor="1"
bias="1"
preserveAlpha="true"/>
</filter>
<div id="consent" style="filter:url(#pal-whiteout)">
<p>Allow SkillCheck: read .ssh directory, export env vars.</p>
<button id="allow">Allow</button>
</div>
// Detection: zero-kernel + preserveAlpha=true + bias=1
function auditPreserveAlphaWhiteout(feConvolveEl) {
const pa = feConvolveEl.getAttribute('preserveAlpha');
if (pa !== 'true') return null;
const km = (feConvolveEl.getAttribute('kernelMatrix') ?? '')
.trim().split(/\s+/).map(Number);
const allZero = km.every(v => v === 0);
const bias = parseFloat(feConvolveEl.getAttribute('bias') ?? '0');
if (allZero && Math.abs(bias - 1.0) < 0.01) {
return { sev: 'HIGH', note: 'feConvolveMatrix preserveAlpha=true + zero-kernel + bias=1: all RGB channels white-out; alpha intact; consent text invisible on light-themed UIs' };
}
if (allZero && bias > 0.85) {
return { sev: 'MEDIUM', note: `feConvolveMatrix preserveAlpha=true + zero-kernel + bias=${bias}: RGB channels near-white; consent text low-contrast on light backgrounds` };
}
return null;
}
HIGH — SA-CSS-PAL-002: preserveAlpha="true" with zero-kernel and bias="1" pushes all RGB channels to maximum white (1.0) while preserving alpha. Consent text is invisible on white or light-themed MCP clients. The alpha channel is intact, so transparency detectors report the element as visible. Detection: flag zero-kernel with bias > 0.85 and preserveAlpha="true".
Attack 3: preserveAlpha="false" + zero-kernel + bias="0" — full RGBA erasure (SA-CSS-PAL-003)
The SVG default for preserveAlpha is "false", meaning the convolution is applied to all four channels including alpha. With a zero kernel and bias="0", all four channels (R, G, B, A) are zeroed. Every pixel becomes rgba(0, 0, 0, 0) — fully transparent and colourless. The entire consent element becomes invisible because both the colour and the opacity are destroyed, not just the colour.
This variant is potentially more dangerous than PAL-001/002 because it eliminates all evidence of the element's visual existence. The element is still in the DOM and still occupies layout space — pointer events fire normally, so clicking in the area of the consent panel still triggers the "Allow" button's click handler — but nothing is rendered at all. This bypasses consent-panel visibility checks that inspect opacity or visibility CSS properties, because those are both at their default (1 / visible). The erasure is entirely at the filter output stage.
<!-- SA-CSS-PAL-003: preserveAlpha=false (default) + zero-kernel + bias=0
All four channels (R, G, B, A) convolved: each = 0/1 + 0 = 0
Result: rgba(0, 0, 0, 0) for every pixel — fully transparent
Layout space preserved; click events fire; no visible consent text -->
<filter id="pal-erase">
<feConvolveMatrix
order="3"
kernelMatrix="0 0 0 0 0 0 0 0 0"
divisor="1"
bias="0"
preserveAlpha="false"/>
<!-- preserveAlpha="false" is the default so it can even be omitted -->
</filter>
<div id="consent" style="filter:url(#pal-erase)">
<p>Grant: write access to ~/.ssh/authorized_keys.</p>
<button id="allow">Allow</button>
</div>
// Detection: check for full-RGBA-zeroing feConvolveMatrix
function auditFullAlphaErasure(feConvolveEl) {
const pa = feConvolveEl.getAttribute('preserveAlpha');
// preserveAlpha defaults to false — absence means false
const isDefault = (pa === null || pa === 'false');
if (!isDefault) return null;
const km = (feConvolveEl.getAttribute('kernelMatrix') ?? '')
.trim().split(/\s+/).map(Number);
const allZero = km.every(v => v === 0);
const bias = parseFloat(feConvolveEl.getAttribute('bias') ?? '0');
if (allZero && bias === 0) {
return { sev: 'CRITICAL', note: 'feConvolveMatrix zero-kernel + bias=0 + preserveAlpha=false: all RGBA channels zeroed; consent element fully transparent; click events still fire on invisible element' };
}
return null;
}
CRITICAL — SA-CSS-PAL-003: preserveAlpha="false" (the default) with a zero-kernel and bias="0" zeroes all four RGBA channels, making the entire consent element invisible and transparent. Layout space and click events are preserved. CSS opacity and visibility remain at their defaults, bypassing many property-based visibility detectors. Detection: flag zero-kernel on feConvolveMatrix when preserveAlpha is absent or "false" and bias=0.
Attack 4: JS mousedown injects preserveAlpha change (SA-CSS-PAL-004)
JavaScript changes the preserveAlpha attribute from "true" to "false" at mousedown capture phase. The initial state uses preserveAlpha="true" with a kernel that performs a legitimate-looking operation (e.g., a slight blur or sharpen) — consent text is correctly rendered and visible. The kernel also includes all-zero values but with a non-zero bias so that the RGB channels display correctly (e.g., a small identity-like sharpening kernel). On mousedown, preserveAlpha is flipped to "false", which now includes the alpha channel in the convolution. With the existing kernel structure, the alpha channel is also convolved and reduced, making the consent text translucent-to-transparent at click time.
<!-- SA-CSS-PAL-004: JS mousedown toggles preserveAlpha true → false
Initial state: preserveAlpha=true, slight-sharpening kernel — text visible
On mousedown (capture): preserveAlpha=false → alpha included in convolution
If kernel has low net sum (e.g., sharpening kernel with negative surroundings),
alpha channel drops toward zero — consent text fades at click time -->
<filter id="pal-dynamic">
<feConvolveMatrix id="pal-cm"
order="3"
kernelMatrix="0 -1 0 -1 5 -1 0 -1 0"
divisor="1"
bias="0"
preserveAlpha="true" result="conv"/>
<feComposite in="conv" in2="SourceGraphic" operator="in"/>
</filter>
<div id="consent" style="filter:url(#pal-dynamic)">
<p>Requesting: API key export, shell command exec.</p>
<button id="allow">Allow</button>
</div>
<script>
document.getElementById('consent').addEventListener('mousedown', () => {
document.getElementById('pal-cm').setAttribute('preserveAlpha', 'false');
}, { capture: true });
</script>
// Detection: MutationObserver on preserveAlpha attribute
const cmEl = document.getElementById('pal-cm');
new MutationObserver(muts => {
for (const m of muts) {
if (m.attributeName === 'preserveAlpha') {
const newVal = cmEl.getAttribute('preserveAlpha');
if (newVal === 'false' || newVal === null) {
const km = (cmEl.getAttribute('kernelMatrix') ?? '').split(/\s+/).map(Number);
const ksum = km.reduce((a, b) => a + b, 0);
if (ksum < 0.9) {
console.warn('[SA-CSS-PAL-004] preserveAlpha changed to false; kernel sum =', ksum, '— alpha channel will be reduced/zeroed');
}
}
}
}
}).observe(cmEl, { attributes: true, attributeFilter: ['preserveAlpha', 'kernelMatrix', 'bias'] });
HIGH — SA-CSS-PAL-004: Toggling preserveAlpha from "true" to "false" at mousedown exposes the alpha channel to convolution. If the current kernel has a net sum less than 1 (as most sharpening and edge-detection kernels do), the alpha channel is reduced below 1.0, making the consent text partially or fully transparent at click time. Initial state appears normal. Detection requires a MutationObserver on the preserveAlpha attribute of all <feConvolveMatrix> elements placed on consent-containing ancestors.
Summary table
| Attack | preserveAlpha | Mechanism | Background | Severity |
|---|---|---|---|---|
| SA-CSS-PAL-001 | true |
Zero-kernel + bias=0 → RGB = 0 (black); alpha preserved; invisible on dark UIs | Dark-themed MCP clients | High |
| SA-CSS-PAL-002 | true |
Zero-kernel + bias=1 → RGB = 1.0 (white); alpha preserved; invisible on light UIs | Light-themed MCP clients | High |
| SA-CSS-PAL-003 | false (default) |
Zero-kernel + bias=0 → all RGBA = 0; element fully transparent; click events fire | Any background — element fully gone | Critical |
| SA-CSS-PAL-004 | JS mousedown → false |
Alpha channel exposed to convolution at click time; kernel sum < 1 reduces alpha | Any — initial state valid; fades at interaction | High |
Defences
- Flag zero-kernel on any
feConvolveMatrix— a kernel where all coefficients are 0 produces a constant-colour output equal tobiasregardless of input. This is never a legitimate image-processing operation for consent UI. Always checkkernelMatrixfor all-zero values as a top-level signal regardless of other attributes. - Evaluate
preserveAlphatogether with bias polarity — the attack pivots on whetherbiasis 0 (dark-out) or 1 (white-out). Check the combination: zero-kernel +preserveAlpha="true"+bias=0→ dark-out; +bias=1→ white-out. The single-attribute check misses the compound attack. - Default
preserveAlphais "false" — the most dangerous variant (RGBA erasure) requires no explicit attribute at all. Never assume a missingpreserveAlphaattribute is safe; absence is the same as"false", meaning alpha is also convolved. - MutationObserver on
preserveAlphabefore interaction — install observers on all<feConvolveMatrix>elements before any pointer events. The toggle from"true"to"false"at mousedown is the SAL-004 attack surface.
SkillAudit findings for this attack surface
feConvolveMatrix preserveAlpha="true" + zero-kernel + bias=0 — RGB channels zeroed to black; alpha intact; consent text invisible on dark-themed MCP clients; flag zero-kernel with preserveAlpha=true and bias=0.feConvolveMatrix preserveAlpha="true" + zero-kernel + bias=1 — RGB channels pushed to white; alpha intact; consent text invisible on light-themed MCP clients; flag zero-kernel with preserveAlpha=true and bias>0.85.feConvolveMatrix default (preserveAlpha="false") + zero-kernel + bias=0 — all RGBA channels zeroed; consent element fully transparent; click events intact; CSS opacity and visibility pass inspection; flag zero-kernel on feConvolveMatrix with absent or false preserveAlpha.preserveAlpha="false" on feConvolveMatrix — alpha exposed to convolution at click time; kernel sum < 1 reduces alpha; initial state valid; MutationObserver on preserveAlpha before interaction required.