MCP server SVG feBlend all HSL modes unified detection consent security
Unified detection algorithm for all four feBlend HSL-decomposition modes — hue, saturation, color, and luminosity — against MCP consent-text attacks. Three of the four modes (hue, saturation, color) share a common L-from-in2 property enabling a single threshold-based check; mode=luminosity is the exception requiring a separate analysis path. Includes complete scanner pseudocode for static DOM analysis and runtime MutationObserver coverage.
Scanners that detect SVG feBlend consent attacks must handle 16 distinct feBlend modes: normal, multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, hue, saturation, color, and luminosity. The final four — hue, saturation, color, luminosity — are the HSL-decomposition family. They share a common mathematical structure but differ in which HSL components they take from in1 (source) vs in2 (backdrop).
A scanner that checks each HSL mode independently requires four separate code paths. This page presents a unified detection algorithm that handles all four modes through a single L-analysis function with per-mode branching, covering all three attack scenarios (white-out, dark-out, contrast-collapse) while correctly excluding false positives.
HSL component assignment across all four modes
| Mode | H from | S from | L from | L-from-in2? | Achromatic in2 attack? |
|---|---|---|---|---|---|
| hue | in1 (source) | in2 (backdrop) | in2 (backdrop) | Yes | Yes — L=L(in2) |
| saturation | in2 (backdrop) | in1 (source) | in2 (backdrop) | Yes | Yes — L=L(in2) |
| color | in1 (source) | in1 (source) | in2 (backdrop) | Yes | Yes — L=L(in2) |
| luminosity | in2 (backdrop) | in2 (backdrop) | in1 (source) | No | No — L=L(source) |
The three modes hue, saturation, and color all take L from in2 (the feFlood backdrop). For all three, the output luminance = L(feFlood). This is the shared property that enables a unified detection check: parse L from in2, threshold it, flag accordingly. Mode=luminosity takes L from in1 (source/consent text), so the output luminance is preserved from the original — achromatic in2 does not change it.
The 3+1 split: Hue, saturation, and color all take L from in2. Luminosity takes L from in1. A unified check for in2 L-value covers three modes. Luminosity requires a separate analysis. Applying the unified check to luminosity generates false positives for achromatic in2 (which is benign for luminosity). Do not conflate all four modes.
Shared attack matrix for hue, saturation, and color
/* For modes: hue, saturation, color — ALL take L from in2 */
function computeOutputL_HSC(mode, L_source, L_in2) {
/* For hue, saturation, color: output L = L(in2) regardless of L_source */
return L_in2;
}
/* Attack thresholds (WCAG-derived, linearised RGB) */
WHITE_OUT_THRESHOLD = 0.70 /* L > 0.70 → output > 0.70 → WCAG vs white < 1.4:1 */
DARK_OUT_THRESHOLD = 0.15 /* L < 0.15 → output < 0.15 → WCAG vs black < 1.3:1 */
CONTRAST_FAIL_LOW = 0.30 /* L in [0.30, 0.65] → WCAG vs white in [1.5:1, 3.7:1] */
CONTRAST_FAIL_HIGH = 0.65
function checkHSCMode(mode, L_in2, bgColor) {
if (L_in2 > WHITE_OUT_THRESHOLD) {
return { severity: "CRITICAL", type: "white-out",
message: `feBlend mode="${mode}" + feFlood L=${L_in2.toFixed(2)}: output at L=${L_in2.toFixed(2)}; WCAG vs white ≈${wcagVsWhite(L_in2).toFixed(1)}:1` };
}
if (L_in2 < DARK_OUT_THRESHOLD) {
return { severity: "CRITICAL", type: "dark-out",
message: `feBlend mode="${mode}" + feFlood L=${L_in2.toFixed(2)}: output at L=${L_in2.toFixed(2)}; WCAG vs black ≈${wcagVsBlack(L_in2).toFixed(1)}:1` };
}
if (L_in2 > CONTRAST_FAIL_LOW && L_in2 < CONTRAST_FAIL_HIGH) {
const wcag = wcagVsBackground(L_in2, bgColor);
if (wcag < 4.5) {
return { severity: "HIGH", type: "contrast-collapse",
message: `feBlend mode="${mode}" + feFlood L=${L_in2.toFixed(2)}: output L=${L_in2.toFixed(2)}; WCAG vs bg ${wcag.toFixed(1)}:1 < 4.5:1` };
}
}
return null; /* no finding */
}
Separate path for mode=luminosity
/* For mode: luminosity — takes L from in1 (source), H+S from in2 */
function computeOutputL_luminosity(L_source, L_in2) {
/* Luminosity: output L = L(source) NOT L(in2) */
/* Achromatic in2: output L preserved from source → no L-collapse attack */
return L_source;
}
function checkLuminosityMode(L_in2, L_source, S_in2, H_in2, bgColor) {
/* Achromatic in2 (S_in2 ≈ 0): output L = L_source; no L-manipulation */
/* Only attack path for luminosity: chromatic in2 that shifts perceived luminance */
/* via hue/saturation override while preserving L_source numerically */
/* This is a more subtle attack (opponent-color channels) — separate analysis needed */
/* For achromatic in2: return null (no finding) */
if (S_in2 < 0.05) {
return null; /* true negative for luminosity + achromatic in2 */
}
/* For chromatic in2: evaluate whether the H+S override from in2 */
/* combined with L from source creates a camouflage attack */
/* (e.g. in2 with same H+S as background → consent text blends in) */
/* This is a different attack surface — see luminosity-specific analysis */
return checkLuminosityChromaticIn2(L_source, S_in2, H_in2, bgColor);
}
Complete unified HSL mode detection pseudocode
/* Entry point: called for any feBlend with HSL mode in consent ancestor filter */
function detectHSLModeAttack(blendNode, consentAncestor, domContext) {
const mode = blendNode.getAttribute("mode");
const in2Node = resolveIn2(blendNode, domContext);
/* Parse in2 feFlood to HSL */
const floodColor = in2Node.getAttribute("flood-color");
const floodOpacity = parseFloat(in2Node.getAttribute("flood-opacity") || "1");
const { H, S, L } = parseColorToHSL(floodColor);
/* Effective L after opacity blend (opacity-0 in2 has no effect) */
const effectiveL = floodOpacity < 0.05 ? null : L;
/* Background luminance for WCAG simulation */
const bgL = getComputedBackgroundLuminance(consentAncestor);
/* Mode routing */
if (mode === "hue" || mode === "saturation" || mode === "color") {
if (effectiveL === null) {
/* Opacity 0: check for JS mutation attack */
watchForMutation(in2Node, blendNode, consentAncestor, mode);
return null;
}
return checkHSCMode(mode, effectiveL, bgL);
} else if (mode === "luminosity") {
if (effectiveL === null) {
watchForMutation(in2Node, blendNode, consentAncestor, mode);
return null;
}
return checkLuminosityMode(effectiveL, null, S, H, bgL);
}
return null;
}
/* MutationObserver: watch for click-time attacks */
function watchForMutation(floodNode, blendNode, consentAncestor, mode) {
const observer = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.attributeName === "flood-opacity" || m.attributeName === "flood-color") {
const newOpacity = parseFloat(floodNode.getAttribute("flood-opacity") || "1");
if (newOpacity > 0.05) {
const { H, S, L } = parseColorToHSL(floodNode.getAttribute("flood-color"));
const bgL = getComputedBackgroundLuminance(consentAncestor);
const finding = mode === "luminosity"
? checkLuminosityMode(L, null, S, H, bgL)
: checkHSCMode(mode, L, bgL);
if (finding) {
reportFinding({ ...finding, trigger: "interaction", mode, timestamp: Date.now() });
}
}
}
}
});
observer.observe(floodNode, { attributes: true, attributeFilter: ["flood-opacity", "flood-color"] });
observer.observe(blendNode, { attributes: true, attributeFilter: ["mode"] });
}
Per-mode attack and detection summary
| Mode | L source | White in2 (L=1) | Black in2 (L=0) | Gray in2 (L=0.5) | Detect via |
|---|---|---|---|---|---|
| hue | in2 | White-out (Critical) | Dark-out (Critical) | Collapse (High) | checkHSCMode(L_in2) |
| saturation | in2 | White-out (Critical) | Dark-out (Critical) | Collapse (High) | checkHSCMode(L_in2) |
| color | in2 | White-out (Critical) | Dark-out (Critical) | Collapse (High) | checkHSCMode(L_in2) |
| luminosity | in1 | No attack (L preserved) | No attack (L preserved) | No attack (L preserved) | checkLuminosityMode (separate) |
False positive prevention: luminosity + achromatic in2
A scanner that applies the HSC mode check (L from in2) to mode=luminosity will flag luminosity+white as Critical white-out. This is incorrect: luminosity+white preserves source luminance. Dark text remains dark. The false positive erodes detector credibility and may lead operators to disable the detection rule entirely — a worse outcome than missing it.
/* FALSE POSITIVE example — do NOT do this */
if (["hue","saturation","color","luminosity"].includes(mode)) {
/* Wrong: applies unified L(in2) check to luminosity too */
const L = parseLuminance(floodColor);
if (L > 0.70) flag("white-out"); // FALSE for luminosity + white!
}
/* CORRECT: mode-specific routing */
if (mode === "hue" || mode === "saturation" || mode === "color") {
/* These take L from in2 — check L(in2) */
checkHSCMode(mode, parseLuminance(floodColor));
} else if (mode === "luminosity") {
/* Takes L from in1 — achromatic in2 is benign; separate path */
checkLuminosityMode(parseLuminance(floodColor), parseSaturation(floodColor), ...);
}
Detection coverage summary (all feBlend modes)
With the HSL modes covered by this page, the complete feBlend detection list now covers all 16 modes:
/* feBlend mode detection coverage as of 2026-10-24 */
const FEBLEND_MODES_COVERED = {
/* Arithmetic/compositing modes */
"normal": "identity; benign in most cases",
"multiply": "dark-out at Cb=0; separate from darken",
"screen": "white-out at Cb=1; floor-raise at near-white",
"overlay": "asymmetric: dark<0.5 → multiply; light>0.5 → screen",
"darken": "min(Cs,Cb); dark-out at Cb=0",
"lighten": "max(Cs,Cb); white-out at Cb=1",
"color-dodge": "Cs/(1-Cb); white at Cb→1; singularity",
"color-burn": "1-(1-Cs)/Cb; black at Cb→0; singularity",
"hard-light": "overlay with in1/in2 swapped",
"soft-light": "softer version of overlay",
"difference": "|Cs-Cb|; inversion at Cb=1; collapse at Cb=0.5",
"exclusion": "Cs+Cb-2CsCb; softer difference; same endpoints",
/* HSL decomposition modes */
"hue": "L from in2; unified L-check; false-positive: none",
"saturation": "L from in2; unified L-check; false-positive: none",
"color": "L from in2; unified L-check; works for chromatic in2",
"luminosity": "L from in1; separate path; achromatic in2 benign",
};
/* All 16 modes covered. Complete coverage achieved. */
Milestone: With the addition of difference, hue, saturation, color, and luminosity mode analysis, all 16 feBlend modes are now covered in SkillAudit's consent-attack detection surface. The unified HSL check (this page) reduces the HSL family to a single L-analysis function with one branch point, enabling efficient and complete coverage without per-mode code duplication for the three modes that share L-from-in2 semantics.
Defences
- Enumerate all 16 feBlend modes: Any feBlend with a mode not in the explicit allowlist {"normal"} should trigger HSL-mode or compositing-mode analysis depending on the mode category.
- Use the unified HSC check for hue, saturation, color: Parse feFlood flood-color to L. Threshold: L > 0.70 → Critical white-out; L < 0.15 → Critical dark-out; 0.30 < L < 0.65 → High contrast-collapse. Apply this to all three modes identically.
- Separate path for luminosity: Achromatic in2 with luminosity is benign. Check S(in2) first; if S < 0.05, return no-finding. Only proceed to chromatic-in2 analysis for S ≥ 0.05.
- Runtime MutationObserver on all HSL mode filter chains: Watch feFlood flood-color, flood-opacity, and feBlend mode attributes within the filter graphs of any consent ancestor element. Click-time transitions from opacity-0 to opacity-1 with high-L flood-color are Critical click-time attacks for hue, saturation, and color modes.
- Parse flood-color numerically: Do not rely on string matching. Parse any CSS color representation (named colors, hex, rgb(), hsl(), etc.) to L via HSL conversion. The threshold-based numeric check covers all representations.
- Combine static + dynamic analysis: Static analysis catches DOM-initial attack configurations. Runtime MutationObserver catches click-time mutations. Both are required: static alone misses SA-CSS-HUE-004/SAT-004/COL-004 click-time variants; runtime alone misses pre-loaded attack filters.
SkillAudit findings for this attack surface
feBlend mode=hue|saturation|color with feFlood L>0.70 — all three modes take L from in2; output L=L(in2); white-out for L(in2)>0.70; applies to any flood-color (achromatic or chromatic) with L in the critical range; three separate mode detection entries required despite identical analysis function; false positive risk: none.feBlend mode=hue|saturation|color with feFlood L<0.15 — output L=L(in2)<0.15; dark-out on dark background; all three modes; separate entries.feBlend mode=hue|saturation|color with feFlood L in [0.30, 0.65] — contrast-collapse; WCAG fails at this L range; use checkHSCMode with WCAG simulation against computed background luminance of consent ancestor.feBlend mode=luminosity — luminosity takes L from in1 (source), not in2; achromatic in2 with luminosity is benign; applying the HSC check generates false positives; scanner must route luminosity to separate analysis path.Individual mode pages: feBlend mode=hue | feBlend mode=saturation | feBlend mode=color | feBlend mode=luminosity