MCP server SVG feConvolveMatrix kernelUnitLength consent security
Four SA-CSS-CVK consent attack patterns exploiting kernelUnitLength in feConvolveMatrix — the attribute that controls the pixel-space distance between kernel sample points. A 3×3 kernel with kernelUnitLength=8 achieves exactly the same blur as a 17×17 kernel with default spacing, while appearing innocuous to any detection rule checking only the order attribute.
The SVG feConvolveMatrix filter primitive applies a discrete 2D convolution to a source image using a programmer-specified kernel. Most attention focuses on the order attribute (kernel dimensions) and kernelMatrix values, but the spec includes a less-examined attribute: kernelUnitLength. According to the SVG specification, kernelUnitLength="dx dy" (or a single value applied to both axes) defines the distance in user-space units between adjacent kernel sample positions. The default value is 1, meaning each kernel coefficient samples one pixel away from the previous. When kernelUnitLength is set to a larger value, the kernel is effectively stretched over a wider pixel area — without changing order.
The key formula is the effective blur radius: (order−1) / 2 × kernelUnitLength. For a 3×3 blur kernel (order=3) with kernelUnitLength=8, this yields an effective radius of 8 pixels — identical to a 17×17 kernel at default spacing, or to feGaussianBlur stdDeviation=8. Detection rules that check order > 8 see only order=3 and report no finding.
Order-based detection is insufficient: Any detection rule that flags feConvolveMatrix based solely on the order attribute will miss kernelUnitLength amplification. The correct metric is the effective blur radius — a function of both order and kernelUnitLength. A 3×3 kernel with kernelUnitLength=16 achieves a 16 px blur radius: the same as a 33×33 kernel at default spacing.
kernelUnitLength semantics and the effective radius formula
In standard user space (1 user unit = 1 pixel, no transform), kernelUnitLength="N" places each kernel sample point N pixels apart. A 3×3 kernel (positions −1, 0, +1 in each axis at default spacing) becomes positions −N, 0, +N when kernelUnitLength=N. The total sampling footprint spans from −N to +N, a diameter of 2N+1 pixels (radius N). For an M×M kernel: radius = (M−1)/2 × N.
The convolution is still applied at every output pixel; it is not sub-sampled. Each output pixel receives contributions from source pixels spaced N pixels apart. For a uniform (box) kernel, this is mathematically equivalent to scaling up the kernel size in pixel space. For non-uniform kernels (Gaussian, Laplacian), the effect differs from simply enlarging the kernel because samples land on a coarse grid with no intermediate values, but the spatial reach — and therefore the blur radius — is the same as the enlarged kernel.
/* Effective blur radius formula:
effective_radius = (order - 1) / 2 × kernelUnitLength
Equivalences between order×kernelUnitLength combinations:
order=3, kernelUnitLength=1 → radius = 1px (17×17 kernel footprint would need order=17)
order=3, kernelUnitLength=4 → radius = 4px (same footprint as order=9 @ kUL=1)
order=3, kernelUnitLength=8 → radius = 8px (same footprint as order=17 @ kUL=1)
order=5, kernelUnitLength=10 → radius = 20px (same footprint as order=41 @ kUL=1)
order=7, kernelUnitLength=6 → radius = 18px (same footprint as order=37 @ kUL=1)
For a uniform (box) blur kernel with kernelUnitLength=N:
Each output pixel is the average of source pixels at positions:
x + i×N for i in {-(order-1)/2, ..., 0, ..., +(order-1)/2}
(and similarly for y)
The output pixel for dark text (L=0) at centre of white region (L=1):
At effective_radius=8px, a 1-3px stroke width character has ≈75-90% of its
contributing sample points landing on white background (L=1) rather than
dark stroke (L=0). Average luminance after blur ≈ 0.75×1 + 0.25×0 = 0.75.
WCAG contrast vs white: 1.05 / (0.75 + 0.05) = 1.31:1 (fail) */
Order × kernelUnitLength effective radius reference
| order | kernelUnitLength | Effective radius (px) | Equivalent order @ kUL=1 | Blur severity at 16px text |
|---|---|---|---|---|
| 3 | 1 (default) | 1 px | 3 | Minimal — sub-pixel softening |
| 3 | 2 | 2 px | 5 | Light — character shapes retained |
| 3 | 4 | 4 px | 9 | Heavy — legibility marginal at 16px |
| 3 | 8 | 8 px | 17 | Severe — text unreadable; WCAG fail |
| 3 | 16 | 16 px | 33 | Complete obliteration at any size |
| 5 | 5 | 10 px | 21 | Severe — text unreadable; WCAG fail |
| 5 | 10 | 20 px | 41 | Complete obliteration |
| 7 | 6 | 18 px | 37 | Complete obliteration |
SA-CSS-CVK-001: order=3, kernelUnitLength=8 — 8 px effective blur on consent text
The simplest kernelUnitLength attack uses a 3×3 uniform (all-ones) kernel with divisor=9 and kernelUnitLength=8. Each of the nine kernel positions samples a source pixel 8 pixels away from its neighbour, giving a sampling footprint of 17×17 pixels. Because the kernel coefficients are all equal to 1 and divisor=9 (the kernel count), the output is the average of nine pixels spread across a 17×17 area — a coarse box blur with 8 px effective radius. The SVG markup shows order="3". Any automated scanner that flags feConvolveMatrix based on order alone will report this kernel as small and benign.
WCAG analysis: a typical consent label uses 16px black text (#000, L≈0.0) on a white background (#fff, L=1.0). At 8 px effective blur radius, the dark stroke of a character is spread over a circle of radius 8 px. For a 2-pixel-wide stroke, approximately 88% of the kernel samples land on white background. Blurred stroke luminance ≈ 0.88×1.0 + 0.12×0.0 = 0.88. WCAG contrast: 1.05 / (0.88 + 0.05) = 1.13:1 — severe fail. At more conservative estimates (50/50 mix at glyph edge), effective luminance ≈ 0.5, contrast ≈ 1.05/0.55 = 1.91:1 — still a fail below the 4.5:1 threshold for normal text.
<filter id="cvk001">
<feConvolveMatrix order="3"
kernelMatrix="1 1 1
1 1 1
1 1 1"
divisor="9"
kernelUnitLength="8"
edgeMode="duplicate"/>
</filter>
<!-- order=3 appears small and innocuous to naive scanners.
kernelUnitLength=8: each kernel position samples 8 pixels apart.
Sampling footprint: rows at y-8, y, y+8 and cols at x-8, x, x+8.
Equivalent to a 17×17 box blur (order=17 at kernelUnitLength=1).
Effective radius = (3-1)/2 × 8 = 8px.
feGaussianBlur equivalent: stdDeviation ≈ 8 (same perceptual blur).
Detection check order=3: PASS (no flag — looks like a tiny kernel)
Detection check effective_radius = (3-1)/2 × 8 = 8: FAIL (threshold 3px)
WCAG for blurred consent text at 16px:
stroke width ≈ 2px; blur radius = 8px
Fraction of kernel samples on white background ≈ 0.85–0.90
Blurred glyph pixel L ≈ 0.85×1.0 + 0.15×0.0 = 0.85
Contrast vs white: 1.05 / (0.85+0.05) = 1.17:1 (FAIL, threshold 4.5:1) -->
SA-CSS-CVK-002: kernelUnitLength=0.5 with sharpening kernel — sub-pixel halo artifacts
kernelUnitLength can also be less than 1. A value of 0.5 places each kernel sample point half a pixel apart, applying the convolution at sub-pixel resolution. When paired with a sharpening kernel (kernelMatrix="-1 -1 -1 -1 9 -1 -1 -1 -1", divisor=1), the sub-pixel convolution creates halo artifacts at character edges: narrow bright bands appear around the glyph silhouette on the white background. On a white background, bright halos are invisible (already at maximum luminance), but they raise the apparent luminance of the background immediately adjacent to the glyph — reducing the perceived contrast between the stroke and the local background. Depending on browser sub-pixel rendering, the halos can also manifest as colour fringing, further degrading the perceived contrast of dark text.
This attack is medium severity. The sharpening kernel does not blur the text to unreadability; it introduces artefacts that reduce perceived contrast and may cause perceptual ambiguity, particularly on high-DPI displays where sub-pixel artefacts are more visible. The attack is also browser-dependent: some browsers clamp or ignore kernelUnitLength values below the device pixel ratio; others apply the sub-pixel convolution faithfully.
<filter id="cvk002">
<feConvolveMatrix order="3"
kernelMatrix="-1 -1 -1
-1 9 -1
-1 -1 -1"
divisor="1"
kernelUnitLength="0.5"
edgeMode="duplicate"/>
</filter>
<!-- Sharpening kernel with sub-pixel sampling.
kernelUnitLength=0.5: kernel samples at x-0.5, x, x+0.5 sub-pixel positions.
Sharpening formula for each pixel: 9×centre - sum(8 neighbours).
At sub-pixel resolution, "centre" and "neighbours" differ by only 0.5px.
Effect: bright halos around character edges on white background.
Halo intensity: raises perceived luminance of background near glyph.
Perceived contrast reduction: moderate (medium severity).
Browser-dependent: halos most visible on high-DPI displays. -->
SA-CSS-CVK-003: order=5, kernelUnitLength=10 — 20 px effective radius, complete obliteration
A 5×5 all-ones kernel (order=5, 25 coefficients each equal to 1, divisor=25) with kernelUnitLength=10 achieves an effective blur radius of (5−1)/2 × 10 = 20 px. The sampling footprint spans a 41×41 pixel area around each output pixel. At 20 px radius, no text at typical web sizes (12–24 px) remains legible. The entire consent dialog appears as a uniform blurred smear. Characters with stroke widths of 1–4 px contribute less than 5% of the total kernel weight to the blurred output pixel, meaning even glyph centres approach white background luminance (L ≈ 0.95). WCAG contrast: 1.05 / (0.95 + 0.05) = 1.05:1 — effectively no contrast.
To a scanner checking order > 8, this kernel (order=5) appears moderate. The 25-coefficient kernel matrix contains no negative values and sums to 25 (matching divisor=25), which a kernel classifier marks as a normalised uniform blur — low order, apparently low severity. Only the effective radius calculation exposes the attack.
<filter id="cvk003">
<feConvolveMatrix order="5"
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"
divisor="25"
kernelUnitLength="10"
edgeMode="duplicate"/>
</filter>
<!-- order=5 uniform blur with 10px sample spacing.
Effective radius = (5-1)/2 × 10 = 20px.
Sampling footprint: 41×41 pixel area.
Equivalent to order=41 at kernelUnitLength=1.
Detection:
Check order > 8: PASS (order=5 — no flag)
Check effective_radius = (5-1)/2 × 10 = 20: FAIL (threshold 3px)
WCAG at 20px radius:
Stroke width ≈ 2px; total sampling area ≈ 41×41 = 1681 pixels
Stroke pixels in kernel area ≈ 2px × glyph_height ≈ 30–60 pixels
Fraction: ~2-4% stroke pixels → blurred L ≈ 0.96–0.98
WCAG contrast vs white: 1.05 / (0.97+0.05) ≈ 1.03:1 (FAIL)
Complete legibility obliteration at any text size. -->
SA-CSS-CVK-004: JS mousedown kernelUnitLength amplification
The most evasion-resistant variant uses a JS event listener to change kernelUnitLength at click time. At page load, the feConvolveMatrix element carries an identity kernel (order=3, centre coefficient 1 all others 0, kernelUnitLength=1). This renders consent text with full contrast and zero blur — correct display at audit time. The identity kernel is the only state visible to static DOM analysis or screenshot-based audit tools. On mousedown, a single setAttribute call sets kernelUnitLength from 1 to 8, and a second replaces kernelMatrix with all-ones. The effective radius jumps from 0 to 8 px instantaneously at the moment of user confirmation. Unlike replacing the entire kernel (as in SA-CSS-CVM-004), this variant minimises the number of attribute mutations: a MutationObserver must watch both kernelMatrix and kernelUnitLength to catch this pattern.
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="cvk004">
<feConvolveMatrix id="cvk004K" order="3"
kernelMatrix="0 0 0 0 1 0 0 0 0"
divisor="1"
kernelUnitLength="1"/>
</filter>
</defs>
</svg>
<div style="filter:url(#cvk004)">
Grant this MCP server read access to your file system.
<button id="allowBtn">Allow</button>
</div>
<script>
// At load: identity kernel, kernelUnitLength=1 → no blur, full legibility.
// Static analysis: reports benign (identity kernel, kUL=1).
document.getElementById("allowBtn").addEventListener("mousedown", () => {
const k = document.getElementById("cvk004K");
k.setAttribute("kernelMatrix", "1 1 1 1 1 1 1 1 1");
k.setAttribute("kernelUnitLength", "8");
// effective_radius = (3-1)/2 × 8 = 8px blur activated at click.
// Text becomes illegible at the moment user taps Allow.
// getComputedStyle().color still returns original dark text color.
});
</script>
// Detection via MutationObserver:
const obs = new MutationObserver(muts => {
for (const m of muts) {
if (m.type === "attributes" &&
(m.attributeName === "kernelUnitLength" || m.attributeName === "kernelMatrix")) {
const el = m.target;
const kul = parseFloat(el.getAttribute("kernelUnitLength") || "1");
const order = parseInt(el.getAttribute("order") || "3");
const effectiveRadius = (order - 1) / 2 * kul;
if (effectiveRadius > 3) {
// Block the click, alert audit system
console.warn("SA-CSS-CVK-004: kernelUnitLength amplification detected", {
order, kul, effectiveRadius
});
}
}
}
});
document.querySelectorAll("feConvolveMatrix").forEach(el => {
obs.observe(el, { attributes: true,
attributeFilter: ["kernelMatrix", "kernelUnitLength", "order"] });
});
Detection: effective blur radius and kernel type classification
The correct detection strategy computes the effective blur radius for every feConvolveMatrix element that is an ancestor filter of a consent element, using: effective_radius = (order−1) / 2 × kernelUnitLength. If the kernel is a blur type (all non-negative coefficients, sum ≈ divisor) and effective_radius > 3, flag as HIGH. If effective_radius > 6, flag as CRITICAL. The kernel type check ensures the rule does not fire on identity or sharpening kernels with large kernelUnitLength. A MutationObserver on kernelUnitLength, kernelMatrix, and order catches dynamic amplification attacks. The observer should re-run the effective radius check on each attribute change and block confirmation if the threshold is exceeded.
function auditKernelUnitLength(filterEl) {
const findings = [];
for (const m of filterEl.querySelectorAll("feConvolveMatrix")) {
const order = parseInt(m.getAttribute("order") || "3");
const kul = parseFloat(m.getAttribute("kernelUnitLength") || "1");
const effectiveRadius = (order - 1) / 2 * kul;
const vals = (m.getAttribute("kernelMatrix") || "").trim().split(/\s+/).map(Number);
const divisor = parseFloat(m.getAttribute("divisor") || "1");
const kernelSum = vals.reduce((a, b) => a + b, 0);
const allNonNeg = vals.every(v => v >= 0);
const isBlurKernel = allNonNeg && Math.abs(kernelSum / divisor - 1.0) < 0.05;
if (isBlurKernel && effectiveRadius > 3) {
findings.push({
pattern: effectiveRadius > 8 ? "SA-CSS-CVK-003" : "SA-CSS-CVK-001",
severity: effectiveRadius > 6 ? "CRITICAL" : "HIGH",
order, kul, effectiveRadius,
note: `order=${order} × kUL=${kul} = ${effectiveRadius}px effective radius`
});
}
// Check for identity kernel that might be dynamically amplified
const centre = Math.floor(vals.length / 2);
const isIdentity = vals[centre] === 1 && vals.filter(v => v !== 0).length === 1;
if (isIdentity && kul === 1) {
findings.push({
pattern: "SA-CSS-CVK-004-candidate",
severity: "MONITOR",
note: "Identity kernel — watch for kernelUnitLength attribute mutation"
});
}
}
return findings;
}
Attack summary
| ID | order | kernelUnitLength | Effective radius | Effect on consent text | Severity |
|---|---|---|---|---|---|
| SA-CSS-CVK-001 | 3 | 8 | 8 px | 17×17 footprint; WCAG contrast 1.17:1–1.91:1 (fail) | High |
| SA-CSS-CVK-002 | 3 (sharpen) | 0.5 | sub-pixel | Halo artifacts; reduced perceived contrast at glyph edges | Medium |
| SA-CSS-CVK-003 | 5 | 10 | 20 px | 41×41 footprint; WCAG contrast ≈1.03:1 (complete fail) | Critical |
| SA-CSS-CVK-004 | 3 → 3 | 1 → 8 (mousedown) | 0 → 8 px | Static: identity (no blur); click-time: 8 px blur applied | Critical |
SkillAudit findings for this attack surface
feConvolveMatrix with order=3, all-ones kernel, divisor=9, kernelUnitLength=8 on consent element ancestor — effective blur radius 8 px (equivalent to 17×17 kernel at default spacing); blurred glyph pixel luminance 0.85–0.95; WCAG contrast vs white 1.05–1.91:1 (fail 4.5:1 threshold); order=3 evades all order-threshold detection rules; fix: compute (order−1)/2 × kernelUnitLength and flag if result > 3 px for blur-type kernels.feConvolveMatrix sharpening kernel (order=3, centre=9, surround=−1, divisor=1) with kernelUnitLength=0.5 on consent element ancestor — sub-pixel sampling produces halo artefacts at character edges; bright halos raise local background luminance adjacent to glyph strokes; perceived contrast reduced at glyph edges; severity browser-dependent; high-DPI displays most affected; MutationObserver should watch kernelUnitLength for values < 0.5 combined with high-pass kernel signature.feConvolveMatrix with order=5, 5×5 all-ones kernel (25 coefficients), divisor=25, kernelUnitLength=10 on consent element — effective radius 20 px (41×41 pixel footprint); complete legibility obliteration at all text sizes; dark glyph pixels diluted to L ≈ 0.95–0.98; WCAG contrast ≈ 1.03–1.10:1; naive order check (order=5) passes — only effective radius formula catches this; WCAG analysis: at 16px text, stroke area < 4% of 41×41 kernel footprint.mousedown handler changes kernelUnitLength from 1 to 8 and replaces kernelMatrix with all-ones on feConvolveMatrix element in consent filter — static DOM shows identity kernel and kernelUnitLength=1 (benign); blur activated at confirmation click; effective radius transitions 0 → 8 px instantaneously; fix: MutationObserver on kernelUnitLength, kernelMatrix, and order attributes of all feConvolveMatrix elements; re-run effective radius check on each mutation; block confirmation if effective radius exceeds threshold.