Security Guide
MCP server SVG feFlood flood-opacity consent security — N-node feBlend accumulation, CSS class override, currentColor cascade, and JS mousedown feFlood injection
The feFlood SVG filter primitive fills its output with a solid color at a specified opacity (flood-color + flood-opacity). Used inside a <filter> applied to an overlay element, it can produce a white cover that hides consent text. The four attacks described here are distinct from CSS-property flood-opacity attacks: they exploit SVG-specific feFlood behavior within multi-node feBlend pipelines, CSS class cascade overriding SVG presentation attributes, the currentColor color keyword resolution through CSS inheritance, and temporal JavaScript injection. Each attack requires a different detection approach beyond a simple getAttribute('flood-opacity') check.
Attack 1: feFlood flood-opacity="0.15" per-node with N-node feBlend accumulation (SA-CSS-SVG-FO-001)
The over compositing formula for alpha blending is: A_out = A_src + A_dst × (1 − A_src). Applying a feFlood with flood-opacity="0.15" and flood-color="white" as a layer over SourceGraphic accumulates opacity through repeated feBlend mode="over" nodes. After 1 node, the overlay is 15% opaque; after 2 nodes (blending the first result over SourceGraphic again with another 15% node), it is 27.75%; after 13 nodes, the accumulated opacity exceeds 90%. Each individual feFlood node’s flood-opacity reads as “0.15” — not suspicious on its own. An audit that checks each feFlood child individually and only flags flood-opacity ≥ 0.9 misses the accumulation effect. The number of feFlood nodes in the filter chain must be counted and the accumulated opacity formula applied.
<!-- SA-CSS-SVG-FO-001: 13 feFlood/feBlend pairs accumulate to ~90% opacity cover -->
<filter id="acc-cover">
<!-- Each pair: feFlood(flood-opacity=0.15) blended over running result -->
<feFlood flood-color="white" flood-opacity="0.15" result="f0"/>
<feBlend in="SourceGraphic" in2="f0" mode="over" result="b0"/>
<feFlood flood-color="white" flood-opacity="0.15" result="f1"/>
<feBlend in="b0" in2="f1" mode="over" result="b1"/>
<!-- ... 11 more pairs ... -->
<feFlood flood-color="white" flood-opacity="0.15" result="f12"/>
<feBlend in="b11" in2="f12" mode="over" result="b12"/>
</filter>
// Detection: accumulate alpha over all feFlood nodes in filter chain
function checkFeBlendAccumulation(filterEl) {
const findings = [];
let accumulatedAlpha = 0;
for (const node of filterEl.querySelectorAll('feFlood')) {
const floodOpacity = parseFloat(
node.getAttribute('flood-opacity') ||
getComputedStyle(node).getPropertyValue('flood-opacity') || '1'
);
const floodColor = node.getAttribute('flood-color') ||
getComputedStyle(node).getPropertyValue('flood-color') || 'black';
if (!isNearWhite(floodColor) && !isNearBackground(floodColor)) continue;
// Over compositing: A_out = A_src + A_dst*(1-A_src)
accumulatedAlpha = floodOpacity + accumulatedAlpha * (1 - floodOpacity);
}
if (accumulatedAlpha > 0.9) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-SVG-FO-001',
note: `N-node feBlend accumulation of feFlood nodes achieves effective opacity ${accumulatedAlpha.toFixed(2)} — above 0.9 cover threshold; individual per-node flood-opacity below threshold (sub-opaque individually); multi-node accumulation formula required`
});
} else if (accumulatedAlpha > 0.6) {
findings.push({
severity: 'HIGH', code: 'SA-CSS-SVG-FO-001',
note: `N-node feBlend accumulation of feFlood nodes achieves effective opacity ${accumulatedAlpha.toFixed(2)} — reduces consent text contrast significantly; per-node check missed; accumulation formula required`
});
}
return findings;
}
CRITICAL — SA-CSS-SVG-FO-001: Multiple feFlood nodes with individually sub-opaque flood-opacity values accumulate through feBlend mode="over" to a combined opacity that can exceed 0.9, producing a near-solid white cover. Any single-node flood-opacity check returns sub-opaque values and passes. Detection must count all feFlood nodes in the filter graph with near-white flood-color and apply the over-compositing accumulation formula to compute the effective combined opacity.
Attack 2: flood-opacity set via CSS class, not SVG attribute (SA-CSS-SVG-FO-002)
The flood-opacity property is both an SVG presentation attribute and a CSS property. When set via a CSS class (.cover-filter feFlood { flood-opacity: 0.95; }), the value is accessible via getComputedStyle(feFloodEl).getPropertyValue('flood-opacity') but NOT via el.getAttribute('flood-opacity'). The SVG attribute and the CSS property are independent; the CSS property overrides the SVG attribute according to the CSS cascade (CSS specificity beats presentation attributes). An audit that only calls getAttribute('flood-opacity') — common in SVG attribute traversal tools — reads the SVG attribute value (which may be absent or set to a benign value like “0.01”) while the CSS class sets it to a cover-producing value. Both getAttribute and getComputedStyle must be checked, and the CSS-cascaded value takes precedence.
<!-- SA-CSS-SVG-FO-002: flood-opacity via CSS class, not SVG attribute -->
<style>
/* CSS class sets flood-opacity to 0.95 -- overrides any SVG attribute */
.hi-opacity-flood { flood-opacity: 0.95; }
</style>
<filter id="cov-filter">
<!-- getAttribute('flood-opacity') returns "0.01" (SVG attribute -- looks safe)
getComputedStyle(el).floodOpacity returns "0.95" (CSS class -- the actual value) -->
<feFlood flood-color="white" flood-opacity="0.01"
class="hi-opacity-flood"
result="flood"/>
<feComposite in="flood" in2="SourceGraphic" operator="over"/>
</filter>
// Detection: always use getComputedStyle for flood-opacity, not getAttribute
function getEffectiveFloodOpacity(feFloodEl) {
const attrVal = parseFloat(feFloodEl.getAttribute('flood-opacity') || '1');
const computedVal = parseFloat(
getComputedStyle(feFloodEl).getPropertyValue('flood-opacity') || '1'
);
// Use max of attr and computed to be conservative;
// CSS cascade may override: computed is the authoritative value
const effective = computedVal; // CSS overrides SVG attribute
if (effective !== attrVal) {
return {
effective,
note: `flood-opacity CSS value (${effective}) differs from SVG attribute (${attrVal}) — CSS class overrides SVG attribute; getAttribute('flood-opacity') returns misleading value`
};
}
return { effective, note: null };
}
CRITICAL — SA-CSS-SVG-FO-002: flood-opacity set via a CSS class on a feFlood element overrides the SVG presentation attribute according to the CSS cascade. getAttribute('flood-opacity') returns the SVG attribute value (which may be absent, zero, or benign); only getComputedStyle(el).getPropertyValue('flood-opacity') returns the CSS-overridden value. All flood-opacity reads must use getComputedStyle as the authoritative source.
Attack 3: feFlood flood-color="currentColor" with CSS color: white on ancestor (SA-CSS-SVG-FO-003)
The currentColor keyword in SVG resolves to the CSS color property value of the element or its nearest ancestor that sets color. When feFlood flood-color="currentColor" is used and a CSS rule sets color: white on an ancestor element (such as the <svg> root, a parent <g>, or the containing HTML element), the effective flood color is white. But getAttribute('flood-color') returns the string “currentColor” — a keyword that looks inherently safe to many auditing tools that check for explicit color values. Detection requires either resolving currentColor through the CSS cascade by reading the element’s or ancestor’s computed color property, or flagging flood-color="currentColor" as requiring further cascade investigation.
<!-- SA-CSS-SVG-FO-003: flood-color="currentColor" with ancestor CSS color:white -->
<div style="color: white">
<svg width="400" height="120">
<filter id="cur-cov">
<!-- getAttribute('flood-color') returns "currentColor" -- looks safe
getComputedStyle resolves currentColor to "white" via div ancestor -->
<feFlood flood-color="currentColor" flood-opacity="0.95" result="flood"/>
<feComposite in="flood" in2="SourceGraphic" operator="over"/>
</filter>
<rect x="0" y="0" width="400" height="120" filter="url(#cur-cov)"/>
<text x="20" y="60" fill="#111" font-size="14">Allow credential access?</text>
</svg>
</div>
// Detection: resolve currentColor through cascade
function resolveFloodColor(feFloodEl) {
const floodColorAttr = feFloodEl.getAttribute('flood-color') ||
getComputedStyle(feFloodEl).getPropertyValue('flood-color') ||
'black';
if (floodColorAttr === 'currentColor') {
// currentColor resolves to the computed color of the element
const resolvedColor = getComputedStyle(feFloodEl).color;
const isWhiteOrBg = isNearWhite(resolvedColor) || isNearBackground(resolvedColor);
if (isWhiteOrBg) {
return {
severity: 'CRITICAL', code: 'SA-CSS-SVG-FO-003',
note: `feFlood flood-color="currentColor" resolves to "${resolvedColor}" via CSS cascade — background-matching effective color; getAttribute reads "currentColor" (looks safe); cascade resolution required to detect`
};
}
return {
severity: 'MEDIUM', code: 'SA-CSS-SVG-FO-003',
note: `feFlood flood-color="currentColor" — requires cascade resolution; resolved color is "${resolvedColor}"; verify flood-opacity and effective cover`
};
}
return null;
}
CRITICAL — SA-CSS-SVG-FO-003: feFlood flood-color="currentColor" resolves to the CSS color property of the element or its nearest CSS color-setting ancestor. When that ancestor sets color: white, the effective flood color is white while getAttribute('flood-color') returns “currentColor”. Detection must either flag currentColor as requiring cascade investigation, or call getComputedStyle(feFloodEl).color to resolve the keyword to its effective color value.
Attack 4: JS mousedown sets flood-opacity to 1.0 via style attribute, removes at mouseup (SA-CSS-SVG-FO-004)
A feFlood element has a static flood-opacity="0.01" SVG attribute (sub-opaque, appears benign). A JavaScript mousedown listener sets feFloodEl.style.floodOpacity = '1' (or feFloodEl.setAttribute('style', 'flood-opacity:1')), which overrides the SVG attribute via the CSS inline style cascade. The filter immediately becomes opaque white. At mouseup, the style attribute is removed. The attack window is 50–200ms. A static pre-click audit reads getAttribute('flood-opacity') = 0.01 and passes; it may also call getComputedStyle which at that moment also returns 0.01 (before the click). Only a MutationObserver watching the style attribute on feFlood elements (since the override is applied via the style attribute), or watching for any flood-opacity CSS property changes, detects this temporal injection.
<!-- SA-CSS-SVG-FO-004: JS sets feFlood style.floodOpacity=1 at mousedown -->
<filter id="trans-cov">
<!-- Static: flood-opacity="0.01" -- appears sub-opaque; passes pre-click audit -->
<feFlood id="ff-node" flood-color="white" flood-opacity="0.01" result="flood"/>
<feComposite in="flood" in2="SourceGraphic" operator="over"/>
</filter>
<script>
const ff = document.getElementById('ff-node');
document.addEventListener('mousedown', () => {
// CSS inline style overrides SVG attribute: getComputedStyle now returns 1
ff.style.floodOpacity = '1';
}, true);
document.addEventListener('mouseup', () => {
ff.style.removeProperty('flood-opacity'); // static attribute 0.01 takes effect again
}, true);
</script>
// Detection: MutationObserver watching style attribute on feFlood elements
function monitorFeFloodInjection(svgEl, onFinding) {
for (const ff of svgEl.querySelectorAll('feFlood')) {
const obs = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.attributeName === 'style' || m.attributeName === 'flood-opacity') {
const effective = parseFloat(
getComputedStyle(ff).getPropertyValue('flood-opacity') || '1'
);
const floodColor = ff.getAttribute('flood-color') ||
getComputedStyle(ff).getPropertyValue('flood-color') || 'black';
if (effective > 0.6 && (isNearWhite(floodColor) || isNearBackground(floodColor))) {
onFinding({
severity: 'CRITICAL', code: 'SA-CSS-SVG-FO-004',
note: `feFlood effective flood-opacity raised to ${effective.toFixed(2)} via dynamic style injection during mousedown window; static attribute was ${ff.getAttribute('flood-opacity')}; temporal injection detected by MutationObserver`
});
}
}
}
});
obs.observe(ff, {
attributes: true,
attributeFilter: ['style', 'flood-opacity'],
attributeOldValue: true
});
}
}
CRITICAL — SA-CSS-SVG-FO-004: JS-injected style.floodOpacity = '1' at mousedown overrides the SVG presentation attribute and makes the feFlood filter fully opaque during the click window. Static pre-click and post-click audits both see the sub-opaque SVG attribute value. A MutationObserver watching the style attribute (and the flood-opacity attribute) on all feFlood elements within filters applied to the consent overlay is the only reliable detection path.
Summary table
| Attack | Mechanism | What it bypasses | Severity |
|---|---|---|---|
| SA-CSS-SVG-FO-001: N-node feBlend accumulation | Multiple feFlood nodes with sub-opaque flood-opacity (e.g. 0.15) combined via N feBlend mode=over nodes; accumulated alpha exceeds 0.9 after 13+ nodes; each individual node reads as sub-opaque | Per-node flood-opacity check (each node below threshold); single-feFlood filter inspection; requires counting all feFlood nodes with near-white flood-color and applying over-compositing accumulation formula | Critical |
| SA-CSS-SVG-FO-002: CSS class flood-opacity override | CSS class sets flood-opacity:0.95; SVG flood-opacity attribute shows benign value or is absent; getAttribute returns wrong value; getComputedStyle returns CSS-overridden value | getAttribute('flood-opacity') audit; requires getComputedStyle(el).getPropertyValue('flood-opacity') for CSS-cascade-authoritative value | Critical |
| SA-CSS-SVG-FO-003: flood-color="currentColor" cascade | flood-color="currentColor" resolves through CSS cascade to ancestor color:white; getAttribute shows "currentColor" (looks safe); effective flood fill is white | getAttribute flood-color check (returns keyword not color); requires cascade resolution via getComputedStyle(feFloodEl).color or ancestor CSS color value traversal | Critical |
| SA-CSS-SVG-FO-004: JS mousedown flood-opacity=1 injection | JS sets style.floodOpacity='1' at mousedown; overrides SVG attribute via CSS inline style; feFlood becomes opaque during 50-200ms click window; style removed at mouseup | Pre-click static scan (attribute shows 0.01); post-click scan (attribute reverted); requires MutationObserver on style attribute of feFlood elements during consent window | Critical |