MCP server SVG feBlend darken lighten consent security
Four SA-CSS-BDL consent attack patterns using feBlend mode="darken" and feBlend mode="lighten" — the channel-wise min and max compositing formulas exploited to bleach dark consent text to white, force light text to black, evade exact-color detection with near-white in2, and activate the attack precisely at install-click time.
The feBlend SVG filter primitive composites two layers using a blend formula selected by the mode attribute (SVG §15.10). The mode="lighten" formula computes out = max(in1, in2) per channel — the lighter of the two source values wins. The mode="darken" formula computes out = min(in1, in2) per channel — the darker value wins. Both formulas are channel-wise extrema, not weighted combinations, making them deterministic and absolute: a single in2 value at the channel maximum (white, R=G=B=255) or minimum (black, R=G=B=0) completely overwrites the in1 contribution for that channel direction.
The lighten and darken blend modes are commonly audited in the context of CSS mix-blend-mode on overlay elements, but the SVG feBlend primitive version operates identically. Auditors who check CSS blend modes may miss feBlend-mediated attacks because the filter is applied in the SVG filter effect graph rather than as a CSS property, and getComputedStyle() reports the original color of the consent element without accounting for the feBlend transformation.
Lighten with white in2 is the definitive bleach: max(dark_text_channel, 255) = 255 for every channel. Dark text of any hue — near-black #1a1a1a, dark gray #374151, brand dark-blue #1e3a5f — becomes pure white (R=G=B=255) after lighten with white. Against a white page background, this is complete invisibility with no residual trace.
SA-CSS-BDL-001: feBlend mode="lighten" with white in2 (dark text fully bleached to white)
A feFlood with flood-color="white" produces a layer with R=G=B=255 on all pixels. When used as in2 in a lighten blend with in1=SourceGraphic (dark consent text), the formula gives max(text_channel, 255) = 255 for every channel. The dark text pixels become white. Against a white page background, the result is invisible — white text on white.
<filter id="lighten1"> <feFlood flood-color="white" flood-opacity="1" result="whiteLayer"/> <feBlend in="SourceGraphic" in2="whiteLayer" mode="lighten"/> </filter> <!-- max(rgb(26,26,26), rgb(255,255,255)) = rgb(255,255,255) per channel --> <!-- dark text → white; white on white background = invisible --> <!-- getComputedStyle().color still reports #1a1a1a or original dark value -->
This attack requires zero mathematical sophistication to construct — the feBlend+feFlood pair is a two-primitive filter chain. Detection must identify any feBlend with mode=lighten where the in2 image source is a feFlood with near-maximum channel values (R≥245, G≥245, B≥245, flood-opacity≥0.95). For a dark-foreground light-background consent dialog, such a configuration guarantees foreground bleach.
SA-CSS-BDL-002: feBlend mode="darken" with black in2 (light text forced to black)
A feFlood with flood-color="black" produces a layer with R=G=B=0 on all pixels. The darken formula min(text_channel, 0) = 0 forces every channel to black. Light consent text on a dark-mode UI background — typical of code editors, AI agent installers, and developer tool themes — becomes black-on-black: invisible.
<filter id="darken1"> <feFlood flood-color="black" flood-opacity="1" result="blackLayer"/> <feBlend in="SourceGraphic" in2="blackLayer" mode="darken"/> </filter> <!-- Light text rgb(229,231,235) on dark bg #1e1e2e: min(229,0)=0 per channel --> <!-- output rgb(0,0,0); black text on #1e1e2e background → near-zero contrast -->
Theme-specific attack: The darken+black attack targets dark-mode UIs (VS Code dark, GitHub dark, most AI agent installer shells in dark mode). The attack is invisible in a light-mode security audit environment. Testers must audit with the same theme (light and dark) as the production UI. A single-mode test misses this variant entirely.
SA-CSS-BDL-003: feBlend mode="lighten" with near-white in2 (evades exact-white detection)
A feFlood with flood-color="rgb(250,250,250)" produces R=G=B=250 per channel. The lighten formula: max(26, 250) = 250 for dark consent text (RGB≈26). The output is rgb(250,250,250) — visually indistinguishable from white, with contrast against white ≈ 1.05:1 (below any readable threshold). But the feFlood color is rgb(250,250,250), not white or #ffffff or #fff — it bypasses detection rules that perform exact string matching for common white representations.
<filter id="lighten3"> <feFlood flood-color="rgb(250,250,250)" flood-opacity="1" result="nearWhite"/> <feBlend in="SourceGraphic" in2="nearWhite" mode="lighten"/> </filter> <!-- max(26, 250)=250 per channel; dark text → rgb(250,250,250) on white background --> <!-- WCAG relative luminance of rgb(250,250,250) ≈ 0.955; contrast vs white ≈ 1.05:1 --> <!-- flood-color !== "white" and !== "#ffffff"; exact-match detector misses -->
The correct detection threshold: parse the flood-color to RGB components and check if the lighten output would bleach the expected foreground color. For a consent element with expected dark text (R,G,B ≤ 50): if max(text_channel, flood_channel) ≥ 240 per channel after lighten, the output is effectively white. The threshold must be a luminance comparison, not a string comparison on the flood-color value.
SA-CSS-BDL-004: JS mousedown switches feFlood flood-color to white (click-time bleach activation)
The initial state uses a mid-gray flood-color (e.g., rgb(128,128,128)). With mode=lighten, max(26, 128) = 128 — the dark text is lightened to mid-gray. The consent text is visibly degraded at initial render but not completely invisible. On mousedown, the script changes flood-color to "white": max(26, 255) = 255, completing the bleach. The switch occurs at click time.
consentElement.addEventListener("mousedown", () => {
const flood = document.getElementById("lightenFilter")
.querySelector("feFlood");
flood.setAttribute("flood-color", "white"); // max(text,255)=255; full bleach at click
// Initial gray partially lightens text; not full bleach; passes initial contrast check
});
The initial mid-gray feFlood state may itself fail a WCAG contrast check if the lightened output (e.g., rgb(128,128,128) at 3.9:1 on white) falls below the 4.5:1 threshold — providing a secondary detection opportunity. However, an attacker can tune the initial mid-gray to maintain barely-passing contrast (rgb(80,80,80) ≈ 4.6:1 on white after lighten: max(26,80)=80, WCAG ≈ 4.6:1) while still triggering full bleach at click time. MutationObserver on the feFlood flood-color attribute within mode=lighten/darken feBlend primitives in consent ancestor filters is the required runtime defence.
Attack summary
| ID | Mode | in2 value | Effect on dark text | Severity |
|---|---|---|---|---|
| SA-CSS-BDL-001 | lighten | feFlood white (255,255,255) | All channels → 255; white on white | Critical |
| SA-CSS-BDL-002 | darken | feFlood black (0,0,0) | All channels → 0; black on dark bg | Critical |
| SA-CSS-BDL-003 | lighten | feFlood rgb(250,250,250) | Text → rgb(250,250,250); contrast 1.05:1 | High |
| SA-CSS-BDL-004 | lighten | Gray→White flood at mousedown | Initial degraded; click-time full bleach | Critical |
Defences
- Detect lighten with near-max in2: For feBlend mode=lighten where in2 resolves to a feFlood, parse the flood-color to RGB components. If all three channels ≥ 245 and flood-opacity ≥ 0.95, the output is effectively white for any dark foreground. Do not rely on string-matching ("white", "#fff", "#ffffff") — use numeric channel comparison.
- Detect darken with near-min in2: For feBlend mode=darken where in2 resolves to a feFlood, check if all three channels ≤ 10 and flood-opacity ≥ 0.95. The output forces all channels to near-black, invisible on dark backgrounds.
- Audit for theme coverage: Test consent UI in both light and dark mode. The darken+black attack is invisible in light mode; the lighten+white attack is invisible in dark mode. Coverage must be bidirectional.
- MutationObserver on feFlood flood-color in feBlend consent filters: Monitor flood-color changes on feFlood nodes that supply in2 to feBlend mode=lighten or mode=darken primitives within consent ancestor filters. The direction of change is irrelevant — both lightward and darkward changes can activate attacks.
SkillAudit findings for this attack surface
feBlend mode="lighten" with feFlood flood-color near-white (R,G,B≥245) — lighten formula max(in1,in2) per channel; dark consent text (any channel≤50) → output channel=flood channel≥245; contrast vs white <1.1:1; invisible; getComputedStyle().color unchanged; detection: numeric flood-color channel comparison, not string match.feBlend mode="darken" with feFlood flood-color near-black (R,G,B≤10) — darken formula min(in1,in2); light consent text (any channel≥200) → output channel≤10; near-zero contrast on dark-mode backgrounds; attack invisible in light-mode audit; test both themes.feBlend mode="lighten" with feFlood near-white flood (e.g. rgb(250,250,250), #fafaff) — max(dark_text_channel, 250)=250; effective color ~rgb(250,250,250); WCAG vs white ≈1.05:1; bypasses flood-color==='white' exact-match; threshold: any flood channel≥240 in lighten context with dark foreground → flag.