← Blog

MCP server SVG feBlend color-dodge color-burn consent security

Four SA-CSS-BCB consent attack patterns using feBlend mode="color-dodge" and feBlend mode="color-burn" — division-based compositing formulas exploited to drive dark consent text to white via near-white in2, force light text to black via black in2, evade exact-color detection with near-white flood, and activate the attack at click time via JS blend-mode switching.

By SkillAudit  |  24 Oct 2026

The feBlend primitive's mode="color-dodge" formula is Cdodge(Cs, Cb) = min(Cb / (1 − Cs), 1), where Cs is the source channel (in1=SourceGraphic, the consent element) and Cb is the backdrop channel (in2, attacker-controlled) — all values normalized to [0,1] (SVG §15.10, compositing-1 spec). The mode="color-burn" formula is the complementary dual: Cburn(Cs, Cb) = max(1 − (1 − Cb) / Cs, 0).

Both formulas involve division by a channel value derived from the source (1−Cs for dodge, Cs for burn). The division creates a nonlinear amplification that produces extreme values — clamped to the [0,1] range — whenever the divisor is small. This nonlinearity is the attack surface: a carefully chosen backdrop value forces the output to the extreme (white or black) regardless of the specific source value, as long as the source is not at the exact boundary.

Color-dodge with white backdrop is a near-universal white-out: With Cb=1 (white backdrop), Cdodge = min(1/(1−Cs), 1). For Cs=0 (absolute black): output=min(1/1,1)=1 (white — but only because Cb=1; for Cs=0 with any Cb, output=Cb). For any Cs>0 (non-black source): the denominator (1−Cs)<1, so Cb/(1−Cs)>Cb=1 → clamped to 1 (white). Dark text with Cs>0 (which includes all near-black grays like #1a1a1a, Cs=0.102) → white. The only exact exception is perfectly achromatic black (Cs=0 per channel), which is rarely used for real UI text.

SA-CSS-BCB-001: feBlend mode="color-dodge" with white in2 (dark text forced to white)

A feFlood with flood-color="white" sets Cb=1 per channel. For dark consent text with Cs>0 (e.g., #1a1a1a: Cs=0.102): Cdodge(0.102, 1) = min(1/(1−0.102), 1) = min(1/0.898, 1) = min(1.114, 1) = 1. White. Against a white background, the consent text is invisible.

<filter id="dodge1">
  <feFlood flood-color="white" flood-opacity="1" result="whiteBackdrop"/>
  <feBlend in="SourceGraphic" in2="whiteBackdrop" mode="color-dodge"/>
</filter>
<!-- Cs=0.102 (text #1a1a1a), Cb=1 (white): Cdodge=min(1/0.898,1)=1 → white -->
<!-- Cs=0.5 (mid-gray), Cb=1: Cdodge=min(1/0.5,1)=min(2,1)=1 → white -->
<!-- Only Cs=0 (absolute black) avoids this: Cdodge(0,1)=min(1/1,1)=1 → still white! -->

The edge case is notable: even for Cs=0 (absolute black per channel), Cdodge(0,1) = min(1/1,1) = 1 — white. This means color-dodge with white backdrop is a true universal white-out for all source values including absolute black. The only exception is when Cs=0 AND Cb<1 (non-white backdrop). With white backdrop, there is no exception — every possible source value produces white output.

SA-CSS-BCB-002: feBlend mode="color-burn" with black in2 (light text forced to black)

A feFlood with flood-color="black" sets Cb=0 per channel. The color-burn formula: Cburn(Cs, 0) = max(1 − 1/Cs, 0) = max((Cs−1)/Cs, 0). For any Cs<1 (non-white source): (Cs−1)/Cs<0 → clamped to 0 (black). Light consent text (Cs=0.9 for #e5e7eb): (0.9−1)/0.9=−0.11 → 0. Output is black.

<filter id="burn1">
  <feFlood flood-color="black" flood-opacity="1" result="blackBackdrop"/>
  <feBlend in="SourceGraphic" in2="blackBackdrop" mode="color-burn"/>
</filter>
<!-- Cs=0.9 (light text), Cb=0 (black): Cburn=(0.9-1)/0.9=-0.11→0 → black -->
<!-- Cs=1 (exact white) is the only exception: Cburn(1,0)=max(1-1/1,0)=max(0,0)=0 -->
<!-- Targets dark-mode installer UIs with light consent text on #1e1e2e bg -->

SA-CSS-BCB-003: feBlend mode="color-dodge" with near-white in2 (evades exact-white detection)

A feFlood with flood-color="rgb(252,252,252)" sets Cb=252/255≈0.988 per channel. For dark consent text (Cs=0.102): Cdodge(0.102, 0.988) = min(0.988/0.898, 1) = min(1.100, 1) = 1 — still white. The near-white backdrop drives the same full bleach as pure white. But the feFlood flood-color is rgb(252,252,252), which is not equal to any common string representation of white ("white", "#ffffff", "#fff", "rgb(255,255,255)").

<filter id="dodge3">
  <feFlood flood-color="rgb(252,252,252)" flood-opacity="1" result="nearWhite"/>
  <feBlend in="SourceGraphic" in2="nearWhite" mode="color-dodge"/>
</filter>
<!-- Cb=0.988: for Cs=0.1 (dark text): 0.988/0.9=1.097→1 (white); same as pure white -->
<!-- flood-color !== "white"; detection requires numeric threshold on Cb, not string match -->
<!-- threshold: if Cb ≥ 0.97 per channel in color-dodge context → full-bleach condition -->

Threshold for color-dodge bleach: Given the formula Cdodge=min(Cb/(1−Cs),1), the minimum Cb that produces output=1 for a given Cs is Cb ≥ 1−Cs. For dark consent text with Cs=0.1: any Cb≥0.9 produces white output. For Cs=0.05: Cb≥0.95. The practical detection threshold: if the feFlood channel value ≥ (1 − expected_foreground_cs) in a color-dodge context, the output is guaranteed white. For any foreground with Cs≤0.2 (dark text range), Cb≥0.8 produces full bleach.

SA-CSS-BCB-004: JS mousedown switches feBlend mode from "normal" to "color-dodge" (click-time bleach)

The initial filter state uses mode="normal" on the feBlend — which in the feBlend context acts as a standard alpha-over composite (identical to the default rendering without a filter). The consent element renders correctly at initial audit time. On mousedown, the script sets the mode to "color-dodge". With the white feFlood already present as in2, the bleach activates instantly.

consentElement.addEventListener("mousedown", () => {
  const blend = document.getElementById("dodgeFilter")
                          .querySelector("feBlend");
  blend.setAttribute("mode", "color-dodge"); // Cdodge with white in2: all→white
  // feFlood flood-color="white" already present as in2; just mode switch needed
  // Initial mode="normal" renders consent correctly; static analysis sees no attack
});

The initial mode="normal" is a standard compositing value with no attack potential. Static tools that enumerate the feBlend mode attribute at load time see a benign value. The JS event listener approach requires dynamic analysis — monitoring for modifications to the mode attribute of feBlend primitives in consent ancestor filters. MutationObserver on the mode attribute specifically (not just the feBlend's child attributes) is required. Note that the feFlood in2 with white flood-color is already present in the static filter definition and does not need to be added at click time — the setup does not trigger a feFlood creation event, only a feBlend mode change.

Attack summary

IDModein2Cs range affectedOutputSeverity
SA-CSS-BCB-001color-dodgefeFlood white (Cb=1)all Cs (universal)White (1)Critical
SA-CSS-BCB-002color-burnfeFlood black (Cb=0)Cs<1 (all non-white)Black (0)Critical
SA-CSS-BCB-003color-dodgefeFlood rgb(252,252,252)Cs≥0.012 (any non-black)White (1)High
SA-CSS-BCB-004normal→color-dodge mousedownfeFlood white pre-loadedall CsIdentity→WhiteCritical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-BCB-001: feBlend mode="color-dodge" with feFlood flood-color white (Cb=1.0) — color-dodge formula Cdodge=min(Cb/(1-Cs),1); Cb=1: for any Cs>0: 1/(1-Cs)>1→1; for Cs=0: 1/1=1→1; universal white output for all source values; dark consent text invisible; getComputedStyle().color unchanged.
CRITICAL SA-CSS-BCB-002: feBlend mode="color-burn" with feFlood flood-color black (Cb=0) — color-burn formula Cburn=max(1-(1-Cb)/Cs,0); Cb=0: max((Cs-1)/Cs,0); Cs<1: negative → clamped to 0 (black); targets dark-mode UI with light consent text; attack invisible in light-mode testing.
HIGH SA-CSS-BCB-003: feBlend mode="color-dodge" with near-white feFlood (Cb≥0.98, e.g. rgb(252,252,252)) — Cdodge(0.1,0.988)=min(0.988/0.9,1)=1; same white output as Cb=1; flood-color!="white" bypasses string match; detection: evaluate formula numerically; flag if Cdodge(expected_foreground_Cs, flood_Cb)≥0.95.
CRITICAL SA-CSS-BCB-004: JS mousedown changes feBlend mode from 'normal' to 'color-dodge' with pre-loaded white feFlood as in2 — static analysis sees mode=normal (benign); click-time: mode=color-dodge; SA-CSS-BCB-001 activates instantly; MutationObserver on feBlend mode attribute in consent ancestor filter required.

Related: feBlend darken/lighten consent attacks  |  feBlend mode=luminosity attacks  |  feBlend mode overview

← Blog  |  Security Checklist