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.
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
| ID | Mode | in2 | Cs range affected | Output | Severity |
|---|---|---|---|---|---|
| SA-CSS-BCB-001 | color-dodge | feFlood white (Cb=1) | all Cs (universal) | White (1) | Critical |
| SA-CSS-BCB-002 | color-burn | feFlood black (Cb=0) | Cs<1 (all non-white) | Black (0) | Critical |
| SA-CSS-BCB-003 | color-dodge | feFlood rgb(252,252,252) | Cs≥0.012 (any non-black) | White (1) | High |
| SA-CSS-BCB-004 | normal→color-dodge mousedown | feFlood white pre-loaded | all Cs | Identity→White | Critical |
Defences
- Evaluate color-dodge formula numerically: For feBlend mode=color-dodge where in2 resolves to a feFlood, compute
Cdodge(Cs_text, Cb_flood)for the expected foreground color. If output ≥ 0.95 for all text pixels, flag SA-CSS-BCB-001 or SA-CSS-BCB-003. Do not use string matching on flood-color — use parsed numeric values. - Apply symmetric analysis for color-burn: For mode=color-burn with feFlood in2, compute
Cburn(Cs_text, Cb_flood)for the expected foreground color. If output ≤ 0.05 for all text pixels, flag SA-CSS-BCB-002. - MutationObserver on feBlend mode attribute: Monitor changes to the
modeattribute on feBlend elements in consent ancestor filters. A change from "normal" to any non-trivial blend mode (color-dodge, color-burn, lighten, darken, etc.) is a high-confidence signal of SA-CSS-BCB-004. - Audit feBlend mode independently from CSS mix-blend-mode: The SVG feBlend mode and CSS mix-blend-mode apply the same formulas but via different code paths. A tool that only checks CSS mix-blend-mode properties on the consent element will miss feBlend-mediated attacks in the filter graph.
SkillAudit findings for this attack surface
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.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.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.