← Blog

MCP server SVG feBlend soft-light consent security

Four SA-CSS-SLT consent attack patterns using feBlend mode="soft-light" — the W3C soft-light compositing formula applies a nonlinear curve to each channel that squares dark values when in2 is black (Cs=0), enabling a curve-darkening attack that renders dark consent text near-invisible without any channel reaching exactly zero, defeating constant-output and alpha-transparency detectors.

By SkillAudit  |  24 Oct 2026

The W3C Compositing and Blending Level 1 specification defines soft-light as a two-branch piecewise formula operating on linearRGB channel values. For a source layer in1 with channel value Cb and a backdrop layer in2 with channel value Cs, the output is:

// W3C soft-light formula (linearRGB channels, values in [0,1])
// Helper function D(Cb):
//   if Cb >= 0.25: D(Cb) = sqrt(Cb)
//   if Cb <  0.25: D(Cb) = ((16×Cb - 12)×Cb + 4)×Cb

// Branch on Cs:
//   if Cs <= 0.5: out = Cb - (1 - 2×Cs) × Cb × (1 - Cb)
//   if Cs >  0.5: out = Cb + (2×Cs - 1) × (D(Cb) - Cb)

In an SVG feBlend filter, in is the SourceGraphic (Cb) and in2 is typically a feFlood constant layer whose flood-color maps to Cs. Setting in2 to black (Cs=0) selects the Cs≤0.5 branch with the coefficient (1-2×0)=1, reducing the formula to out = Cb - Cb×(1-Cb) = Cb². This squaring operation is the mathematical property exploited by SA-CSS-SLT attacks: it compresses the entire dark tonal range toward zero while leaving midtones and highlights relatively unaffected.

