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.
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
| ID | in2 | Cs | Formula result for dark text (Cb=0.05) | Effect | Severity |
|---|---|---|---|---|---|
| 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
- Numeric WCAG simulation — not string matching: For any
feBlend mode="soft-light"in a consent ancestor filter, extract thein2feFloodflood-coloras linearRGBCs. Apply the piecewise formula to representative consent text (Cb_text≈0.05) and page background (Cb_bgfrom computed style). Compute WCAG contrast ratio of the resulting values. Flag if contrast < 4.5:1 for normal text or < 3:1 for large text. - Detect the Cs≤0.5 darkening branch: For
Cs≤0.5, the formula darkens all inputs. The coefficient is(1-2Cs). AtCs=0it equals 1 (squaring). AtCs=0.1it equals 0.8. AtCs=0.4it equals 0.2 — still darkening. AnyCs<0.5in2 color should trigger the numeric contrast check; do not rely on flood-color string comparisons. - Detect the Cs>0.5 lightening branch on near-white backgrounds: For
Cs>0.5, the formula appliesD(Cb)gamma lightening. On near-white backgrounds (Cb_bg≥0.9), compute the post-blend text value using the D formula and check contrast ratio. Near-white in2 (Cs=0.9) can push dark text above the background's effective lightness, destroying contrast. - Soft-light is not detectable by constant-output analysis: Unlike feColorMatrix with extreme values, soft-light produces output that varies by
Cb. The output for the text pixel and the output for the background pixel differ, but their contrast can still be near 1:1. Any detection based solely on "does this filter always produce the same output pixel" will miss SA-CSS-SLT-001 through SA-CSS-SLT-003. - MutationObserver on dynamic attributes: SA-CSS-SLT-004 requires observing
feBlend[mode],feFlood[flood-opacity], andfeFlood[flood-color]on all SVG filter elements that are ancestors or applied filters of the consent panel. On mutation, immediately re-run the numeric WCAG contrast check and revoke the filter if contrast falls below threshold. - Audit JS event handlers on consent ancestors: Static filter analysis misses SA-CSS-SLT-004. Scan for event listeners (
mousedown,pointerdown,click) on consent-ancestor elements that modify feBlend or feFlood attributes.
SkillAudit findings for this attack surface
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.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.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.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.