Security Guide
MCP server SVG feColorMatrix consent security — type="matrix" alpha-zeroing, type="saturate" greyscale badge collapse, type="hueRotate" inversion, and type="luminanceToAlpha" transparent text
The SVG feColorMatrix filter primitive applies a 5×4 matrix transformation to the RGBA channels of each pixel in the filter input. The four type shortcuts — matrix, saturate, hueRotate, and luminanceToAlpha — each expand to a specific matrix form that transforms color space, alpha channel, or both simultaneously. When an MCP server controls the feColorMatrix applied to a consent panel element, it can exploit each type to suppress, invert, or destroy the consent UI’s readability in ways that are invisible to DOM-based audits. All four patterns leave textContent intact and getBoundingClientRect() returning full element dimensions. The filter is only referenced by URL in getComputedStyle(el).filter; the matrix values, type, and resulting per-pixel transformations are only recoverable by traversing the SVG filter graph and reading the feColorMatrix element’s attributes directly.
Attack 1: feColorMatrix type="matrix" with alpha row all zeros — fully transparent consent panel (SA-CSS-FCM-001)
The type="matrix" form applies a full 5×4 color transformation matrix. The matrix is specified as 20 space-separated numbers in row-major order, representing four rows (for R_out, G_out, B_out, A_out) each with five columns (multipliers for R_in, G_in, B_in, A_in, and a constant term). The output alpha is: A_out = M[15]×R + M[16]×G + M[17]×B + M[18]×A + M[19]. An MCP server specifies values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0" — the identity matrix for RGB but with the entire alpha row (indices 15–19) set to zero. The result: R_out = R, G_out = G, B_out = B (colors preserved), A_out = 0 at every pixel.
The consent panel is rendered fully transparent. Every glyph, every color badge, every button label is pixel-perfect in the RGB channels but completely invisible because the alpha channel is zero everywhere. The element’s layout box, DOM text, and interactive children (including the Allow button) are fully preserved. The consent panel background is also erased because the background color is also subject to the filter. The attack is algebraically guaranteed: with A_out coefficient row all zeros, there is no input color value that produces non-zero output alpha. Detection requires reading the 20 matrix values and checking whether indices 15–18 are all zero (or nearly zero) and whether index 19 (the alpha constant term) is also zero.
/* SA-CSS-FCM-001: feColorMatrix type="matrix" with alpha row zeroed
5x4 matrix: [R_out, G_out, B_out, A_out] = M * [R_in, G_in, B_in, A_in, 1]
values="1 0 0 0 0 ← R_out = R_in (identity)
0 1 0 0 0 ← G_out = G_in (identity)
0 0 1 0 0 ← B_out = B_in (identity)
0 0 0 0 0" ← A_out = 0*R + 0*G + 0*B + 0*A + 0 = 0 ALWAYS
Colors preserved; alpha zeroed everywhere — fully transparent consent panel
textContent intact; getBoundingClientRect full size; getComputedStyle blind */
<svg width="0" height="0" style="position:absolute">
<defs>
<filter id="consent-alpha-zero" color-interpolation-filters="sRGB">
<!-- Identity matrix for RGB but alpha row is all zeros
A_out = 0*R + 0*G + 0*B + 0*A + 0 = 0 at every pixel -->
<feColorMatrix type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 0 0"/>
</filter>
</defs>
</svg>
<!-- ATTACK: entire consent panel becomes fully transparent -->
<div id="consent" style="filter:url(#consent-alpha-zero); width:420px; padding:24px">
<h3>Permission Request</h3>
<p>Requesting: shell execution, credential access, filesystem write, network outbound.</p>
<span style="color:red">DANGEROUS: Permanent shell access</span>
<button id="allow-btn">Allow</button>
<button>Deny</button>
</div>
// DOM checks that FAIL:
// document.getElementById('consent').textContent → full text (MISS)
// document.getElementById('consent').getBoundingClientRect() → {w:468, h:140} (MISS)
// getComputedStyle(el).opacity → "1" (MISS)
// getComputedStyle(el).filter → "url(#consent-alpha-zero)" — no matrix detail (MISS)
// --- Detection: read feColorMatrix values and inspect alpha row ---
function detectFeColorMatrix(el) {
const filterVal = getComputedStyle(el).filter || el.getAttribute('filter') || '';
const urlMatch = filterVal.match(/url\(["']?(#[\w-]+)["']?\)/);
if (!urlMatch) return null;
const filterEl = document.querySelector(urlMatch[1]);
if (!filterEl) return null;
const findings = [];
for (const fcm of filterEl.querySelectorAll('feColorMatrix')) {
const type = (fcm.getAttribute('type') || 'matrix').toLowerCase();
const valAttr = fcm.getAttribute('values') || '';
const vals = valAttr.trim().split(/\s+/).map(Number);
// --- type="matrix": inspect the alpha row (indices 15-19) ---
if (type === 'matrix' && vals.length === 20) {
// Alpha row: A_out = vals[15]*R + vals[16]*G + vals[17]*B + vals[18]*A + vals[19]
const alphaInputCoeffs = [vals[15], vals[16], vals[17], vals[18]]; // R,G,B,A multipliers
const alphaConst = vals[19]; // constant term
const allAlphaInputsZero = alphaInputCoeffs.every(v => Math.abs(v) < 0.01);
const alphaConstZero = Math.abs(alphaConst) < 0.01;
if (allAlphaInputsZero && alphaConstZero) {
findings.push({
type,
alphaRow: alphaInputCoeffs.concat(alphaConst),
severity: 'CRITICAL',
note: 'Alpha row all zeros — A_out=0 at every pixel, consent panel fully transparent'
});
} else if (allAlphaInputsZero && alphaConst < 0.1) {
findings.push({
type,
alphaRow: alphaInputCoeffs.concat(alphaConst),
severity: 'HIGH',
note: `Alpha input coefficients all zero; constant term=${alphaConst.toFixed(3)} — max opacity ${(alphaConst * 100).toFixed(1)}%`
});
}
}
// --- type="saturate": flag near-zero saturation ---
if (type === 'saturate') {
const sat = parseFloat(valAttr) || 0;
if (sat < 0.05) {
findings.push({
type,
saturation: sat,
severity: sat === 0 ? 'HIGH' : 'HIGH',
note: `saturate=${sat} — all color hues reduced to luminance-weighted grey; color-coded permission badges indistinguishable`
});
}
}
// --- type="hueRotate": flag large rotations outside cosmetic range ---
if (type === 'huerotate') {
const deg = parseFloat(valAttr) || 0;
const normDeg = ((deg % 360) + 360) % 360; // normalise to [0, 360)
if (normDeg > 10 && normDeg < 350) {
const severity = (normDeg > 150 && normDeg < 210) ? 'CRITICAL' : 'HIGH';
findings.push({
type,
degrees: deg,
normalisedDegrees: normDeg,
severity,
note: `hueRotate=${deg}° — hues shifted by ${normDeg.toFixed(0)}°; consent text color collides with background; ${normDeg >= 170 && normDeg <= 190 ? 'near-180° inversion neutralises any deliberate color scheme' : 'significant hue displacement'}`
});
}
}
// --- type="luminanceToAlpha": always flag on consent elements ---
if (type === 'luminancetoa' || type === 'luminancetoalpha') {
findings.push({
type: 'luminanceToAlpha',
severity: 'CRITICAL',
note: 'luminanceToAlpha: dark consent text (low luminance) → near-transparent; background (high luminance) → opaque black; panel fully illegible'
});
}
}
return findings.length ? findings : null;
}
// detectFeColorMatrix(document.getElementById('consent')) →
// [{
// type: "matrix",
// alphaRow: [0, 0, 0, 0, 0],
// severity: "CRITICAL",
// note: "Alpha row all zeros — A_out=0 at every pixel, consent panel fully transparent"
// }]
CRITICAL — SA-CSS-FCM-001: feColorMatrix type="matrix" with the alpha output row (values indices 15–19) all set to zero produces A_out = 0 at every pixel regardless of input color, luminance, or existing alpha. The RGB channels are optionally preserved (or can also be zeroed), but alpha = 0 makes the entire consent panel fully transparent. All permission scopes, color-coded badges, button labels, and consent prose vanish. The element retains full DOM presence and interactivity. Detection requires reading the 20 values numbers, decomposing index groups 15–18 (alpha input multipliers) and 19 (alpha constant term), and flagging any configuration where all five are zero or near-zero. No legitimate consent panel uses this matrix configuration.
Attack 2: feColorMatrix type="saturate" values="0" — greyscale collapse of color-coded permission badges (SA-CSS-FCM-002)
The type="saturate" shorthand applies a saturation scaling matrix. At values="0", every color is converted to its luminance-weighted greyscale equivalent using the ITU-R BT.709 coefficients: R_grey = G_grey = B_grey = 0.2126×R + 0.7152×G + 0.0722×B. Alpha is preserved unchanged. The attack does not make content invisible in the same absolute sense as alpha-zeroing, but it destroys the color-encoding that consent UIs rely on to communicate permission severity to users.
MCP consent UIs commonly use color-coded permission badges: red badges signal dangerous permissions (shell execution, credential access), green badges signal safe or read-only permissions, and orange badges indicate moderate risk (network access). At saturate=0, all these badges become identical grey values. A red danger badge at hsl(0, 100%, 40%) has luminance L = 0.2126×0.8 + 0.7152×0 + 0.0722×0 = 0.170, producing grey rgb(43, 43, 43). A green safe badge at hsl(120, 60%, 40%) with approximately R=0.26, G=0.64, B=0.26 has luminance L = 0.2126×0.26 + 0.7152×0.64 + 0.0722×0.26 ≈ 0.529, producing rgb(135, 135, 135). Both become different shades of grey — but if the consent text is also grey (as in a dark-mode UI), some combinations of text and background may coincidentally yield near-identical grey values, making text invisible. Additionally, when the consent text color is a warning orange such as #FF6B35 (a common UI color) rendered on a near-white background #FFF5F0, both are likely to produce similar luminance-weighted grey values, causing the warning text to merge with the background.
/* SA-CSS-FCM-002: feColorMatrix type="saturate" values="0"
All colors converted to luminance-weighted greyscale:
R_grey = G_grey = B_grey = 0.2126*R + 0.7152*G + 0.0722*B
Alpha is preserved unchanged.
Color-coded permission badges become identical grey values:
- Red danger badge hsl(0,100%,40%) → L≈0.170 → rgb(43,43,43)
- Green safe badge hsl(120,60%,40%) → L≈0.529 → rgb(135,135,135)
- Orange warning text #FF6B35 → L≈0.281 → rgb(72,72,72)
Users cannot distinguish dangerous from safe permissions by color */
<svg width="0" height="0" style="position:absolute">
<defs>
<filter id="consent-greyscale" color-interpolation-filters="sRGB">
<!-- saturate=0: full desaturation — all hues collapsed to grey -->
<feColorMatrix type="saturate" values="0"/>
</filter>
</defs>
</svg>
<div id="consent-sat" style="filter:url(#consent-greyscale);
background:#fff; color:#222; width:420px; padding:24px">
<h3>Permission Request</h3>
<span style="background:red; color:white; padding:2px 8px">DANGEROUS</span>
<p>Requesting: shell execution, credential access.</p>
<span style="background:green; color:white; padding:2px 8px">SAFE</span>
<p>Requesting: config file read.</p>
<button id="allow-btn">Allow All</button>
<button>Deny</button>
</div>
// After saturation=0 filter:
// The red DANGEROUS badge and the green SAFE badge are now different greys
// A user relying on color to distinguish danger level sees only two grey badges
// The spatial label text is still legible — but if UI relies on color as the PRIMARY
// signal (as many permission UIs do), the attack degrades the security communication
// Worst case: consent text color designed to contrast with background by hue
// e.g., orange warning #FF6B35 on near-white #FFF5F0:
// Orange luminance: 0.2126*(1.0) + 0.7152*(0.419) + 0.0722*(0.208) ≈ 0.527
// Near-white luminance: 0.2126*(1.0) + 0.7152*(0.961) + 0.0722*(0.941) ≈ 0.987
// Grey values: rgb(134,134,134) vs rgb(252,252,252) — still distinguishable
// BUT: at saturation=0.05 (not quite zero), near-colors may coincide
// Saturation=0 additionally blocks color-contrast WCAG checks:
// A red (#ef4444) text on white has contrast ratio ~4.6:1 (WCAG AA)
// After saturation=0, same text is grey on grey — contrast may drop below 3:1
// Detection is handled by detectFeColorMatrix() above
// flag: type="saturate", values near 0 → HIGH finding on any consent element
const findings = detectFeColorMatrix(document.getElementById('consent-sat'));
// findings →
// [{
// type: "saturate",
// saturation: 0,
// severity: "HIGH",
// note: "saturate=0 — all color hues reduced to luminance-weighted grey; color-coded permission badges indistinguishable"
// }]
// Supplementary color-contrast check after simulated desaturation:
function checkPostSaturateContrast(el) {
// Read CSS color of text and background
const style = getComputedStyle(el);
const fgColor = style.color;
const bgColor = style.backgroundColor;
// Compute relative luminance of each (simplified — assumes sRGB)
// If the desaturated luminances are within 1.5:1 contrast ratio → flag
// Full implementation uses canvas pixel sampling after filter rasterisation
return { fgColor, bgColor, note: 'Use canvas rasterisation for post-filter contrast measurement' };
}
HIGH — SA-CSS-FCM-002: feColorMatrix type="saturate" values="0" converts all colors in the consent panel to their luminance-weighted greyscale equivalents, destroying the color-encoded severity distinctions that MCP consent UIs depend on. Red danger badges and green safe badges become different shades of grey; users cannot identify dangerous permissions (shell execution, credential access) from safe ones (config read) by color alone. Additionally, some consent text colors and background colors may coincide in greyscale luminance, causing text to vanish against the background. Detection: read type attribute; flag type="saturate" with values below 0.05 as HIGH on consent elements. No legitimate consent UI uses saturation=0.
Attack 3: feColorMatrix type="hueRotate" values="180" — hue inversion neutralising consent color schemes (SA-CSS-FCM-003)
The type="hueRotate" shorthand rotates the hue of every pixel by the specified number of degrees in the HSL color cylinder. A rotation of 180° is the maximal hue inversion: it maps every color to its complementary hue at the opposite point of the color wheel. Red (0°) becomes cyan (180°). Blue (240°) becomes yellow (60°). Green (120°) becomes magenta (300°). Critically, hue rotation does not change luminance: the output pixel has the same L value as the input, so brightness-based pixel audits and luminance-based contrast ratio checks do not detect the attack. Only explicit hue and color-contrast checks reveal it.
The attack vector is precise: an MCP server that knows the consent panel’s color scheme can choose a hue rotation that maps the text color to the background color (or sufficiently close to it). A consent panel with blue text (#2563EB, hue 225°) on a white background that is subtly tinted yellow (#FBBF24, hue 43°) — a common amber/yellow warning background — receives hueRotate(180): the blue text (hue 225°) becomes yellow (hue 45°), near-identical to the amber background. Text and background are now the same hue family. The contrast ratio collapses. Even without a perfectly calibrated rotation, hueRotate(180) is a brute-force choice that neutralises most carefully designed color schemes because it displaces every hue to its maximum-distance complement, guaranteeing a high probability of color collision with any standard UI background.
/* SA-CSS-FCM-003: feColorMatrix type="hueRotate" values="180"
Rotates all hues by 180° — every color mapped to its complementary hue
Luminance is PRESERVED — brightness-based audits do not detect this attack
Only color-contrast checks with hue awareness can reveal it
Example: consent panel with blue text on amber warning background
Blue text #2563EB → hue 225° → after +180° → hue 45° (yellow/amber)
Amber bg #FBBF24 → hue 43° → unchanged (filter applies to consent text element)
Result: blue text is now yellow against amber background — near-zero contrast */
<svg width="0" height="0" style="position:absolute">
<defs>
<filter id="consent-hueinvert" color-interpolation-filters="sRGB">
<!-- 180° hue rotation: every hue mapped to its complement -->
<feColorMatrix type="hueRotate" values="180"/>
</filter>
</defs>
</svg>
<div style="background:#FBBF24; padding:24px; width:420px">
<!-- Blue text (#2563EB) on amber background with hueRotate(180) applied -->
<div id="consent-hue" style="filter:url(#consent-hueinvert); color:#2563EB">
<h3>Permission Request</h3>
<p>Requesting: shell execution, credential access, persistent network access.</p>
<button id="allow-btn">Allow</button>
<button>Deny</button>
</div>
</div>
// Color transformation analysis for hueRotate(180):
// #2563EB (blue, H=225°, S=84%, L=53%) → after +180°: H=45° (amber/yellow)
// The hue-rotated blue text is now approximately #d4a027 — amber-yellow
// Background is #FBBF24 (amber, H=43°) — extremely close to the rotated text color
// Contrast ratio ≈ 1.05:1 — effectively invisible text
// Additional examples of 180° hue rotation on common MCP consent colors:
// Red danger #EF4444 (H=0°) → Cyan #44EFEF — on white bg: still legible (different)
// Green safe #22C55E (H=142°)→ Magenta #C522A9 — on white bg: still legible (different)
// Orange warn #F97316 (H=25°) → Teal #168CF9 — on white bg: legible
// Blue label #2563EB (H=225°)→ Amber #EBA325 — on amber/yellow bg: INVISIBLE
// The attack is most effective when the MCP server knows the host UI's background color
// and can select a consent text color (via its own HTML) that maps to that background after rotation
// Subtlety: luminance is UNCHANGED by hue rotation
// getImageData pixel brightness values before and after hueRotate are identical
// Only hue-aware contrast ratio check (WCAG 1.4.3 using actual rendered colors) detects this
// Detection: detectFeColorMatrix() above flags hueRotate > 10° on consent elements
// For near-180° rotations (170-190°), severity is CRITICAL due to complement-mapping risk
const el = document.getElementById('consent-hue');
const findings = detectFeColorMatrix(el);
// findings →
// [{
// type: "huerotate",
// degrees: 180,
// normalisedDegrees: 180,
// severity: "CRITICAL",
// note: "hueRotate=180° — hues shifted by 180°; consent text color collides with background; near-180° inversion neutralises any deliberate color scheme"
// }]
// Additional detection: compare pre-filter and post-filter dominant hue of consent text
// If post-filter text hue is within 30° of background hue → flag as color-collision attack
function detectHueCollision(textColor, bgColor, hueRotateDeg) {
function hue(hex) {
const r = parseInt(hex.slice(1,3),16)/255, g = parseInt(hex.slice(3,5),16)/255, b = parseInt(hex.slice(5,7),16)/255;
const max = Math.max(r,g,b), min = Math.min(r,g,b), d = max-min;
if (d === 0) return 0;
let h = max === r ? (g-b)/d % 6 : max === g ? (b-r)/d+2 : (r-g)/d+4;
return ((h*60)+360)%360;
}
const textHueRotated = (hue(textColor) + hueRotateDeg + 360) % 360;
const bgHue = hue(bgColor);
const delta = Math.min(Math.abs(textHueRotated - bgHue), 360 - Math.abs(textHueRotated - bgHue));
return { textHueAfterRotation: textHueRotated.toFixed(0), bgHue: bgHue.toFixed(0), deltaHue: delta.toFixed(0), collision: delta < 30 };
}
// detectHueCollision('#2563EB', '#FBBF24', 180) →
// { textHueAfterRotation: "45", bgHue: "43", deltaHue: "2", collision: true }
CRITICAL — SA-CSS-FCM-003: feColorMatrix type="hueRotate" values="180" inverts all hues to their complementary values without changing luminance, making the attack undetectable by brightness-based pixel audits. An MCP server that knows or controls the consent panel background color can select a consent text color whose 180°-rotated complement collides with that background, causing the consent text to vanish. Blue text (#2563EB, hue 225°) becomes amber (hue 45°), invisible against an amber warning background (#FBBF24). Only color-contrast checks that account for the post-filter rendered color — rather than the CSS color property — reveal the attack. Detection: read type attribute; flag type="hueRotate" with normalised degrees outside 0–10° on consent elements; escalate to CRITICAL for rotations in the 170–190° range.
Attack 4: feColorMatrix type="luminanceToAlpha" — dark consent text becomes nearly transparent (SA-CSS-FCM-004)
The type="luminanceToAlpha"A_out = 0.2126×R_in + 0.7152×G_in + 0.0722×B_in, and R_out = G_out = B_out = 0. This transformation is intended for creating alpha masks from greyscale content for use as in2 sources in subsequent filter primitives, never for direct rendering of text elements.
When applied directly to a consent panel element, the results are consistently catastrophic for readability. Dark consent text (R ≈ G ≈ B ≈ 0.1) has luminance L = 0.2126×0.1 + 0.7152×0.1 + 0.0722×0.1 = 0.1 × 1.0 = 0.1. The dark text pixels become: RGB = (0, 0, 0), alpha = 0.1. Dark text pixels are 10% opaque black. White background pixels (R = G = B = 1.0) have luminance L = 1.0. The background pixels become: RGB = (0, 0, 0), alpha = 1.0. Fully opaque black shapes appear where the background was. The visual result is a complete inversion of the intended rendering: the background becomes opaque black shapes, the dark text becomes nearly transparent black pixels, and the entire consent panel is rendered illegible — a high-contrast noise pattern of black shapes with near-invisible text ghosts.
/* SA-CSS-FCM-004: feColorMatrix type="luminanceToAlpha"
A_out = 0.2126*R + 0.7152*G + 0.0722*B
R_out = G_out = B_out = 0 (all RGB channels zeroed, always)
Effect on dark consent text (R=G=B=0.1):
A_out = 0.2126*0.1 + 0.7152*0.1 + 0.0722*0.1 = 0.1*1.0 = 0.1
→ Text pixels: black at 10% opacity — nearly transparent
Effect on white background (R=G=B=1.0):
A_out = 0.2126*1 + 0.7152*1 + 0.0722*1 = 1.0
→ Background pixels: black at 100% opacity — fully opaque black
Combined: consent panel inverted — background becomes black shapes,
text becomes near-transparent ghosts — entirely illegible */
<svg width="0" height="0" style="position:absolute">
<defs>
<filter id="consent-lum2alpha" color-interpolation-filters="sRGB">
<!-- luminanceToAlpha: the ONLY legal use is as an intermediate alpha mask source
When applied to a consent element directly, it destroys legibility -->
<feColorMatrix type="luminanceToAlpha"/>
</filter>
</defs>
</svg>
<div id="consent-l2a" style="filter:url(#consent-lum2alpha);
background:#ffffff; color:#111111; width:420px; padding:24px">
<h3>Permission Request</h3>
<p>Requesting: shell execution, credential access, filesystem write, network outbound.</p>
<span style="background:#ef4444; color:white">CRITICAL PERMISSION</span>
<button id="allow-btn">Allow</button>
<button>Deny</button>
</div>
// Pixel-by-pixel analysis of luminanceToAlpha on a standard consent panel:
//
// Color R G B → Luminance → A_out RGB_out
// White background 1.000 1.000 1.000 → 1.000 → 1.0 (0,0,0) black, fully opaque
// Near-white bg tint 0.980 0.980 0.985 → 0.980 → 0.98 (0,0,0) near-opaque black
// Dark text #111 0.067 0.067 0.067 → 0.067 → 0.067 (0,0,0) black, 6.7% opaque
// Black text #000 0.000 0.000 0.000 → 0.000 → 0.0 (0,0,0) fully transparent
// Red badge #EF4444 0.937 0.267 0.267 → 0.280 → 0.28 (0,0,0) 28% opaque black
// White badge text #FFF 1.000 1.000 1.000 → 1.000 → 1.0 (0,0,0) opaque black
//
// User sees: black shapes (ex-backgrounds), near-transparent smears (ex-text), noise pattern
// The "CRITICAL PERMISSION" red badge text is now an irregular black smear — unreadable
// luminanceToAlpha is NEVER a legitimate filter on a consent element:
// - Its only valid use is as an intermediate node producing an alpha mask for a subsequent
// feComposite in= reference — never as the final output of the filter graph
// - Any feColorMatrix type="luminanceToAlpha" as the last (or only) primitive in a filter
// applied to a consent element is CRITICAL regardless of other parameters
// Detection: flag any type="luminanceToAlpha" on a consent element — no valid use case
const el = document.getElementById('consent-l2a');
const findings = detectFeColorMatrix(el);
// findings →
// [{
// type: "luminanceToAlpha",
// severity: "CRITICAL",
// note: "luminanceToAlpha: dark consent text (low luminance) → near-transparent; background (high luminance) → opaque black; panel fully illegible"
// }]
// Further check: whether luminanceToAlpha is used as final output vs intermediate mask
function isLuminanceToAlphaFinalOutput(filterEl) {
const primitives = Array.from(filterEl.children);
const lum2alpha = primitives.filter(p => p.tagName.toLowerCase() === 'fecolormatrix'
&& (p.getAttribute('type') || '').toLowerCase() === 'luminancetoalpha');
for (const lum of lum2alpha) {
const resultId = lum.getAttribute('result');
// Check if any later primitive references this result as "in" or "in2"
const usedByLater = primitives.some(p => {
const idx = primitives.indexOf(p);
return idx > primitives.indexOf(lum)
&& (p.getAttribute('in') === resultId || p.getAttribute('in2') === resultId);
});
if (!usedByLater) return true; // luminanceToAlpha result is unused → it IS the final output
}
return false;
}
CRITICAL — SA-CSS-FCM-004: feColorMatrix type="luminanceToAlpha" replaces each pixel’s alpha with its luminance and zeros all RGB channels. Dark consent text (L ≈ 0.1) becomes 10%-opaque black pixels; white background pixels (L = 1.0) become fully opaque black shapes. The consent panel is rendered as illegible inverted noise: the background appears as opaque black shapes and the text appears as near-transparent ghost pixels. There is no legitimate use for luminanceToAlpha as the final output of a filter applied to a consent element — its only valid SVG use is as an intermediate alpha mask source. Detection: flag any feColorMatrix type="luminanceToAlpha" on a consent element as CRITICAL; additionally check whether the result is referenced by a subsequent primitive (intermediate use) or is the final filter output (attack).
Summary table
| Attack | Mechanism | What it hides | Severity |
|---|---|---|---|
SA-CSS-FCM-001: type="matrix" alpha row zeroed |
5×4 matrix with indices 15–19 all zero: A_out = 0×R + 0×G + 0×B + 0×A + 0 = 0 at every pixel; RGB preserved; alpha = 0 everywhere; consent panel fully transparent; algebraically guaranteed | Entire consent panel — all permission scopes, color badges, button labels, consent prose; element fully interactive in DOM with full-size layout box | Critical |
SA-CSS-FCM-002: type="saturate" values="0" |
All hues converted to ITU-R BT.709 luminance-weighted grey: R=G=B=0.2126R+0.7152G+0.0722B; alpha preserved; color-coded permission badges become different grey shades; red danger and green safe badges visually indistinct; some text/background color pairs may coincide in greyscale | Color-coding of permission severity (red/green/orange badges); deliberate contrast between warning text and background designed around hue differences; WCAG color-contrast ratios for hue-dependent designs | High |
SA-CSS-FCM-003: type="hueRotate" values="180" |
All hues displaced by 180°: every color maps to its complementary hue; luminance unchanged (brightness audits miss this); blue consent text becomes amber, colliding with amber backgrounds; attack calibrated to any known background color; only color-contrast checks using actual post-filter colors detect it | Any consent text whose post-rotation hue collides with the background; color-scheme integrity; WCAG color-contrast ratios; permission severity signaling via hue | Critical |
SA-CSS-FCM-004: type="luminanceToAlpha" |
A_out = luminance; R=G=B=0; dark text (L≈0.1) becomes 10%-opaque black; white background (L=1.0) becomes fully opaque black; entire panel rendered as illegible inverted noise; no valid consent-element use; final-output detection distinguishes from legitimate intermediate mask use | All consent text legibility; entire consent panel visual meaning; background and text relationship is inverted to produce unreadable noise pattern | Critical |
Defences
- Traverse the SVG filter graph and read every
feColorMatrixelement on consent elements —getComputedStyle(el).filteryields only the filter URL; resolve it to the<filter>element viadocument.querySelector(filterId); iterate all childfeColorMatrixprimitives; read both thetypeattribute and thevaluesattribute for each; this traversal is mandatory because CSSOM does not expose any of these values. - For
type="matrix": parse all 20 values and inspect the alpha output row — extract indices 15–19; if all five are zero or near-zero (absolute value below 0.01), flag CRITICAL; if the alpha-input coefficients (15–18) are all zero but the constant term (19) is below 0.1, flag HIGH with the effective maximum opacity; flag any matrix configuration that reduces the original alpha channel coefficient (index 18) below 0.5 as MEDIUM. - Flag
type="saturate"with values below 0.05 on consent elements as HIGH — saturation values near zero destroy color-encoded permission severity distinctions; additionally probe rendered pixel colors using canvas rasterisation to detect coincidental greyscale collisions between text and background colors after desaturation. - Flag
type="hueRotate"with normalised values outside 0–10° on consent elements — escalate to CRITICAL for rotations in the 170–190° range (near-complement mapping); perform a post-rotation hue collision check between consent text color and background color; flag any hue delta below 30° between rotated text and background as a color-collision consent attack. - Flag any
type="luminanceToAlpha"on consent elements as CRITICAL with no exceptions — this type has no legitimate use as the final output of a filter applied to a consent element; additionally check whether theluminanceToAlpharesult is referenced by a later primitive (intermediate use, possibly legitimate) or is the terminal output (attack); intermediate use on consent elements still warrants review. - Enforce a feColorMatrix type allowlist on consent elements — consent panels should use no color-transforming filters at all; if a
feColorMatrixis detected, it should be one of a small set of cosmetic-only types with tightly bounded value ranges; anytype="matrix",type="saturate"below 0.8,type="hueRotate"beyond 10°, ortype="luminanceToAlpha"on a consent element should block rendering until explicitly approved.
SkillAudit findings for this attack surface
feColorMatrix type="matrix" with alpha output row (values indices 15–19) all zeros — A_out = 0 at every pixel regardless of input; consent panel fully transparent; colors preserved in RGB channels but invisible at alpha=0; textContent and getBoundingClientRect() intact; detection requires reading all 20 matrix values and checking the alpha row; algebraically guaranteed zero-alpha output across all browsers.feColorMatrix type="saturate" values="0" — all colors converted to luminance-weighted greyscale using ITU-R BT.709 coefficients; color-coded permission severity badges (red=dangerous, green=safe) become different grey shades indistinguishable by hue; some text/background combinations may coincide in greyscale luminance causing text to vanish; detection requires reading type and values attributes from every feColorMatrix in the filter graph.feColorMatrix type="hueRotate" values="180" — all hues displaced to their complementary positions without changing luminance; blue consent text (#2563EB) rotated to amber (hue 45°), invisible against amber warning backgrounds (#FBBF24); luminance-based pixel audits do not detect this attack; only hue-aware color-contrast checks reveal the post-filter color collision; flag normalised rotations in the 170–190° range as CRITICAL.feColorMatrix type="luminanceToAlpha" — A_out replaces each pixel’s alpha with its luminance; RGB zeroed; dark consent text (L≈0.1) becomes 10%-opaque black; white background (L=1.0) becomes fully opaque black; consent panel rendered as inverted illegible noise; no legitimate final-output use on consent elements; detection: flag any type="luminanceToAlpha" as CRITICAL and check whether the result is used as an intermediate mask or is the filter’s terminal output.