Squaring property at Cs=0: The soft-light formula at Cs=0 yields out = Cb² for every channel independently. This is a curve-darkening attack, not a constant-output attack — the output pixel value depends on the input Cb. Dark consent text in linearRGB (e.g., #333 ≈ Cb=0.04) produces 0.04²=0.0016. A dark MCP client background (Cb≈0.02) produces 0.02²=0.0004. The WCAG relative luminance contrast ratio is (0.0016+0.05)/(0.0004+0.05) ≈ 1.02:1 — effectively no contrast. Constant-output and zero-channel detectors will miss this attack entirely.

SA-CSS-SLT-001: mode="soft-light" + black in2 (Cs=0) — squaring dark-out

A feFlood flood-color="black" flood-opacity="1" produces Cs=0 across all channels. The soft-light formula selects the Cs≤0.5 branch with coefficient (1-2×0)=1:

// Cs = 0, Cs <= 0.5 branch:
// out = Cb - (1 - 2×0) × Cb × (1 - Cb)
//     = Cb - 1 × Cb × (1 - Cb)
//     = Cb - Cb + Cb²
//     = Cb²

// Dark consent text: #333 → linearRGB Cb ≈ 0.04
// out = 0.04² = 0.0016

// Dark MCP client background: Cb_bg ≈ 0.02
// out_bg = 0.02² = 0.0004

// WCAG contrast ratio: (0.0016 + 0.05) / (0.0004 + 0.05) ≈ 1.02:1
// Threshold for normal text: 4.5:1 — FAR below; full dark-out on dark themes

The attack is notable because the output is not constant and not zero — it equals Cb², which varies by pixel. A security scanner looking for "feBlend with flood-color=black produces all-zero output" will find that the output is nonzero (e.g., 0.0016 for the text, 0.0004 for the background), and may therefore classify the filter as benign. However, the contrast between those nonzero values is catastrophically low. Detection requires computing the WCAG contrast ratio of the filtered text color against the filtered background, not just checking that output channels are nonzero.

<filter id="slt1">
  <feFlood flood-color="black" flood-opacity="1" result="black_in2"/>
  <feBlend in="SourceGraphic" in2="black_in2" mode="soft-light"/>
</filter>
<!--
  soft-light(Cb, Cs=0): out = Cb²
  All channels squared; dark text (Cb=0.04) → 0.0016; near-invisible on dark bg
  NOT a constant-output attack; evades zero-output and flood-color-black string checkers
-->

SA-CSS-SLT-002: mode="soft-light" + white in2 (Cs=1) — gamma lightening toward mid-range

A feFlood flood-color="white" flood-opacity="1" produces Cs=1. Because Cs=1 > 0.5, the formula selects the second branch:

// Cs = 1, Cs > 0.5 branch:
// out = Cb + (2×1 - 1) × (D(Cb) - Cb)
//     = Cb + 1 × (D(Cb) - Cb)
//     = D(Cb)

// D(Cb) for Cb >= 0.25: D = sqrt(Cb)
// D(Cb) for Cb <  0.25: D = ((16×Cb - 12)×Cb + 4)×Cb

// Dark text: Cb = 0.04 (linearRGB #333), Cb < 0.25:
//   D = ((16×0.04 - 12)×0.04 + 4)×0.04
//     = ((-11.36)×0.04 + 4)×0.04
//     = (-0.4544 + 4)×0.04
//     = 3.5456 × 0.04
//     = 0.1418  (lightens 0.04 → 0.142)

// Mid-dark text: Cb = 0.1, Cb < 0.25:
//   D = ((16×0.1 - 12)×0.1 + 4)×0.1
//     = ((-10.4)×0.1 + 4)×0.1
//     = (-1.04 + 4)×0.1
//     = 2.96 × 0.1
//     = 0.296  (lightens 0.1 → 0.296)

// White background (Cb = 1, Cb >= 0.25): D = sqrt(1) = 1; bg unchanged
// Light-bg contrast: (0.142 + 0.05) / (1.0 + 0.05) = 0.192 / 1.05 ≈ 5.5:1
// Passes WCAG 4.5:1 minimum but drops from original 21:1 to 5.5:1
// With near-white in2 Cs=0.95: more dramatic lightening; contrast < 4.5:1

White in2 does not cause a complete dark-out. Instead it applies the D(Cb) gamma curve — a nonlinear brightening function that maps dark channels toward the mid-range. For very dark text (Cb=0.04), the output is approximately 0.142, which is lighter but still readable against a pure white background (contrast ≈5.5:1). However, the original contrast (21:1 for pure black text on white) is dramatically reduced. Against slightly off-white or light-gray backgrounds, the reduced output contrast can fall below the WCAG 4.5:1 threshold. The attack is therefore context-sensitive: effective on near-white backgrounds or when combined with a near-white in2 (e.g., Cs=0.95), which pushes the output closer to the background color.

<filter id="slt2">
  <feFlood flood-color="white" flood-opacity="1" result="white_in2"/>
  <feBlend in="SourceGraphic" in2="white_in2" mode="soft-light"/>
</filter>
<!--
  soft-light(Cb, Cs=1): out = D(Cb)
  Dark text Cb=0.04 → ~0.142; contrast vs white bg ≈5.5:1 (passes WCAG min, loses original ratio)
  Near-white in2 (Cs=0.95): out ≈ Cb + 0.9×(D(Cb)-Cb); more dramatic lightening; <4.5:1
-->

SA-CSS-SLT-003: mode="soft-light" + near-black in2 (Cs≈0.1) — subtle nonlinear darkening

With a near-black feFlood (flood-color approximately #1a1a1a, linearRGB ≈ 0.1), Cs=0.1≤0.5 selects the first branch with coefficient (1-2×0.1)=0.8:

// Cs = 0.1, Cs <= 0.5 branch:
// out = Cb - (1 - 2×0.1) × Cb × (1 - Cb)
//     = Cb - 0.8 × Cb × (1 - Cb)

// Dark text: Cb = 0.05 (linearRGB):
//   out = 0.05 - 0.8 × 0.05 × 0.95
//       = 0.05 - 0.038
//       = 0.012

// Dark background: Cb_bg = 0.02:
//   out = 0.02 - 0.8 × 0.02 × 0.98
//       = 0.02 - 0.01568
//       = 0.00432

// Contrast ratio: (0.012 + 0.05) / (0.00432 + 0.05) ≈ 1.14:1 — still very low

// The attack is subtle: near-black is not a color that triggers "constant-dark" heuristics
// Nonlinear darkening is harder to detect than flood-color=black string matching

The near-black in2 attack is the most evasion-resistant variant. A scanner that flags flood-color="black" as a risk indicator will not flag flood-color="#1a1a1a" or flood-color="#0d0d0d". Yet the mathematical effect on contrast is nearly identical to the full black case for typical dark consent text values. The output is nonzero, the in2 color is not extreme-black, and the soft-light mode may appear to be a legitimate aesthetic effect. Only numeric WCAG contrast simulation against both filtered text and filtered background will reveal the attack.

<filter id="slt3">
  <feFlood flood-color="#1a1a1a" flood-opacity="1" result="nearblack_in2"/>
  <feBlend in="SourceGraphic" in2="nearblack_in2" mode="soft-light"/>
</filter>
<!--
  soft-light(Cb, Cs≈0.1): out = Cb - 0.8×Cb×(1-Cb)
  Dark text Cb=0.05 → 0.012; dark bg Cb=0.02 → 0.004; contrast ≈1.14:1
  Near-black in2 evades "flood-color=black" string matching; subtle but devastating
-->

Evasion property of SA-CSS-SLT-003: Because the squaring coefficient at Cs=0.1 is only 0.8 (vs. 1.0 at Cs=0), the output values are slightly higher than the full-black case. The attacker can tune Cs anywhere in [0, 0.5) to trade off evasion (higher Cs, less extreme in2 color) against attack effectiveness. Any value of Cs below approximately 0.15 yields WCAG contrast below 1.5:1 for typical dark MCP client text on dark backgrounds.

SA-CSS-SLT-004: JS mousedown mode "normal"→"soft-light" + feFlood opacity 0→1 (black) — click-time squaring dark-out

The filter is initialized in a benign state: mode="normal" and feFlood flood-opacity="0" (transparent in2). Normal blend with a transparent in2 is an identity operation — the consent element renders exactly as authored. On mousedown, the script atomically switches mode to "soft-light" and makes the black feFlood fully opaque. The output immediately becomes Cb² — the squaring dark-out — at the precise moment the user clicks.

// Initial state (identity — consent renders normally):
// <feBlend in="SourceGraphic" in2="flood_in2" mode="normal"/>
// <feFlood flood-color="black" flood-opacity="0" result="flood_in2"/>

consentElement.addEventListener("mousedown", () => {
  const filter = document.getElementById("sltFilter");
  const fb = filter.querySelector("feBlend");
  const ff = filter.querySelector("feFlood");
  fb.setAttribute("mode", "soft-light");    // activates nonlinear soft-light formula
  ff.setAttribute("flood-opacity", "1");    // Cs=0; triggers Cb² squaring
  // All dark channels now squared; dark text on dark bg contrast ≈ 1.02:1
  // Click proceeds to the consent action on now-invisible text
});

The timing of this attack exploits the browser's rendering pipeline. The DOM mutation and re-filter are processed synchronously during the mousedown event, before the pointer event is dispatched to the consent element. The user sees the text normally before clicking, then experiences the dark-out at the moment of interaction — if their eyes are on the text as they click, the visual state changes just as they commit the action. Combined with dark-themed MCP clients, the click lands on invisible consent text.

Static analysis insufficiency: SA-CSS-SLT-004 has a filter that is safe at parse time. A static scanner reading the HTML will find mode="normal" and flood-opacity="0" — both benign. The attack payload exists only in the JS event handler. Defense requires a MutationObserver watching feBlend[mode], feFlood[flood-opacity], and feFlood[flood-color] on SVG filter elements that are ancestors of consent panels.

Attack summary

IDin2CsFormula result for dark text (Cb=0.05)EffectSeverity
SA-CSS-SLT-001 Black (Cs=0) 0 0.05²=0.0025 (near-zero) Squaring dark-out Critical
SA-CSS-SLT-002 White (Cs=1) 1 D(0.05)≈0.14 (lightens toward mid) Contrast reduction via gamma lightening High
SA-CSS-SLT-003 Near-black (Cs≈0.1) ≈0.1 0.05−0.8×0.05×0.95≈0.012 (darkening) Subtle nonlinear darkening Medium
SA-CSS-SLT-004 Transparent→black (mousedown) 0→0 identity→Cb² Click-time squaring dark-out Critical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-SLT-001: feBlend mode="soft-light" + feFlood in2 black (Cs=0) — soft-light Cs≤0.5 branch: out=Cb−(1-2Cs)×Cb×(1-Cb); at Cs=0: out=Cb²; squaring dark-out; dark consent text (Cb=0.04) → 0.0016; dark bg (Cb=0.02) → 0.0004; WCAG contrast ≈1.02:1; evades constant-output and zero-channel detectors; requires numeric WCAG simulation.
HIGH SA-CSS-SLT-002: feBlend mode="soft-light" + feFlood in2 white (Cs=1) — Cs>0.5 branch: out=D(Cb); D(Cb)=sqrt(Cb) for Cb≥0.25; D(Cb)=((16Cb-12)Cb+4)Cb for Cb<0.25; dark text Cb=0.04 → 0.142; contrast vs white bg ≈5.5:1 (reduced from 21:1); near-white in2 (Cs=0.95) drops below 4.5:1 WCAG threshold; context-sensitive severity; numeric formula check required.
Medium SA-CSS-SLT-003: feBlend mode="soft-light" + feFlood in2 near-black (#1a1a1a, Cs≈0.1) — coefficient (1-2×0.1)=0.8; dark text Cb=0.05 → 0.012; dark bg Cb=0.02 → 0.004; contrast ≈1.14:1; near-black in2 evades flood-color="black" string matching; tunable Cs allows attacker to balance evasion vs. effectiveness; numeric contrast check required.
CRITICAL SA-CSS-SLT-004: JS mousedown changes feBlend mode "normal"→"soft-light" + feFlood flood-opacity 0→1 (black) — initial state benign (normal blend, transparent in2); at mousedown: soft-light + Cs=0 → Cb² squaring; dark text on dark MCP client background invisible at click time; static scanner misses attack; MutationObserver on feBlend mode, feFlood flood-opacity, flood-color required for consent ancestor filters.

Related: feBlend mode=overlay consent attacks  |  feBlend mode=hard-light consent attacks  |  feBlend mode=luminosity consent attacks  |  feBlend mode=darken+lighten consent attacks

← Blog  |  Security Checklist