MCP server CSS flood-opacity security: SVG feFlood near-full opacity overlay, CSS attribute override, animated opacity timing attack, and partial opacity contrast reduction on consent text
Published 2026-09-26 — SkillAudit Research
The flood-opacity CSS property controls the alpha channel of the feFlood SVG filter primitive's flood region. It is independent of flood-color — the same two-channel separation that exists between fill-color and fill-opacity, or between stop-color and stop-opacity. When a feFlood element is composited over consent text via feComposite or feBlend, the degree of occlusion is determined by flood-opacity: at 1.0 the text is completely hidden, at 0 the flood is transparent and invisible.
Attacks using flood-opacity exploit the gap between the zero-alpha floor (which auditors check for) and the visibility threshold (above which text becomes illegible). Near-full opacity values (0.9–0.99) pass threshold checks while reducing consent text contrast below the 3:1 minimum. CSS flood-opacity can override SVG presentation attributes via the cascade — allowing silent modification of a previously-safe filter. This property pair with flood-color must be checked together to assess total filter occlusion.
Independent from flood-color: Auditing flood-color for background-matching values while neglecting flood-opacity misses attacks where flood-color is non-transparent but flood-opacity provides near-full occlusion. Both properties contribute to the effective degree of overlay. A flood with flood-color="#ffffff" and flood-opacity="0.95" produces 95% occlusion of underlying text — legibility is near zero — while flood-color alone might appear innocuous (white on a white background surface is expected but flood-opacity=0.95 is the attack mechanism).
Attack findings
A
feFlood with a white flood color (flood-color: #ffffff) is composited over consent text with flood-opacity: 0.97. The compositing formula mixes 97% flood (white) with 3% source text (dark) — effective contrast of the resulting pixels is approximately 1.06:1 against the white page background. The text technically exists in the filter output but is imperceptibly faint. An auditor checking flood-opacity < 0.99 may pass this through, since 0.97 is below 1.0 and a strict full-opacity check might not flag it. Only a contrast-ratio measurement of the composited output catches it.
<filter id="consent-dim"> <feFlood flood-color="#ffffff" flood-opacity="0.97" result="overlay"/> <feComposite in="overlay" in2="SourceGraphic" operator="over"/> </filter> /* Compositing: Output = overlay · αOverlay + source · (1 - αOverlay) = white · 0.97 + dark_text_pixel · 0.03 = near-white Contrast of resulting text against white background: ~1.06:1 WCAG AA large text minimum: 3:1 → FAIL Auditor check (flood-opacity < 1.0): 0.97 < 1.0 → PASS (incorrect) Detection threshold: contrast ratio of composited output < 3:1 */
A
feFlood element has flood-opacity="0" as an SVG attribute — a transparent flood is harmless, as it contributes nothing when composited. A CSS rule overrides it: #consent-filter feFlood { flood-opacity: 0.98; }. The rendered flood is now a 98%-opaque overlay. An attribute-reading auditor finds flood-opacity="0" — transparent, no concern. Only getComputedStyle(feFlood).floodOpacity returns "0.98". This follows the same CSS cascade override pattern as the flood-color attribute override attack.
<!-- SVG attribute: transparent flood (innocuous) -->
<feFlood flood-color="#ffffff" flood-opacity="0"/>
/* CSS override: near-full opacity */
#consent-filter feFlood {
flood-opacity: 0.98; /* overrides attribute "0" → near-full occlusion */
}
/* getAttribute('flood-opacity'): "0" → transparent → PASS (incorrect)
getComputedStyle(feFlood).floodOpacity: "0.98" → near-full → ATTACK DETECTED
Both flood-color AND flood-opacity must be read via computed style */
A CSS animation on
feFlood transitions flood-opacity from 0 (transparent, text fully visible) to 0.95 over the same duration as the button activation delay. At page load, consent text is readable. By the time the button becomes active and the user can click Accept, the flood has reached near-full opacity, making the text illegible. The flood color is the page background color. This is the filter-layer equivalent of the stroke-dashoffset timing attack. Static scan at load time sees flood-opacity: 0 and no concern.
@keyframes consent-flood-rise {
0% { flood-opacity: 0; } /* page load: text fully visible */
100% { flood-opacity: 0.95; } /* at button activation: text obscured */
}
#consent-filter feFlood {
flood-color: #ffffff;
animation: consent-flood-rise 4s forwards; /* matches button delay */
}
/* t=0: flood-opacity=0 → transparent → text visible → PASS
t=4s: flood-opacity=0.95 → 95% occlusion → text illegible
Static audit at t=0: no concern
Detection: simulate animation timeline to button activation time;
re-read computed flood-opacity; check composited contrast */
A
feFlood with flood-opacity: 0.6 is composited over consent text. The resulting contrast of dark text through a 60%-opaque white flood against a white background is approximately 2.1:1 — below the 3:1 WCAG AA minimum for large text, and below the 4.5:1 minimum for normal text. Text at this contrast level appears light gray rather than dark — readable to some users under good conditions but ambiguous and legally questionable as "clear disclosure." Authorization framing text at reduced contrast may be read as decorative or supplementary rather than binding.
<filter id="consent-wash"> <feFlood flood-color="#ffffff" flood-opacity="0.60" result="wash"/> <feComposite in="wash" in2="SourceGraphic" operator="over"/> </filter> /* Source text color: #1a1a1a (very dark gray), background: #ffffff Composited effective text color: = white · 0.60 + #1a1a1a · 0.40 = approximately #a3a3a3 (light gray) Contrast ratio #a3a3a3 on #ffffff: ~1.9:1 WCAG AA large (18pt+): 3:1 → FAIL WCAG AA normal: 4.5:1 → FAIL Detection: measure composited pixel contrast, not raw text color */
Detection
function checkFloodOpacity(svgRoot, pageBackground = '#ffffff') {
const findings = [];
const textEls = svgRoot.querySelectorAll('text, tspan, [data-consent]');
for (const el of textEls) {
const filterRef = el.getAttribute('filter') || getComputedStyle(el).filter;
if (!filterRef || filterRef === 'none') continue;
const filterId = (filterRef.match(/url\(#([^)]+)\)/) || [])[1];
if (!filterId) continue;
const filter = svgRoot.getElementById(filterId);
if (!filter) continue;
const floods = filter.querySelectorAll('feFlood');
for (const flood of floods) {
const cs = getComputedStyle(flood);
/* Check for CSS vs attribute mismatch on flood-opacity */
const cssOpacity = parseFloat(cs.floodOpacity ?? '1');
const attrOpacity = parseFloat(flood.getAttribute('flood-opacity') ?? '1');
if (Math.abs(cssOpacity - attrOpacity) > 0.02) {
findings.push({
severity: 'high', flood,
issue: `flood-opacity attribute="${attrOpacity}" overridden by CSS="${cssOpacity}"`
});
}
/* Skip transparent floods */
if (cssOpacity < 0.05) continue;
/* Check contrast of composited output */
const floodColorStr = cs.floodColor || flood.getAttribute('flood-color') || 'black';
const textColorStr = getComputedStyle(el).fill || '#000000';
const composited = computeCompositedColor(textColorStr, floodColorStr, cssOpacity);
const contrast = getContrastRatio(composited, pageBackground);
if (contrast < 3.0) {
findings.push({
severity: cssOpacity > 0.85 ? 'high' : 'medium', flood,
issue: `feFlood with flood-opacity=${cssOpacity} composited over consent text — composited text contrast ratio ${contrast.toFixed(2)}:1 below 3:1 minimum`
});
}
}
}
return findings.length ? findings : null;
}
Remediation
| Control | How it helps |
|---|---|
Check flood-opacity via getComputedStyle(feFlood).floodOpacity in addition to flood-color via computed style | Both properties are independent CSS channels; a near-transparent flood-color with high flood-opacity still produces significant occlusion of underlying text |
| Compute the composited pixel contrast ratio (flood-color × flood-opacity blended over source text) and compare against the WCAG 3:1 minimum, not against a raw opacity threshold | A contrast-ratio measurement captures the full occlusion effect including partial-opacity floods that reduce contrast below the readable threshold without reaching full opacity |
Treat any flood-opacity value above 0.5 (50% occlusion) on a filter applied to consent text as a finding requiring further contrast analysis | 50% occlusion of dark text against a white background already reduces contrast to approximately 2.5:1 — at the edge of the legibility threshold for large text |
Simulate CSS animation timelines on feFlood elements to the button activation delay time and re-check flood-opacity at that point | Timing attacks set flood-opacity=0 at load time (passes static check) and animate to near-full opacity by button activation time — only dynamic simulation catches them |
SkillAudit reads flood-opacity via CSS computed style, computes the contrast ratio of the composited filter output against the page background, and simulates animation timelines to the button activation delay. Run a free audit on any MCP server GitHub URL to detect feFlood opacity attacks across the full SVG filter consent manipulation surface.