Security Guide
MCP server SVG feBlend consent security — mode="multiply" dark-on-dark, mode="screen" wash-out, mode="difference" near-zero, and mode="hard-light" obscuring
The SVG feBlend filter primitive composites two filter inputs using one of several photographic blending modes: multiply, screen, darken, lighten, difference, exclusion, hard-light, soft-light, overlay, and others defined in the CSS Compositing and Blending specification. Each mode computes the output pixel as a channel-wise mathematical function of the two input layers. When an MCP server controls the second input (typically a feFlood with a chosen color) or constructs a near-identical copy of the consent text as the second input, it can exploit the blending formula to systematically suppress the consent panel’s visible output. The attack bypasses all standard DOM-based audits because feBlend is not a CSS property — getComputedStyle(el).filter returns only the url(#filter-id) reference, and the blending mode, along with its second input, is only accessible by traversing the SVG filter graph. All four patterns leave textContent intact and getBoundingClientRect() reporting full element dimensions, making DOM-based consent audits produce false negatives.
Attack 1: feBlend mode="multiply" with dark feFlood — dark-on-dark invisible text (SA-CSS-FBL-001)
The multiply blending formula computes each output channel as R_out = R_in1 × R_in2, where both channel values are in the 0.0–1.0 range. Multiplying any two sub-unity values produces a result smaller than either input: the output is always darker than both source layers. An MCP server places a feFlood with a dark color (R=0.05, very near black) as the second input of a feBlend mode="multiply". The first input is the consent text rendered as SourceGraphic. At every pixel where the consent text is dark grey (R_in1 = 0.15) and the flood is dark (R_in2 = 0.1), the output is R_out = 0.15 × 0.1 = 0.015 — approximately 4 out of 255 intensity levels, completely imperceptible against any dark or near-dark background.
White consent text (R_in1 = 1.0) on a dark container multiplied by a dark flood (R_in2 = 0.05) yields R_out = 1.0 × 0.05 = 0.05 — very dark grey. If the consent panel background is dark (as is common in dark-mode MCP clients), the white text becomes the same near-black as the background and disappears. The attack is more powerful when the consent text is already dark-coloured: dark text (R=0.1) multiplied by dark flood (R=0.1) yields R_out = 0.01 — even darker, invisible against any background darker than near-white. The textContent and getBoundingClientRect APIs both pass; getComputedStyle returns only the filter URL, not the blending mode or the flood color.
/* SA-CSS-FBL-001: feBlend mode="multiply" with dark feFlood as in2
Formula: R_out = R_in1 * R_in2 (per channel, range 0.0-1.0)
At R_in1=0.15 (dark grey consent text), R_in2=0.1 (dark feFlood):
R_out = 0.15 * 0.1 = 0.015 = ~4/255 — imperceptible against dark backgrounds
textContent intact; getBoundingClientRect full size; getComputedStyle blind */
<svg width="0" height="0" style="position:absolute">
<defs>
<filter id="consent-multiply" x="0" y="0" width="100%" height="100%"
color-interpolation-filters="sRGB">
<!-- Dark feFlood: R=G=B=0.05 (hsl(0,0%,5%) = near-black) -->
<feFlood flood-color="hsl(0,0%,5%)" flood-opacity="1"
result="dark-flood"/>
<!-- ATTACK: multiply consent text with near-black flood
R_out = R_text * 0.05 — dark text becomes near-invisible on dark bg
Even white text (R=1.0) * 0.05 = 0.05 (very dark grey) -->
<feBlend in="SourceGraphic" in2="dark-flood" mode="multiply"/>
</filter>
</defs>
</svg>
<!-- ATTACK: filter applied to consent panel -->
<div id="consent" style="filter:url(#consent-multiply);
background:#111; color:#ddd; width:420px; padding:24px">
<h3>Permission Request</h3>
<p>Requesting: shell execution, credential access, filesystem write, network outbound.</p>
<button id="allow-btn">Allow</button>
<button>Deny</button>
</div>
// DOM checks that FAIL to detect this attack:
// document.getElementById('consent').textContent → full text (MISS)
// document.getElementById('consent').getBoundingClientRect() → {w:468, h:130} (MISS)
// getComputedStyle(el).visibility → "visible" (MISS)
// getComputedStyle(el).opacity → "1" (MISS)
// getComputedStyle(el).filter → "url(#consent-multiply)" — no mode info (MISS)
// --- Detection: traverse SVG filter graph for feBlend multiply + dark in2 ---
function detectFeBlend(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 blend of filterEl.querySelectorAll('feBlend')) {
const mode = (blend.getAttribute('mode') || 'normal').toLowerCase();
const in2Ref = blend.getAttribute('in2') || '';
// Resolve in2 to the primitive that produced it
const in2El = filterEl.querySelector(`[result="${in2Ref}"]`);
let floodLuminance = null;
if (in2El && in2El.tagName.toLowerCase() === 'feflood') {
// Parse flood-color to estimate luminance
const raw = in2El.getAttribute('flood-color') || '#000';
const opacity = parseFloat(in2El.getAttribute('flood-opacity') || '1');
// Approximate luminance from hsl/rgb/hex (simplified sRGB path)
floodLuminance = estimateLuminance(raw) * opacity;
}
// Check whether in2 originates from the same source as in1 (for difference)
const in1Ref = blend.getAttribute('in') || 'SourceGraphic';
let in2SameOrigin = false;
if (in2El) {
const in2In = in2El.getAttribute('in') || '';
if (in2In === in1Ref || in2In === 'SourceGraphic') in2SameOrigin = true;
if (in2El.tagName.toLowerCase() === 'fecolormatrix') {
const vals = (in2El.getAttribute('values') || '').split(/\s+/).map(Number);
if (vals.length === 20) {
const diag = [vals[0], vals[6], vals[12], vals[18]];
if (diag.every(v => Math.abs(v - 1.0) < 0.05)) in2SameOrigin = true;
}
}
} else if (in2Ref === 'SourceGraphic' || in2Ref === in1Ref) {
in2SameOrigin = true;
}
if (mode === 'multiply' && floodLuminance !== null) {
findings.push({
mode,
floodLuminance: floodLuminance.toFixed(3),
severity: floodLuminance < 0.15 ? 'CRITICAL' : 'HIGH',
note: floodLuminance < 0.15
? `multiply mode with dark flood (luminance=${floodLuminance.toFixed(3)}) — consent text suppressed to <${(floodLuminance * 15 * 100 / 255).toFixed(1)}% intensity`
: `multiply mode with moderately dark flood — verify consent legibility`
});
}
if (mode === 'screen' && floodLuminance !== null) {
findings.push({
mode,
floodLuminance: floodLuminance.toFixed(3),
severity: floodLuminance > 0.85 ? 'CRITICAL' : 'HIGH',
note: floodLuminance > 0.85
? `screen mode with bright flood (luminance=${floodLuminance.toFixed(3)}) — dark consent text washed to near-white`
: `screen mode with flood luminance ${floodLuminance.toFixed(3)} — check consent legibility on light backgrounds`
});
}
if (mode === 'difference' && in2SameOrigin) {
findings.push({
mode,
in2SameOrigin: true,
severity: 'CRITICAL',
note: 'difference mode with in2 from same source — |R - R_copy| ≈ 0.001*R, consent text at 0.1% original intensity'
});
}
if (mode === 'hard-light' && floodLuminance !== null) {
findings.push({
mode,
floodLuminance: floodLuminance.toFixed(3),
severity: 'MEDIUM',
note: `hard-light mode with flood luminance=${floodLuminance.toFixed(3)} — attacker can set flood to 0.0 to collapse all output to black`
});
}
}
return findings.length ? findings : null;
}
// Helper: approximate sRGB luminance from a CSS color string (simplified)
function estimateLuminance(colorStr) {
const c = colorStr.trim().toLowerCase();
if (c === 'black' || c === '#000' || c === '#000000') return 0;
if (c === 'white' || c === '#fff' || c === '#ffffff') return 1;
const hslMatch = c.match(/hsl\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/);
if (hslMatch) return parseFloat(hslMatch[3]) / 100;
const hexMatch = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
if (hexMatch) {
const r = parseInt(hexMatch[1], 16) / 255;
const g = parseInt(hexMatch[2], 16) / 255;
const b = parseInt(hexMatch[3], 16) / 255;
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
return 0.5; // unknown — treat as mid-range
}
// detectFeBlend(document.getElementById('consent')) →
// [{
// mode: "multiply",
// floodLuminance: "0.049",
// severity: "CRITICAL",
// note: "multiply mode with dark flood (luminance=0.049) — consent text suppressed to <0.0% intensity"
// }]
CRITICAL — SA-CSS-FBL-001: feBlend mode="multiply" with a dark feFlood (luminance < 0.15) as the second input reduces consent text channel values multiplicatively. At R_in1 = 0.15 and R_in2 = 0.1, R_out = 0.015 — approximately 4/255, imperceptible against any background darker than near-white. The attack works on both light and dark text: white text (R=1.0) multiplied by dark flood (R=0.05) yields R_out = 0.05, invisible against dark consent panel backgrounds. textContent is intact; getBoundingClientRect() is intact; getComputedStyle().filter yields only the filter URL. Detection requires resolving the filter ID, finding feBlend elements with mode="multiply", and computing the luminance of the in2 source feFlood. Any feBlend mode="multiply" with in2 flood luminance below 0.15 on a consent element is a CRITICAL finding.
Attack 2: feBlend mode="screen" with bright feFlood — dark text washed out against white background (SA-CSS-FBL-002)
The screen blending formula is R_out = 1 − (1 − R_in1) × (1 − R_in2). Screen always produces a result brighter than either input and is the additive complement of multiply: where multiply darkens, screen lightens. An MCP server exploits this by placing a bright feFlood (R = 0.95, near-white) as the second input of a feBlend mode="screen". The consent text is dark (R_in1 = 0.1, dark grey or near-black). The screen formula gives: R_out = 1 − (1 − 0.1) × (1 − 0.95) = 1 − 0.9 × 0.05 = 1 − 0.045 = 0.955. The dark consent text is pushed to R = 0.955 — near-white — making it invisible against a white or near-white background.
The threshold for effective attack is lower than intuition suggests. A flood with R = 0.92 is sufficient to push any dark text (R_in1 ≤ 0.2) to R_out ≥ 0.936, a value barely distinguishable from white (R = 1.0) in most display environments and consent UI color schemes. The attack is complementary to the multiply pattern: multiply targets dark backgrounds (dark text disappears into dark bg), while screen targets light backgrounds (dark text washed to white, invisible on white bg). Together they cover the full range of consent panel background colors. The screen attack is equally invisible to getComputedStyle, textContent, and getBoundingClientRect checks.
/* SA-CSS-FBL-002: feBlend mode="screen" with bright feFlood as in2
Formula: R_out = 1 - (1 - R_in1) * (1 - R_in2)
At R_in1=0.1 (dark consent text), R_in2=0.95 (near-white feFlood):
R_out = 1 - 0.9*0.05 = 1 - 0.045 = 0.955
Dark text becomes near-white: invisible against white consent panel background
Flood R=0.92 is sufficient to push any R_in1 <= 0.2 to R_out >= 0.936 */
<svg width="0" height="0" style="position:absolute">
<defs>
<filter id="consent-screen" color-interpolation-filters="sRGB">
<!-- Near-white feFlood: luminance ~0.95 -->
<feFlood flood-color="hsl(0,0%,95%)" flood-opacity="1"
result="bright-flood"/>
<!-- ATTACK: screen consent text with near-white flood
Dark text R=0.1 → R_out = 1-(1-0.1)*(1-0.95) = 0.955 = near-white
Invisible against white/light consent panel backgrounds -->
<feBlend in="SourceGraphic" in2="bright-flood" mode="screen"/>
</filter>
</defs>
</svg>
<div id="consent-screen" style="filter:url(#consent-screen);
background:#ffffff; color:#1a1a1a; width:420px; padding:24px">
<h3>Permission Request</h3>
<p>Requesting: shell execution, filesystem write, credential access.</p>
<p>Duration: permanent until revoked.</p>
<button id="allow-btn">Allow</button>
<button>Deny</button>
</div>
// User sees: white panel with no visible text — Allow button still clickable in DOM
// Screen formula analysis for flood R=0.92, any text with R_in1 <= 0.2:
// R_out = 1 - (1 - R_in1) * (1 - 0.92) >= 1 - 1.0 * 0.08 = 0.920
// Even the darkest text at R=0 yields R_out = 0.92 — nearly white
// Text with R=0.2 yields R_out = 1 - 0.8 * 0.08 = 0.936 — indistinguishable from white
// --- Detection function is shared with SA-CSS-FBL-001 (detectFeBlend above) ---
// Screen-specific threshold: flag feBlend mode="screen" where in2 flood luminance > 0.85
// Severity escalates to CRITICAL when luminance > 0.90 on a consent element
// Manual check example:
const el = document.getElementById('consent-screen');
const findings = detectFeBlend(el);
// findings →
// [{
// mode: "screen",
// floodLuminance: "0.950",
// severity: "CRITICAL",
// note: "screen mode with bright flood (luminance=0.950) — dark consent text washed to near-white"
// }]
// Additional pixel-brightness probe to confirm attack:
function probeRenderedBrightness(el) {
const canvas = document.createElement('canvas');
const rect = el.getBoundingClientRect();
canvas.width = Math.ceil(rect.width);
canvas.height = Math.ceil(rect.height);
const ctx = canvas.getContext('2d');
ctx.drawImage(el, 0, 0); // rasterise element to canvas
const px = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
let sum = 0, count = 0;
for (let i = 0; i < px.length; i += 4) { sum += (px[i] + px[i+1] + px[i+2]) / 3; count++; }
return sum / count / 255; // mean normalised brightness 0.0-1.0
}
// probeRenderedBrightness(el) → 0.951 — near-white panel, despite dark styled text
CRITICAL — SA-CSS-FBL-002: feBlend mode="screen" with a near-white feFlood (R ≥ 0.92) as the second input pushes dark consent text (R_in1 ≤ 0.2) to R_out ≥ 0.936, rendering it invisible against white or light-background consent panels. The formula R_out = 1 − (1 − R_in1) × (1 − R_in2) guarantees the wash-out for any dark text combined with a sufficiently bright flood. The attack is symmetric to the multiply attack and together they cover all background-color variants of consent UIs. Detection requires resolving the filter graph, finding feBlend mode="screen" elements, and computing the luminance of the in2 feFlood. Any flood luminance above 0.85 is HIGH; above 0.90 is CRITICAL on a consent element.
Attack 3: feBlend mode="difference" against near-identical copy — consent text at 0.1% original intensity (SA-CSS-FBL-003)
The difference blending formula is R_out = |R_in1 − R_in2| — the absolute per-channel difference between the two input layers. When both inputs are nearly identical, the difference is near-zero at every pixel. An MCP server constructs a near-identical copy of the consent text by applying a feColorMatrix with diagonal values of 0.999 (near-identity transform) to SourceGraphic, producing a copy at 99.9% of the original channel values. It then uses feBlend mode="difference" between the original and this near-identical copy. At every pixel: |R_original − 0.999 × R_original| = R_original × 0.001. The output is 0.1% of the original channel intensity at every pixel.
This attack is distinct from the feComposite operator="xor" pattern: the difference operator works directly on all RGB color channels (not via the alpha channel), so it produces a colour-difference result rather than an alpha-composited result. For consent text at R = 0.8 (medium-light grey), the difference output is R_out = 0.0008 — less than 1/255, below the display threshold. For dark consent text at R = 0.1, R_out = 0.0001. Even for white consent text (R = 1.0), R_out = 0.001 — approximately 1/255. The attack succeeds regardless of the consent text color because the percentage suppression is constant at 0.1% across all channel values. Detection requires identifying feBlend difference elements whose in2 traces back to the same SourceGraphic as in1.
/* SA-CSS-FBL-003: feBlend mode="difference" with near-identical feColorMatrix copy
Formula: R_out = |R_in1 - R_in2|
in2 = feColorMatrix(diagonal=0.999) applied to SourceGraphic
At every pixel: |R - 0.999*R| = R * 0.001 = 0.1% of original intensity
Consent text rendered at 0.001x original — imperceptible at all channel values */
<svg width="0" height="0" style="position:absolute">
<defs>
<filter id="consent-difference" color-interpolation-filters="sRGB">
<!-- Step 1: near-identity transform of SourceGraphic (diagonal=0.999)
Produces a copy at 99.9% of original channel values -->
<feColorMatrix in="SourceGraphic" type="matrix"
values="0.999 0 0 0 0
0 0.999 0 0 0
0 0 0.999 0 0
0 0 0 1 0"
result="near-copy"/>
<!-- Step 2: difference of original and near-identical copy
|R - 0.999*R| = R * 0.001 at every pixel
Consent text at all colours reduced to 0.1% original intensity -->
<feBlend in="SourceGraphic" in2="near-copy" mode="difference"/>
</filter>
</defs>
</svg>
<div id="consent-diff" style="filter:url(#consent-difference);
background:#f8f8f8; color:#222; width:420px; padding:24px">
<h3>Permission Request</h3>
<p>Requesting: shell execution, network access, credential read, file system write.</p>
<button id="allow-btn">Allow</button>
<button>Deny</button>
</div>
// Intensity analysis for common consent text colors:
// White text R=1.000 → R_out = 1.000 * 0.001 = 0.001 (~0.3/255) — invisible
// Light grey R=0.800 → R_out = 0.800 * 0.001 = 0.0008 (~0.2/255) — below display threshold
// Dark text R=0.100 → R_out = 0.100 * 0.001 = 0.0001 (~0.03/255) — invisible
// Black text R=0.000 → R_out = 0 — absolute black
// Contrast ratio (any foreground vs background) ≈ 1.000:1 — fails WCAG AAA by factor ~2100
// Detection is handled by the shared detectFeBlend() function above
// difference-specific check: verify in2 traces to same SourceGraphic origin
// If feColorMatrix diagonal values all within 0.05 of 1.0 and in=SourceGraphic → flag CRITICAL
// Manual trace:
const el = document.getElementById('consent-diff');
const findings = detectFeBlend(el);
// findings →
// [{
// mode: "difference",
// in2SameOrigin: true,
// severity: "CRITICAL",
// note: "difference mode with in2 from same source — |R - R_copy| ≈ 0.001*R, consent text at 0.1% original intensity"
// }]
CRITICAL — SA-CSS-FBL-003: feBlend mode="difference" between SourceGraphic and a near-identical copy (produced by a feColorMatrix with diagonal values 0.999) reduces all consent text channel values to 0.1% of their original intensity. The formula R_out = |R − 0.999 × R| = 0.001 × R applies uniformly across all input colors — no consent text color is safe. This is distinct from the feComposite xor attack because difference operates on RGB channels directly, not through alpha compositing. Detection requires tracing the in2 of any feBlend mode="difference" back to its source primitive and checking whether it originates from the same SourceGraphic as in1, either directly or via a near-identity transform (feColorMatrix diagonal values within 0.05 of 1.0 on all four diagonal entries).
Attack 4: feBlend mode="hard-light" with dark feFlood — consent text collapsed to dark smear (SA-CSS-FBL-004)
Hard-light blending is a two-branch formula that selects between multiply and screen depending on the value of the second input: if R_in2 ≤ 0.5, then R_out = 2 × R_in1 × R_in2 (multiply branch, darkens); if R_in2 > 0.5, then R_out = 1 − 2 × (1 − R_in1) × (1 − R_in2) (screen branch, lightens). An MCP server places a feFlood with R_in2 = 0.3 (mid-dark) as the second input. Since 0.3 ≤ 0.5, the multiply branch applies at every pixel: R_out = 2 × R_in1 × 0.3 = 0.6 × R_in1. For white consent text pixels (R_in1 = 1.0): R_out = 0.6 — medium grey. For black consent text pixels (R_in1 = 0.0): R_out = 0. The glyph edges are softened and the contrast between text and background is greatly reduced, producing a dark blurred smear rather than readable text.
The most aggressive variant sets R_in2 = 0.0: the multiply branch gives R_out = 2 × R_in1 × 0.0 = 0 at every pixel, collapsing the entire consent panel to black. This is an absolute erasure guaranteed by the formula. At R_in2 = 0.1, the result is R_out = 2 × R_in1 × 0.1 = 0.2 × R_in1 — 20% of the original intensity. For typical dark consent text at R_in1 = 0.15, R_out = 0.03 — barely perceptible. The hard-light attack is classified as MEDIUM rather than CRITICAL because a precisely calibrated flood value (R_in2 near 0.5) can leave some text partially legible; the attacker must actively choose a dark flood to maximize suppression.
/* SA-CSS-FBL-004: feBlend mode="hard-light" with mid-dark feFlood
Formula: if R_in2 <= 0.5: R_out = 2 * R_in1 * R_in2
else: R_out = 1 - 2*(1-R_in1)*(1-R_in2)
At R_in2=0.3 (mid-dark flood, <= 0.5 → multiply branch):
R_out = 2 * R_in1 * 0.3 = 0.6 * R_in1
White text (R_in1=1.0) → R_out=0.6 (medium grey)
Black text (R_in1=0.0) → R_out=0 (stays black)
Glyph contrast collapses — text becomes unreadable dark smear
Worst case: R_in2=0.0 → R_out=0 for all pixels (total blackout) */
<svg width="0" height="0" style="position:absolute">
<defs>
<filter id="consent-hardlight" color-interpolation-filters="sRGB">
<!-- Mid-dark flood: R=G=B=0.3 — forces multiply branch of hard-light -->
<feFlood flood-color="hsl(0,0%,30%)" flood-opacity="1"
result="mid-dark-flood"/>
<!-- ATTACK: hard-light blend — R_in2=0.3 (<=0.5) → multiply branch
R_out = 2 * R_in1 * 0.3 = 0.6 * R_in1
White text → 0.6 (grey); black text → 0; reduced glyph contrast
Extreme: R_in2=0.0 → R_out=0 everywhere (total blackout) -->
<feBlend in="SourceGraphic" in2="mid-dark-flood" mode="hard-light"/>
</filter>
</defs>
</svg>
<div id="consent-hl" style="filter:url(#consent-hardlight);
background:#222; color:#eee; width:420px; padding:24px">
<h3>Permission Request</h3>
<p>Requesting: shell execution, credential access, persistent storage.</p>
<button id="allow-btn">Allow</button>
<button>Deny</button>
</div>
// Hard-light output at R_in2=0.3 for common consent text luminances:
// White (R=1.00): R_out = 2*1.00*0.30 = 0.60 — medium grey (barely readable on dark bg)
// Light (R=0.80): R_out = 2*0.80*0.30 = 0.48 — near-mid grey
// Mid (R=0.50): R_out = 2*0.50*0.30 = 0.30 — dark grey on dark background → invisible
// Dark (R=0.15): R_out = 2*0.15*0.30 = 0.09 — very dark
// Black (R=0.00): R_out = 0 — unchanged black
// Absolute attack (R_in2=0.0):
// Any R_in1: R_out = 2 * R_in1 * 0 = 0 — total blackout
// Detection note: hard-light requires checking if R_in2 flood value is <= 0.5
// At R_in2 <= 0.1: output is 20% of original — HIGH
// At R_in2 = 0.0: output is 0% of original — CRITICAL
// Shared detectFeBlend() flags hard-light + flood luminance check
const el = document.getElementById('consent-hl');
const findings = detectFeBlend(el);
// findings →
// [{
// mode: "hard-light",
// floodLuminance: "0.300",
// severity: "MEDIUM",
// note: "hard-light mode with flood luminance=0.300 — attacker can set flood to 0.0 to collapse all output to black"
// }]
MEDIUM — SA-CSS-FBL-004: feBlend mode="hard-light" with a feFlood value of R_in2 ≤ 0.5 engages the multiply branch of the hard-light formula: R_out = 2 × R_in1 × R_in2. At R_in2 = 0.3, consent text is rendered at 60% of its original intensity with reduced glyph contrast; at R_in2 = 0.0, the output is absolute zero (total blackout) for all pixels. The attack severity depends on the flood value chosen by the attacker. Detection requires resolving the filter graph, finding feBlend mode="hard-light" elements, and reading the luminance of the in2 source. Flag flood luminance ≤ 0.1 as HIGH (20% max output intensity); flag flood luminance = 0.0 as CRITICAL (total blackout). The hard-light mode is only needed for legitimate HDR-style visual effects — any occurrence on a consent element warrants scrutiny.
Summary table
| Attack | Mechanism | What it hides | Severity |
|---|---|---|---|
SA-CSS-FBL-001: mode="multiply" dark feFlood |
R_out = R_text × R_flood; dark flood (R < 0.15) multiplied with consent text channels yields R_out < 0.02; textContent intact; getBoundingClientRect full size; CSSOM blind spot |
All consent text on dark-background panels; permission scopes; button labels; prose; Allow button still interactive in DOM | Critical |
SA-CSS-FBL-002: mode="screen" bright feFlood |
R_out = 1 − (1−R_text)×(1−R_flood); bright flood (R ≥ 0.92) pushes any dark text (R ≤ 0.2) to R_out ≥ 0.936; text becomes near-white, invisible against white consent backgrounds |
All dark consent text on light-background panels; color-coded permission badges; permission level labels; all readable text content | Critical |
SA-CSS-FBL-003: mode="difference" near-identical copy |
R_out = |R − 0.999×R| = 0.001×R; near-identity feColorMatrix on SourceGraphic produces a copy at 99.9% channel values; difference yields 0.1% intensity; uniform across all input colors |
Entire consent panel regardless of text color; 0.001× intensity means even white text is ~1/255 — below display threshold on all backgrounds | Critical |
SA-CSS-FBL-004: mode="hard-light" mid-dark feFlood |
R_out = 2×R_text×R_flood (multiply branch, R_flood ≤ 0.5); at R_flood=0.3, output is 60% of original with collapsed glyph contrast; at R_flood=0.0, total blackout; severity depends on exact flood value |
Consent text legibility via reduced glyph contrast; with R_flood=0.0, entire panel collapses to black; intermediate values produce unreadable dark smear | Medium |
Defences
- Traverse the SVG filter graph for all consent element filters —
getComputedStyle(el).filterreturns only the filter URL; resolve the URL to the<filter>element viadocument.querySelector(filterId); iterate all childfeBlendprimitives; read themodeattribute and resolve thein2reference; none of this is exposed by CSSOM and all consent-targeting audits must perform this traversal. - Compute the luminance of all
feFloodsources used asfeBlendinputs — parse theflood-colorandflood-opacityattributes; compute sRGB luminance using the ITU-R BT.709 coefficients (0.2126×R + 0.7152×G + 0.0722×B); flag anyfeBlend mode="multiply"with flood luminance < 0.15 as CRITICAL; flag anyfeBlend mode="screen"with flood luminance > 0.85 as CRITICAL. - Trace
feBlend mode="difference"inputs back to their source primitives — if bothin1andin2can be traced toSourceGraphic(directly or via near-identityfeColorMatrixwith all diagonal values within 0.05 of 1.0), flag as CRITICAL; the difference of any image with a near-identical copy produces near-zero output regardless of the input image content. - Flag
feBlend mode="hard-light"on consent elements for manual review — read thein2flood luminance; if ≤ 0.5 (multiply branch engaged), compute the maximum output intensity as2 × 1.0 × R_flood; flag values producing less than 50% maximum output as HIGH; flag flood luminance = 0 as CRITICAL. - Probe rendered pixel brightness using canvas rasterisation — use
HTMLCanvasElement.drawImage()andgetImageData()on the consent element after rasterisation; sample the mean brightness of pixels within the consent text bounding box; if mean brightness is within 5% of the background colour (indicating invisible text), flag as a rendering attack regardless of the blending mode used. - Enforce a filter allowlist on consent elements — consent panels should use no SVG filters at all; if a filter is detected on a consent element, block rendering and require explicit approval;
feBlendwith modes other thannormalshould never appear on consent UI elements in a trusted MCP client implementation.
SkillAudit findings for this attack surface
feBlend mode="multiply" with a dark feFlood (luminance < 0.15) as the second input — the multiply formula R_out = R_text × R_flood produces R_out < 0.015 for dark grey consent text on a dark flood; near-zero channel intensity makes the consent text imperceptible on dark-background consent panels; textContent intact; getBoundingClientRect() intact; detection requires SVG filter graph traversal and flood-color luminance computation.feBlend mode="screen" with a near-white feFlood (luminance ≥ 0.92) as the second input — the screen formula pushes any dark consent text (R ≤ 0.2) to R_out ≥ 0.936, rendering it indistinguishable from a white background; symmetric complement to the multiply attack, together covering the full range of consent panel background colors; detection requires flood luminance computation from the feFlood flood-color attribute.feBlend mode="difference" with a near-identical copy of SourceGraphic (produced via a feColorMatrix with diagonal 0.999) as the second input — the difference formula yields 0.1% of original channel intensity at every pixel regardless of text color; below display threshold for all practical consent text colors; detection requires tracing both in1 and in2 to SourceGraphic and identifying near-identity matrix transforms.feBlend mode="hard-light" with a mid-dark feFlood (R_flood = 0.3) as the second input — the multiply branch of hard-light (R_in2 ≤ 0.5) yields R_out = 2 × R_in1 × 0.3 = 0.6 × R_in1; glyph contrast collapses producing an unreadable dark smear; extreme case R_flood = 0.0 produces absolute blackout; severity escalates to CRITICAL when flood luminance = 0.