← Blog

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.

By SkillAudit  |  24 Oct 2026

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

IDModein2 valueEffect on dark textSeverity
SA-CSS-BDL-001lightenfeFlood white (255,255,255)All channels → 255; white on whiteCritical
SA-CSS-BDL-002darkenfeFlood black (0,0,0)All channels → 0; black on dark bgCritical
SA-CSS-BDL-003lightenfeFlood rgb(250,250,250)Text → rgb(250,250,250); contrast 1.05:1High
SA-CSS-BDL-004lightenGray→White flood at mousedownInitial degraded; click-time full bleachCritical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-BDL-001: 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.
CRITICAL SA-CSS-BDL-002: 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.
HIGH SA-CSS-BDL-003: 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.
CRITICAL SA-CSS-BDL-004: JS mousedown sets feFlood flood-color from tuned-passing-gray to white — initial: max(text,80)=80; WCAG ~4.6:1 (barely passing); click-time: max(text,255)=255; full bleach; MutationObserver on feFlood flood-color attribute in mode=lighten/darken feBlend within consent ancestor filter required.

Related: feBlend mode overview consent attacks  |  feBlend mode=luminosity attacks  |  CSS mix-blend-mode consent attacks

← Blog  |  Security Checklist