MCP server SVG feBlend multiply screen consent security
Six SA-CSS-MSC consent attack patterns using feBlend mode="multiply" and feBlend mode="screen" — the dual multiplicative formulas exploited to erase dark consent text to zero via Cs×0, bleach all text to white via the screen floor-raise, and cycle between dark-out and white-out in a single interaction using the opposite extreme outputs of the two modes.
The feBlend SVG filter primitive composites two image layers using a blend formula selected by the mode attribute (SVG §15.10). Two of these modes — multiply and screen — form a mathematical dual pair derived from multiplicative compositing. The multiply formula is out = Cs × Cb: the source channel value is scaled by the blend layer channel value. The screen formula is out = Cs + Cb − Cs×Cb, equivalently 1 − (1−Cs)×(1−Cb): screen is the dual of multiply in the sense that screen(Cs, Cb) = 1 − multiply(1−Cs, 1−Cb).
This duality has a direct security consequence: multiply and screen produce identical outputs to darken and lighten respectively at the extreme endpoints of Cb. When Cb=0, darken(Cs,0)=min(Cs,0)=0 and multiply(Cs,0)=Cs×0=0 — the same total erasure to black. When Cb=1, lighten(Cs,1)=max(Cs,1)=1 and screen(Cs,1)=Cs+1−Cs=1 — the same total bleach to white. Scanners that enumerate darken and lighten as known attack modes but omit multiply and screen have incomplete coverage: the same attack works through a different code path.
Formula duality and detection gaps: At Cb=0, multiply≡darken (both output 0). At Cb=1, screen≡lighten (both output 1). But the mode strings are different — a rule matching mode="darken" does not match mode="multiply". Separate detection entries are required for each mode. No shared shortcut exists.
Formula pair: multiply and screen
Both formulas operate on linearised RGB channels in [0,1] as required by the SVG compositing specification:
/* multiply */
out = Cs × Cb
/* screen */
out = Cs + Cb - Cs×Cb
= 1 - (1-Cs)×(1-Cb) /* equivalent dual form */
/* Duality identity */
screen(Cs, Cb) = 1 - multiply(1-Cs, 1-Cb)
/* Extreme-endpoint equivalences */
multiply(Cs, 0) = 0 = darken(Cs, 0) = min(Cs,0) = 0 /* Cb=0: both erase to black */
screen(Cs, 1) = 1 = lighten(Cs, 1) = max(Cs,1) = 1 /* Cb=1: both bleach to white */
/* Mid-point divergence (Cb=0.5) */
darken(0.8, 0.5) = min(0.8, 0.5) = 0.5
multiply(0.8, 0.5) = 0.4 /* multiply is darker than darken at mid-Cb */
lighten(0.2, 0.5) = max(0.2, 0.5) = 0.5
screen(0.2, 0.5) = 0.2+0.5-0.1 = 0.6 /* screen is lighter than lighten at mid-Cb */
SA-CSS-MSC-001: feBlend mode="multiply" + black in2 (Cb=0) — complete dark-out
A feFlood with flood-color="black" produces Cb=0 for all channels. The multiply formula: Cs × 0 = 0. Every pixel of every channel in the source graphic collapses to 0 regardless of source color. Dark consent text, light consent text, any hue — all become R=G=B=0 (black). On a dark-mode UI background, this is invisible.
<filter id="msc001"> <feFlood flood-color="black" flood-opacity="1" result="blackLayer"/> <feBlend in="SourceGraphic" in2="blackLayer" mode="multiply"/> </filter> <!-- Cs×0 = 0 for every channel, every pixel --> <!-- Dark text rgb(26,26,26): 0.10×0=0; light text rgb(229,231,235): 0.90×0=0 --> <!-- Mathematically identical to mode="darken" in2=black (min(Cs,0)=0) --> <!-- Different code path: Cs×0 vs min(Cs,0); same output at Cb=0 --> <!-- Scanner checking only mode="darken" with black in2 misses this -->
The result is mathematically identical to SA-CSS-BDL-002 (feBlend mode="darken" + black in2) — both produce out=0 — but the mode string is different. A detection rule that matches mode="darken" does not match mode="multiply". The detection list must include both.
Complete erasure, not just darkening: multiply+black erases ALL source content to zero regardless of source channel value. Unlike general multiply (which darkens proportionally), multiply with Cb=0 is total. Cs=0.04 (near-black dark text) and Cs=0.96 (near-white light text) both produce 0. The attack works against any foreground color.
SA-CSS-MSC-002: feBlend mode="screen" + white in2 (Cb=1) — complete white-out
A feFlood with flood-color="white" produces Cb=1 for all channels. The screen formula: Cs + 1 − Cs×1 = Cs + 1 − Cs = 1. Every channel of every pixel rises to 1 (white) regardless of source color. Dark consent text on a white/light background becomes white-on-white: invisible.
<filter id="msc002"> <feFlood flood-color="white" flood-opacity="1" result="whiteLayer"/> <feBlend in="SourceGraphic" in2="whiteLayer" mode="screen"/> </filter> <!-- Cs+1-Cs×1 = 1 for every channel, every pixel --> <!-- Dark text rgb(26,26,26): 0.04+1-0.04=1; output rgb(255,255,255) --> <!-- Mathematically identical to mode="lighten" in2=white (max(Cs,1)=1) --> <!-- Different code path: Cs+Cb-Cs×Cb vs max(Cs,Cb); same output at Cb=1 -->
This is the screen-mode counterpart to SA-CSS-BDL-001 (lighten+white). Both produce out=1 at Cb=1 but via different formulas and different mode strings. Detection rules listing mode="lighten" do not cover mode="screen".
SA-CSS-MSC-003: feBlend mode="screen" + near-white in2 (Cb=0.94) — near-white-out, WCAG fail
A feFlood with flood-color="#f0f0f0" (sRGB 240/255≈0.941 linear≈0.94) produces Cb≈0.94. The screen formula for each channel: Cs + 0.94 − Cs×0.94 = 0.94 + Cs×(1−0.94) = 0.94 + 0.06×Cs. This has a key property: the output is lower-bounded by Cb=0.94 (as Cs→0 the output approaches Cb). Near-white in2 floor-raises all dark foreground values to near-white.
<filter id="msc003"> <feFlood flood-color="#f0f0f0" flood-opacity="1" result="nearWhite"/> <feBlend in="SourceGraphic" in2="nearWhite" mode="screen"/> </filter> <!-- out = 0.94 + 0.06×Cs (per channel) --> <!-- Dark text Cs=0.04: 0.94 + 0.06×0.04 = 0.94 + 0.0024 = 0.9424 --> <!-- WCAG relative luminance of output ≈ 0.9424 (linearised) --> <!-- Contrast vs white background: (1+0.05)/(0.9424+0.05) = 1.05/0.9924 ≈ 1.058:1 --> <!-- Well below WCAG AA 4.5:1 threshold; consent text completely unreadable --> <!-- flood-color="#f0f0f0" ≠ "white" ≠ "#ffffff"; evades exact-string detection -->
The floor-raise property of screen with near-white Cb is the key insight: the screen formula guarantees output ≥ Cb for all Cs in [0,1]. Any near-white in2 (Cb≥0.90) ensures all output channels are at least 0.90 — rendering dark text as near-white regardless of the original foreground color. Detection must use numeric thresholding on the feFlood channel values, not string matching on the flood-color attribute.
Screen output is lower-bounded by Cb: For screen, out = Cb + Cs×(1−Cb) ≥ Cb for all Cs≥0. If Cb=0.94, no output channel can fall below 0.94 regardless of source. Near-white in2 is a guaranteed floor-raise attack with no escape for dark foreground colors.
SA-CSS-MSC-004: feBlend mode="multiply" + near-white in2 (Cb=0.94) — identity-like for dark text, NOT an attack
The multiply formula with near-white in2: Cs × 0.94. For dark consent text (Cs=0.04): 0.04×0.94=0.0376 — essentially identical to the original 0.04, with contrast vs white only marginally reduced. Multiply with near-white in2 does not create a contrast attack for dark foreground text.
<!-- multiply+near-white: Cs×0.94 --> <!-- Dark text Cs=0.04: 0.04×0.94 = 0.0376 (very close to original) --> <!-- No meaningful contrast loss for dark foreground --> <!-- multiply+white (Cb=1) is PERFECT IDENTITY: Cs×1 = Cs --> <!-- White in2 has NO effect with multiply mode --> <!-- Do NOT flag multiply+white — it is a true negative --> <!-- Light text (Cs=0.9): 0.9×0.94=0.846 — slight darkening, still readable --> <!-- multiply near-white slightly darkens light text but not an erasure attack -->
This is an important asymmetry: while screen with near-white in2 is a strong contrast attack (floor-raise), multiply with near-white in2 is not. And multiply with exact white (Cb=1) is a perfect identity transformation — Cs×1=Cs — producing no change whatsoever. A scanner that flags multiply+white as an attack would generate false positives. The detection rule for multiply must target near-black in2 (Cb near 0), not near-white in2.
SA-CSS-MSC-005: JS mousedown mode="normal"→"screen" + flood-opacity 0→1 (white) — click-time white-out
The initial state has mode="normal" and flood-opacity="0": the feBlend passes the source graphic through unchanged (mode=normal is identity; opacity-0 flood contributes nothing). On mousedown — exactly when the user activates the consent checkbox or install button — the script sets mode to "screen" and flood-opacity to "1". With Cb=1 (white flood at full opacity), the screen formula immediately produces out=1: complete white-out at the instant of interaction.
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="msc005">
<feFlood id="msc005Flood" flood-color="white" flood-opacity="0" result="fl"/>
<feBlend id="msc005Blend" in="SourceGraphic" in2="fl" mode="normal"/>
</filter>
</defs>
</svg>
<div id="consent" style="filter:url(#msc005)">
I agree to grant full file-system access to this MCP server.
</div>
<script>
document.getElementById("consent").addEventListener("mousedown", () => {
document.getElementById("msc005Blend").setAttribute("mode", "screen");
document.getElementById("msc005Flood").setAttribute("flood-opacity", "1");
// screen(Cs, 1) = 1 for every channel; instant white-out at click time
});
</script>
The visual effect is identical to the lighten+white click-time attack (SA-CSS-BDL-004) and the color-dodge+white attack (SA-CSS-CDG-001): consent text is visible before the click, then erased to white at the moment of activation. All three attacks require separate per-mode detection strings because the mode attribute values "lighten", "color-dodge", and "screen" are distinct. A MutationObserver watching for mode attribute changes on feBlend nodes within consent ancestor filters must monitor these changes regardless of the new mode value.
Three modes, one visual effect, three detection entries: feBlend mode=lighten+white, mode=color-dodge+white, and mode=screen+white all produce out=1 at click time. Each requires a separate detection entry. No formula shortcut allows one rule to cover all three. Runtime MutationObserver on feBlend mode attribute changes in consent filter graphs is required.
SA-CSS-MSC-006: Cross-mode evasion — multiply+black → screen+white cycle
This dual-mode attack exploits the opposite extreme outputs of multiply and screen at opposite Cb extremes. Initial state: mode="multiply", in2 from a feFlood with flood-color="black" (Cb=0). Multiply formula: Cs×0=0. Output is black. On a dark-mode background, the consent text is invisible — dark-out. On mousedown: mode switches to "screen" and flood-color switches to "white" (Cb=1). Screen formula: Cs+1−Cs=1. Output is white. On a light-mode or white background, the consent text is invisible — white-out. The consent text is never visible in either state.
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="msc006">
<feFlood id="msc006Flood" flood-color="black" flood-opacity="1" result="fl"/>
<feBlend id="msc006Blend" in="SourceGraphic" in2="fl" mode="multiply"/>
</filter>
</defs>
</svg>
<div id="consent2" style="filter:url(#msc006)">
I agree to grant full file-system access to this MCP server.
</div>
<script>
document.getElementById("consent2").addEventListener("mousedown", () => {
// Before: mode=multiply, Cb=0 → out=0 (black); invisible on dark bg
document.getElementById("msc006Blend").setAttribute("mode", "screen");
document.getElementById("msc006Flood").setAttribute("flood-color", "white");
// After: mode=screen, Cb=1 → out=1 (white); invisible on light bg
// Consent text is never visible in either state
});
</script>
This attack is particularly resilient because it is adaptive: the two invisible states target different background themes. If the MCP installer uses a dark-mode UI, the pre-click dark-out hides the text. If the installer uses a light background for the consent confirmation page, the post-click white-out hides the text when the click is processed. The attacker covers both cases in a single filter configuration by exploiting the fact that multiply and screen have opposite extreme outputs at opposite ends of the Cb range.
Both states are invisible: multiply+black produces dark-out (invisible on dark UI). screen+white produces white-out (invisible on light UI). The consent text has zero opportunity for visibility before or during the click. Static analysis of the initial DOM finds mode=multiply+black (dark-out, CRITICAL). Dynamic analysis at click time finds mode=screen+white (white-out, CRITICAL). Both detections are independently required.
multiply vs darken and screen vs lighten — boundary equivalence
Understanding the precise conditions under which mode pairs produce identical outputs is necessary for writing complete detection coverage without duplicate logic.
| Comparison | Cb value | darken/lighten result | multiply/screen result | Equal? |
|---|---|---|---|---|
| multiply vs darken | 0 (black) | min(Cs,0)=0 | Cs×0=0 | Yes — both 0 |
| multiply vs darken | 0.5 | min(0.8,0.5)=0.5 | 0.8×0.5=0.4 | No — multiply darker |
| multiply vs darken | 1 (white) | min(Cs,1)=Cs | Cs×1=Cs | Yes — both identity |
| screen vs lighten | 0 (black) | max(Cs,0)=Cs | Cs+0-0=Cs | Yes — both identity |
| screen vs lighten | 0.5 | max(0.2,0.5)=0.5 | 0.2+0.5-0.1=0.6 | No — screen lighter |
| screen vs lighten | 1 (white) | max(Cs,1)=1 | Cs+1-Cs=1 | Yes — both 1 |
The equivalences at boundary Cb values (0 and 1) explain why all four modes can produce identical consent-erasure attacks at those extremes, but different behavior at intermediate Cb means their detection conditions diverge for near-black/near-white thresholding.
Attack summary
| ID | Mode | in2 Cb | Formula result (dark text Cs=0.04) | Effect | Severity |
|---|---|---|---|---|---|
| SA-CSS-MSC-001 | multiply | 0 (black) | 0.04×0=0 | Dark-out (≡darken+black) | Critical |
| SA-CSS-MSC-002 | screen | 1 (white) | 0.04+1−0.04=1 | White-out (≡lighten+white) | Critical |
| SA-CSS-MSC-003 | screen | 0.94 (near-white) | 0.94+0.06×0.04=0.9424 | Near-white-out; WCAG 1.058:1 | High |
| SA-CSS-MSC-005 | normal→screen (mousedown) | 0→1 | identity→1 | Click-time white-out | Critical |
| SA-CSS-MSC-006 | multiply→screen cycle | 0→1 | 0→1 | Dual-mode dark/white cycle | Critical |
Defences
- Extend per-mode detection list: Add
multiplyandscreento the feBlend mode enumeration checked for consent filter attacks. The full list must include: darken, lighten, overlay, soft-light, hard-light, color-dodge, color-burn, exclusion, luminosity, multiply, screen. No mode string can serve as a shortcut for another. - For multiply: flag near-black in2 (Cb≤0.05): Resolve the feFlood flood-color to linearised RGB. If all channels ≤ 0.05 and flood-opacity ≥ 0.95, flag as potential dark-out attack (MSC-001). Simulate
Cs×Cbfor expected foreground channel values and verify WCAG contrast. - For screen: flag near-white in2 (Cb≥0.90): If all channels ≥ 0.90 and flood-opacity ≥ 0.95, flag as potential near-white-out attack (MSC-002/MSC-003). Simulate
Cb + Cs×(1−Cb)for dark foreground (Cs≈0.04) — if output ≥ 0.88, contrast vs white < 1.2:1; flag. - Do NOT flag multiply+white or screen+black:
multiply(Cs,1)=Csis perfect identity — white in2 with multiply has zero effect.screen(Cs,0)=Cs+0−0=Csis perfect identity — black in2 with screen has zero effect. These are true negatives; flagging them generates false positives and erodes detector trust. - Detect dual-mode cycle (MSC-006): If a consent element's filter graph contains a feBlend with a feFlood as in2, and the feFlood flood-color or the feBlend mode is mutated on any user interaction event (mousedown, click, pointerdown, touchstart), flag regardless of which mode or color the mutation produces. Both the before-state and after-state may independently be erasure attacks.
- MutationObserver on feBlend mode attribute: Monitor consent ancestor filter graphs at runtime. Any change to the
modeattribute on a feBlend node — or to theflood-color/flood-opacityattribute on a feFlood supplying in2 — within the filter chain of a consent element should trigger immediate re-evaluation of the consent element's computed color and contrast. - Asymmetric threshold rules: multiply attacks occur at near-black in2; screen attacks occur at near-white in2. The thresholds are mirrors of each other and must not be swapped. A screen+black (Cb=0) configuration is benign; a multiply+white (Cb=1) configuration is benign. The attack directions are asymmetric.
SkillAudit findings for this attack surface
feBlend mode="multiply" with feFlood flood-color near-black (all channels≤0.05 linear) — formula Cs×Cb; at Cb=0, Cs×0=0 for all source channels; complete dark-out regardless of source color; mathematically equivalent to mode="darken"+black but via different code path; detection list must include "multiply" separately from "darken".feBlend mode="screen" with feFlood flood-color white (Cb=1) — formula Cs+1−Cs=1; all channels raised to 1; white-out; equivalent to mode="lighten"+white (both output 1 at Cb=1) but different code path; separate "screen" detection entry required; dark consent text of any hue → rgb(255,255,255); invisible on white/light background.feBlend mode="screen" with near-white feFlood (Cb≥0.90, e.g. #f0f0f0, #ebebeb) — formula output = Cb + Cs×(1−Cb); dark text Cs=0.04: output≈Cb+0.06×0.04≈Cb; floor-raises all dark text to near-white; WCAG contrast vs white <1.1:1; evades exact-white flood-color string detection; threshold: any feFlood channel≥0.88 in screen blend context with dark foreground text.