MCP server SVG feColorMatrix near-white bias consent security
Four SA-CSS-CMB consent attack patterns using feColorMatrix type="matrix" with all-zero RGB input coefficients and a sub-maximum constant offset — the near-white bias attack that forces all pixels to a constant light gray value, produces a WCAG contrast failure below 4.5:1 against white backgrounds, and evades the exact-offset=1.0 detection rule that catches the pure-white SA-CSS-CMM-003 variant.
The SA-CSS-CMM-003 attack forces all RGB channels to 1.0 via the offset column (values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"), making the entire consent panel pure white. A detection rule that flags any feColorMatrix where all RGB output coefficients are zero and all offsets are 1.0 correctly identifies this pattern. The near-white bias attack uses offsets in the range 0.6–0.99 instead of exactly 1.0. The pixel output is a constant gray instead of pure white. The contrast against a white background is below 4.5:1 at any bias above 0.183. The detector checking for exact offset=1.0 produces a false negative.
Exact-1.0 detection is insufficient: Any all-zero RGB coefficient row with an offset between 0.183 and 0.999 produces a constant-output channel that fails WCAG AA against white. An offset of 0.85 produces WCAG 1.17:1; an offset of 0.5 produces WCAG 1.91:1; an offset of 0.2 produces WCAG 4.20:1 (just below the 4.5 threshold). The correct detection threshold is: flag any all-zero RGB coefficient row with a constant output α satisfying 0.183 < α < 0.999. The exact-1.0 check misses the entire 0.183–0.999 attack range.
Constant-output bias formula and WCAG thresholds
/* feColorMatrix all-zero coefficients + offset α: constant output channel
R' = 0×R + 0×G + 0×B + 0×A + α = α (constant, regardless of input)
G' = α
B' = α
A' = A (preserved)
WCAG contrast of constant gray (L=α) vs white background (L=1.0):
contrast = (1.0 + 0.05) / (α + 0.05) = 1.05 / (α + 0.05)
Threshold: contrast < 4.5 (WCAG AA) when:
1.05 / (α + 0.05) < 4.5
α + 0.05 > 1.05 / 4.5 = 0.233
α > 0.183
So: any constant gray output with α > 0.183 → WCAG AA fail against white.
Key values:
α = 0.85: contrast = 1.05/0.90 = 1.17:1 (CRITICAL — invisible)
α = 0.75: contrast = 1.05/0.80 = 1.31:1 (CRITICAL — invisible)
α = 0.60: contrast = 1.05/0.65 = 1.62:1 (CRITICAL — invisible)
α = 0.50: contrast = 1.05/0.55 = 1.91:1 (CRITICAL — very low contrast)
α = 0.30: contrast = 1.05/0.35 = 3.00:1 (HIGH — below AA)
α = 0.20: contrast = 1.05/0.25 = 4.20:1 (HIGH — just below AA)
α = 0.183: contrast = 1.05/0.233 = 4.5:1 (at the exact threshold)
α = 0.10: contrast = 1.05/0.15 = 7.00:1 (passes AAA — not an attack)
Detection threshold: flag if maxOutputChannel(A=1) in range (0.183, 0.999).
The upper bound 0.999 excludes the exact-1.0 case (CMM-003, already covered separately).
The lower bound 0.183 is where WCAG AA failure begins. */
SA-CSS-CMB-001: near-white bias α=0.85 — light gray, WCAG 1.17:1, evades exact-1.0 check
Setting all RGB coefficient rows to zero and the offset column to 0.85 forces every pixel in the consent panel to the constant linearRGB value (0.85, 0.85, 0.85) at original alpha. The consent text is indistinguishable from the background: WCAG contrast 1.17:1 against white. Unlike the exact-white attack (offset=1.0), this variant is a light gray — perceptually distinct from white if viewed on a colored background but nearly invisible on the typical white consent dialog background. Exact-1.0 detection rules do not flag 0.85.
<filter id="cmb001">
<feColorMatrix type="matrix"
values="0 0 0 0 0.85
0 0 0 0 0.85
0 0 0 0 0.85
0 0 0 1 0"/>
</filter>
<!-- Every pixel: R'=G'=B'=0.85 (constant light gray), A'=A (original alpha)
Dark text: was (0.017, 0.017, 0.017) → (0.85, 0.85, 0.85) = light gray
White background: was (1.0, 1.0, 1.0) → (0.85, 0.85, 0.85) = light gray
Wait — BOTH text AND background become the same gray!
But: if the background's CSS background-color is applied OUTSIDE the filter
(white background div, filtered inner text), the text becomes gray on white bg.
If the background IS inside the filter: both text and bg → same gray → invisible.
Either way: consent text is invisible. -->
/* SA-CSS-CMB-001 detection:
In the 20-element values array, check each RGB output row (rows 0,1,2):
- Indices 0,1,2,3 (input coefficients) should all be ≈ 0
- Index 4 (constant offset) = 0.85 ≠ 0 and ≠ 1
If first four indices of any RGB row are all < 0.01:
constantOutput = values[rowStart + 4] // the offset term
if (constantOutput > 0.183 && constantOutput < 0.999):
flag as CRITICAL (WCAG contrast = 1.05/(constantOutput+0.05)) */
function detectConstantOutputBias(filterEl) {
for (const m of filterEl.querySelectorAll('feColorMatrix[type="matrix"]')) {
const v = (m.getAttribute('values') || '').trim().split(/\s+/).map(Number);
if (v.length !== 20) continue;
// Check RGB rows (row 0: v[0-4], row 1: v[5-9], row 2: v[10-14])
for (const base of [0, 5, 10]) {
const allInputsZero = [v[base],v[base+1],v[base+2],v[base+3]].every(c => Math.abs(c) < 0.01);
if (allInputsZero) {
const bias = v[base + 4];
if (bias > 0.183 && bias < 0.999) {
const wcag = 1.05 / (bias + 0.05);
return { risk: 'CRITICAL', wcag: wcag.toFixed(2), bias };
}
}
}
}
return null;
}
SA-CSS-CMB-002: mid-gray bias α=0.5 — WCAG 1.91:1, evades both exact-0 and exact-1 checks
A bias of 0.5 forces all pixels to constant mid-gray. This is more perceptually obvious than the near-white variant (the consent panel is clearly gray rather than subtly off-white), but it still fails WCAG AA with 1.91:1 contrast against white. More importantly, it evades both the exact-1.0 check (SA-CSS-CMM-003) and any exact-0.0 check (transparent-output detection). Mid-gray bias is a “moderate evasion” attack: the visible gray may look like a rendering artifact or dark-mode confusion rather than a deliberate attack.
<filter id="cmb002">
<feColorMatrix type="matrix"
values="0 0 0 0 0.5
0 0 0 0 0.5
0 0 0 0 0.5
0 0 0 1 0"/>
</filter>
<!-- Every pixel → mid-gray (0.5, 0.5, 0.5) at original alpha
WCAG vs white: 1.05/0.55 = 1.91:1 — fail
The consent panel appears as a gray rectangle — may be attributed to
CSS theming error, dark-mode loading glitch, or transition state.
An attacker may combine this with a brief appearance delay
(filter applied after 50ms) to make the gray state appear like a load flash. -->
SA-CSS-CMB-003: low bias α=0.2 — WCAG 4.20:1, just below AA threshold
A bias of 0.2 produces dark-gray output (L=0.2) with WCAG contrast 1.05/0.25=4.20:1 against white — just below the WCAG AA 4.5:1 minimum. This is the most subtle variant: the consent text is perceivably dark but technically fails WCAG. An automated scanner using a threshold of α>0.3 would miss this variant; a scanner using the correct threshold α>0.183 would catch it. The visual difference between this and a legitimate dark-gray style is negligible to humans.
<filter id="cmb003">
<feColorMatrix type="matrix"
values="0 0 0 0 0.2
0 0 0 0 0.2
0 0 0 0 0.2
0 0 0 1 0"/>
</filter>
<!-- Every pixel → dark gray (0.2, 0.2, 0.2) at original alpha
WCAG vs white: 1.05/(0.2+0.05) = 1.05/0.25 = 4.20:1 — just below AA (4.5:1)
A scanner with threshold α > 0.3 misses this; correct threshold: α > 0.183
Visual appearance: legitimate dark-gray text styling — high deceptive quality
When combined with actual legitimate low-contrast styling elsewhere in the
consent UI, this is nearly undetectable without filter graph analysis. -->
SA-CSS-CMB-004: JS mousedown identity → near-white bias at click time
The initial filter is the identity matrix. Consent text renders correctly. On mousedown, the values are swapped to the near-white bias matrix. All consent text immediately becomes light gray at the moment the user activates the “Allow” button. As with all click-time attacks, static analysis sees only the identity.
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="cmb004">
<feColorMatrix id="cmb004M" type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
</filter>
</defs>
</svg>
<div style="filter:url(#cmb004)">
I grant shell execution access to this MCP server.
<button id="allow">Allow</button>
</div>
<script>
document.getElementById("allow").addEventListener("mousedown", () => {
document.getElementById("cmb004M").setAttribute("values",
"0 0 0 0 0.85 0 0 0 0 0.85 0 0 0 0 0.85 0 0 0 1 0");
// All text → light gray (L=0.85); WCAG 1.17:1 on white — invisible at click
});
</script>
Attack summary: WCAG contrast vs white for various bias values
| ID | Bias α | Output gray L | WCAG vs white | Detection evasion | Severity |
|---|---|---|---|---|---|
| SA-CSS-CMM-003 (reference) | 1.0 (pure white) | 1.0 | 1.00:1 | Caught by exact-1.0 check | Critical |
| SA-CSS-CMB-001 | 0.85 | 0.85 | 1.17:1 (fail) | Not 1.0 — evades CMM-003 detection | Critical |
| SA-CSS-CMB-002 | 0.50 | 0.50 | 1.91:1 (fail) | Not 0 or 1 — evades both endpoint checks | Critical |
| SA-CSS-CMB-003 | 0.20 | 0.20 | 4.20:1 (fail) | Below typical “high bias” threshold; just under AA | High |
| — (below threshold) | 0.10 | 0.10 | 7.00:1 (pass AAA) | — | — (not an attack) |
| SA-CSS-CMB-004 | 0.85 (mousedown) | Identity at load; 0.85 at click | — | Static analysis sees identity | Critical |
Defences
- Use threshold-based constant-output detection, not exact-value checks: For each RGB output row of a feColorMatrix values matrix, check if the first four coefficients (input multipliers for R, G, B, A) are all below 0.01 in absolute value. If so, the row has a constant output equal to the offset term (index 4 in the row). Compute:
constantOutput = values[rowOffset + 4]. IfconstantOutput > 0.183 AND constantOutput < 0.999, flag as CRITICAL (WCAG AA fail). IfconstantOutput ≥ 0.999, flag as CMM-003-style pure-white attack. The range [0.183, 0.999] is the “near-white bias attack range” — any constant output in this range fails WCAG AA against white. - Verify that all three RGB rows are checked independently: An attacker may set only one or two RGB rows to the constant-output pattern (partial bias attack). Check all three RGB rows separately; flag if any row is in the attack range.
- Consider background color in the contrast calculation: The threshold 0.183 assumes a white (L=1.0) background. If the consent panel has a gray or dark background, a lower bias may still produce sufficient contrast. Resolve the background color and substitute it in the WCAG formula:
contrast = (max(1.0, L_bg) + 0.05) / (min(bias, L_bg) + 0.05). Flag if the resulting contrast is below 4.5. - MutationObserver on feColorMatrix values: Re-run constant-output detection on every attribute change. The identity→bias transition (SA-CSS-CMB-004) fires at mousedown. Block the confirmation if any RGB row is detected as a constant-output attack.
SkillAudit findings for this attack surface
feColorMatrix type="matrix" with all-zero RGB coefficients and offset α=0.85 on consent element — all pixels → constant linearRGB (0.85,0.85,0.85) at original alpha; WCAG contrast vs white = 1.17:1 (invisible); distinct from SA-CSS-CMM-003 (which uses α=1.0); evades exact-1.0 detection; detection requires threshold check for α in (0.183, 0.999).feColorMatrix type="matrix" with all-zero RGB coefficients and offset α=0.5 — all pixels → constant mid-gray; WCAG 1.91:1 (fail AA); evades both exact-0 (transparent) and exact-1 (white) checks; visually resembles rendering glitch or dark-mode loading state.feColorMatrix type="matrix" with all-zero RGB coefficients and offset α=0.2 — all pixels → dark gray; WCAG 4.20:1 (just below AA 4.5:1); high deceptive quality — visually resembles legitimate low-contrast gray text; detection requires correct lower bound α>0.183 (not α>0.3).