MCP server CSS flood-color security: SVG feFlood filter background overlay, transparent flood masking, CSS vs attribute override, and feComposite erasure on consent text
Published 2026-09-26 — SkillAudit Research
The SVG feFlood filter primitive fills the entire filter region with a solid color, defined by the flood-color and flood-opacity CSS properties (also available as SVG presentation attributes). feFlood is a source primitive — it generates a filled region that can then be composited with the element's rendered output using feComposite or feBlend.
When an SVG filter applied to a consent text element includes a feFlood primitive composited over the source graphic, the flood region paints on top of the text. If flood-color matches the page background and flood-opacity is 1, the text is completely obscured — replaced by a solid background-colored rectangle. CSS flood-color can override the SVG attribute via the cascade, making this attack invisible to attribute-reading auditors.
Filter graph audit requirement: Standard visibility checks (display, visibility, opacity, dimensions) do not detect feFlood overlay attacks. The consent text element has positive dimensions, is not hidden, and has non-zero opacity. The attack operates via the CSS filter rendering pipeline — the element's pixel output is replaced by the feFlood result before compositing into the page. Auditing requires traversing the filter reference graph and checking feFlood primitives within any filter applied to consent text elements.
Attack findings
An SVG filter applied to consent text uses
feFlood to fill the filter region with the page background color, then feComposite with operator="over" to place the flood region on top of the source graphic. The flood paints the entire filter bounding box in the background color — the text is fully obscured. The element has positive getBoundingClientRect dimensions (the flood fills the same bounding box as the text). The filter attribute is set on the text element. The text exists in the DOM with non-null content. All standard checks pass; only examining the filter graph reveals the overlay.
<defs>
<filter id="consent-erase" x="0" y="0" width="100%" height="100%">
<!-- Generate the source text rendering -->
<feFlood flood-color="#ffffff" flood-opacity="1" result="bg"/>
<!-- Composite flood (bg) OVER the SourceGraphic (text) -->
<feComposite in="bg" in2="SourceGraphic" operator="over"/>
</filter>
</defs>
<text filter="url(#consent-erase)">
By clicking Accept, you agree to binding arbitration.
</text>
/* Result:
feFlood generates white rectangle (flood-color: #ffffff)
feComposite places white rectangle OVER text → text invisible
getBoundingClientRect: non-zero (flood fills same region) → passes size check
getComputedStyle(textEl).filter: "url(#consent-erase)" → reveals filter reference
feFlood.flood-color computed: "rgb(255,255,255)" → background match → CRITICAL */
An MCP server sets
flood-color="rgba(0,0,0,0)" as an SVG attribute on a feFlood primitive (a transparent flood — innocuous). A CSS rule overrides this: #consent-erase feFlood { flood-color: #ffffff; }. The rendered flood is an opaque white rectangle composited over the text. An auditor reading feFlood.getAttribute('flood-color') finds "rgba(0,0,0,0)" — transparent, no concern. Only getComputedStyle(feFlood).floodColor returns "rgb(255,255,255)", revealing the CSS override. This follows the same cascade override pattern as stop-color and fill-opacity attribute attacks.
<!-- SVG attribute: transparent (innocuous) -->
<feFlood id="consent-flood" flood-color="rgba(0,0,0,0)" flood-opacity="1"/>
/* CSS cascade override: background-matching */
#consent-erase feFlood {
flood-color: #ffffff; /* overrides transparent attribute */
}
/* getAttribute('flood-color'): "rgba(0,0,0,0)" → transparent → PASS (incorrect)
getComputedStyle(feFlood).floodColor: "rgb(255,255,255)" → white → ATTACK DETECTED
CSS author stylesheet wins over SVG presentation attribute in cascade */
feBlend with mode="normal" composites the foreground layer over the background layer, replacing background pixels at positions where the foreground has non-zero alpha. An MCP server uses feFlood to generate a solid background-colored rectangle, then feBlend with mode="normal" and the flood as the foreground. The flood's opaque pixels replace all text pixels. A superficial filter inspection that checks for feBlend but expects image-effect modes (multiply, screen, overlay) may not flag a mode="normal" feBlend as an attack vector — it appears to be a standard compositing operation.
<filter id="consent-blend-erase"> <!-- Flood produces background-colored rectangle --> <feFlood flood-color="#f8f8f8" flood-opacity="1" result="overlay"/> <!-- feBlend mode=normal: foreground (overlay) replaces SourceGraphic (text) pixels --> <feBlend in="overlay" in2="SourceGraphic" mode="normal"/> </filter> /* feBlend mode=normal compositing: Dst = Src · αSrc + Dst · αDst · (1 - αSrc) αSrc = 1 (flood is fully opaque) → Dst = Src (flood color) All text pixels replaced by flood color → text invisible Auditor checking feBlend.mode: "normal" may not flag as attack (legitimate use: sprite compositing) Attack revealed by: flood-color == page background color */
A CSS animation changes
flood-color from a transparent or neutral value (visible at page load) to the background color (invisible at button activation time). At the moment the user clicks Accept, the filter flood covers the text. This is the filter-layer variant of the timing attack pattern. A static scan at DOMContentLoaded sees a transparent flood and no concern. Detection requires simulating the CSS animation timeline to the button activation delay time and re-reading computed flood-color.
/* Innocent at page load */
@keyframes flood-reveal {
0% { flood-color: transparent; } /* load: innocuous, text visible */
80% { flood-color: transparent; } /* most of delay: text visible */
100% { flood-color: #ffffff; } /* at button activation: text invisible */
}
#consent-erase feFlood {
animation: flood-reveal 3s forwards; /* matches button activation delay */
flood-opacity: 1;
}
/* Static audit at t=0: flood-color=transparent → no concern
At t=3s (button activates): flood-color=#ffffff → text obscured
Detection: simulate animation timeline; check flood-color at animation-end-time */
Detection
function checkFeFloodFilters(svgRoot, pageBackground = '#ffffff') {
const findings = [];
/* Find all consent text elements with filters applied */
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;
/* Traverse filter graph for feFlood primitives */
const floods = filter.querySelectorAll('feFlood');
for (const flood of floods) {
/* Must use getComputedStyle — CSS may override SVG attribute */
const cs = getComputedStyle(flood);
const floodColor = cs.floodColor || flood.getAttribute('flood-color') || 'black';
const floodOpacity = parseFloat(cs.floodOpacity || flood.getAttribute('flood-opacity') || '1');
/* Check for CSS vs attribute mismatch */
const attrColor = flood.getAttribute('flood-color') || '';
if (attrColor && cs.floodColor && attrColor !== cs.floodColor) {
findings.push({
severity: 'high', flood,
issue: `flood-color attribute "${attrColor}" overridden by CSS "${cs.floodColor}" — attribute audit sees wrong color`
});
}
/* Check if flood is opaque and background-matching */
const bgRGB = parseColorToRGB(pageBackground);
const floodRGB = parseColorToRGB(floodColor);
if (floodOpacity > 0.8 && floodRGB && bgRGB && colorsMatch(floodRGB, bgRGB, 20)) {
/* Check compositing operator: is flood placed over SourceGraphic? */
const composite = flood.parentElement?.querySelector('feComposite, feBlend');
findings.push({
severity: 'critical', flood,
issue: `feFlood with background-matching flood-color (${floodColor}) at opacity=${floodOpacity} — likely composited over consent text to erase it`
});
}
}
}
return findings.length ? findings : null;
}
Remediation
| Control | How it helps |
|---|---|
| Traverse the filter reference graph for all SVG filters applied to consent text elements and check feFlood primitives within them | Standard visibility checks do not detect filter-layer attacks; only examining the filter graph reveals feFlood overlay patterns |
Read flood-color via getComputedStyle(feFlood).floodColor, not via getAttribute('flood-color') | CSS cascade overrides SVG presentation attributes; the CSS-computed flood color is the value used in rendering |
Compare computed flood-color against the page background color and flag near-matches with flood-opacity > 0.5 | Background-matching floods composited over text are the primary erase attack vector; contrast comparison between flood color and background reveals camouflage |
Check feComposite and feBlend operators in the filter graph to determine whether a flood region is placed over or under the source graphic | feFlood under SourceGraphic (in2="SourceGraphic", operator="in") is a masking pattern; feFlood over SourceGraphic is an overlay erase — the compositing operator determines attack direction |
SkillAudit traverses SVG filter reference graphs on consent text elements and checks feFlood primitives for background-matching flood colors, CSS cascade overrides, and compositing operators that place flood regions over source text. Run a free audit on any MCP server GitHub URL to detect feFlood overlay attacks and the full SVG filter consent manipulation surface.