Security Guide
MCP server SVG feFlood consent security — solid-color direct overlay, flood-opacity="0.95" near-opaque wash, feBlend mode="normal" compositor, and dynamic flood-opacity mutation at interaction time
The <feFlood> SVG filter primitive generates a rectangle of a single solid color that fills the entire filter region. Unlike other SVG filter primitives that transform an input image, feFlood ignores its input entirely — it simply produces a solid-color output covering the full filter area. When used as a filter’s last (or only) primitive, its output becomes the rendered output of the filtered element: a uniform rectangle that covers everything the element was supposed to display. Applied to an MCP consent panel, feFlood flood-color="white" flood-opacity="1" replaces the entire consent panel contents with a white rectangle. The DOM element is unchanged — its textContent is still the consent string, its layout is still present, and getComputedStyle(el).filter returns only the filter URL, not the flood color or opacity. Four feFlood consent attack patterns follow.
Attack 1: feFlood flood-color="white" flood-opacity="1" as the sole filter output — direct consent panel erasure (SA-CSS-FLD-001)
The simplest feFlood consent attack places a single <feFlood> primitive in a filter element and applies that filter to the consent panel. Since feFlood ignores its input and produces a solid-color output, the entire filter output is a white (or background-colored) rectangle. The consent text, permission scopes, button labels, and all other content inside the filtered element are completely overwritten.
The attack is maximally effective when the flood color matches the page background: flood-color="white" on a white-background page makes the consent panel visually indistinguishable from the surrounding page area. The panel border, if defined in CSS rather than SVG, is still visible (because borders are painted outside the filter region by default), giving the appearance of an empty container. The consent text is present in the DOM and accessible via textContent, innerHTML, and screen reader accessibility trees — it simply does not appear visually.
<!-- SA-CSS-FLD-001: feFlood as sole filter output — consent text erased -->
<svg width="0" height="0" style="position:absolute">
<defs>
<filter id="flood-erase">
<!-- feFlood ignores SourceGraphic entirely.
Output: solid white rectangle filling the filter region.
Consent panel becomes a blank white box. -->
<feFlood flood-color="white" flood-opacity="1"/>
</filter>
</defs>
</svg>
<div id="consent" style="filter:url(#flood-erase); border:1px solid #e5e7eb; padding:24px">
<h3>Permission Request</h3>
<p>This MCP server requests: file system read, credential access, outbound network.</p>
<button id="allow">Allow</button>
<button>Deny</button>
</div>
// CSSOM blind spot:
getComputedStyle(document.getElementById('consent')).filter;
// → "url(#flood-erase)"
// Does not reveal flood-color or flood-opacity.
// Must resolve the URL and inspect the feFlood child element directly.
// Detection:
function detectFeFlood(el) {
const filterVal = getComputedStyle(el).filter || el.getAttribute('filter') || '';
const urlMatch = filterVal.match(/url\(["']?(#[\w-]+)["']?\)/);
if (!urlMatch) return null;
const def = document.querySelector(urlMatch[1]);
if (!def) return [{ sev: 'HIGH', note: 'Filter not found' }];
const findings = [];
for (const ff of def.querySelectorAll('feFlood')) {
const opacity = parseFloat(ff.getAttribute('flood-opacity') ?? '1');
const color = ff.getAttribute('flood-color') || 'black';
// Check if feFlood is used as the terminal primitive (last child or no downstream feBlend)
const isTerminal = ff === def.lastElementChild
|| !ff.nextElementSibling?.matches('feBlend, feComposite');
if (isTerminal && opacity > 0.7) {
findings.push({
sev: opacity > 0.9 ? 'CRITICAL' : 'HIGH',
note: `feFlood flood-color="${color}" flood-opacity="${opacity}" as terminal primitive — consent content overwritten`
});
}
}
return findings.length ? findings : null;
}
CRITICAL — SA-CSS-FLD-001: feFlood as the terminal (last or sole) primitive of a consent element filter replaces all rendered content with a solid color. The consent DOM is unchanged; only the visual output is affected. getComputedStyle(el).filter returns only the filter URL — the flood color and opacity are not accessible via CSSOM without resolving the filter definition. Detection requires resolving the filter URL and inspecting feFlood children for flood-opacity > 0.9 in terminal position.
Attack 2: flood-opacity="0.95" near-opaque overlay — contrast collapse below WCAG threshold (SA-CSS-FLD-002)
A flood opacity of exactly 1 may trigger simple detection rules that check for fully opaque floods. A near-opaque flood with flood-opacity="0.95" is a bypass: the consent text is not fully invisible, but the contrast ratio between the original text color and the flood-washed output falls below human readability thresholds.
For dark text (#111827, approximately rgb(17,25,39)) on a white background, with a white flood at opacity=0.95, the composited text color is: output = flood × opacity + source × (1 - opacity) = 255×0.95 + 17×0.05 ≈ 242 + 0.85 ≈ 243 per RGB channel. The resulting color is approximately rgb(243,243,244) — nearly white. The WCAG contrast ratio against a white background is approximately 1.1:1, far below the 4.5:1 AA threshold and even below the 3:1 threshold for large text. The consent text is effectively invisible while the flood opacity is technically less than 1.
<!-- SA-CSS-FLD-002: flood-opacity=0.95 near-opaque — bypasses opacity=1 detection rules
Dark text #111827 on white → composited output ≈ rgb(243,243,244)
WCAG contrast against white background: ≈ 1.1:1 (invisible) -->
<filter id="flood-near-opaque">
<feFlood flood-color="white" flood-opacity="0.95" result="wash"/>
<feComposite in="wash" in2="SourceGraphic" operator="over"/>
</filter>
// Compositing formula (flood over source):
// R_out = flood_R × flood_opacity + src_R × (1 - flood_opacity)
// = 255 × 0.95 + 17 × 0.05
// = 242.25 + 0.85
// = 243.1 ≈ 243
// Resulting WCAG relative luminance:
// sRGB(243/255) = 0.9529 → linearized = ((0.9529+0.055)/1.055)^2.4 ≈ 0.904
// Contrast ratio vs white (luminance=1.0): (1.0+0.05)/(0.904+0.05) = 1.05/0.954 ≈ 1.10:1
// 1.10:1 — invisible; AA requires 4.5:1 for normal text
// Detection: check flood-opacity > 0.5 (not just > 0.9)
// Threshold for WCAG 4.5:1 fail on dark-on-white text ≈ flood-opacity > 0.75
HIGH — SA-CSS-FLD-002: flood-opacity="0.95" near-opaque white flood collapses dark-on-white consent text contrast to approximately 1.1:1. Detection rules that check only flood-opacity > 0.9 catch this; rules that check only flood-opacity === 1 miss it. The correct threshold for a WCAG 4.5:1 AA failure on standard dark-on-white consent text is flood-opacity > 0.75 for a white flood — above this value, the composited contrast falls below 4.5:1 for any text darker than #888.
Attack 3: feFlood as feBlend in2 with mode="normal" — flood composited over SourceGraphic (SA-CSS-FLD-003)
When feFlood is used not as the terminal output but as the in2 input to a subsequent <feBlend mode="normal">, the feBlend primitive composites the flood over the SourceGraphic. feBlend mode="normal" computes the output as: result = in × (1 − flood_opacity) + in2 × flood_opacity. With flood-opacity="1", this simplifies to result = in2 — the output is the flood color regardless of the source. The feBlend simply replaces the SourceGraphic with the flood.
This two-primitive pattern is more complex than the single-feFlood variant but the effect is identical: the consent panel is replaced by a solid-color rectangle. The additional feBlend element may cause simple detection rules to stop at "no terminal feFlood" and miss the attack, because the terminal element is now feBlend rather than feFlood. Detection requires following the primitive chain and recognizing that feBlend mode=normal with a solid-color in2 at opacity=1 is equivalent to a single feFlood.
<!-- SA-CSS-FLD-003: feFlood as feBlend in2 with mode=normal
feBlend mode=normal formula: out = in*(1-α_in2) + in2*α_in2
With flood-opacity=1: out = in*(1-1) + flood*1 = flood
SourceGraphic completely replaced by flood color.
Terminal element is feBlend, not feFlood — hides the attack from feFlood-specific checks. -->
<filter id="flood-blend-erase">
<feFlood flood-color="white" flood-opacity="1" result="white-flood"/>
<feBlend in="SourceGraphic" in2="white-flood" mode="normal"/>
</filter>
// feBlend mode values and their consent attack potential:
// mode="normal": out = in*(1-α_in2) + in2*α_in2 → flood-over-source → CRITICAL if α_in2≈1
// mode="screen": out = 1-(1-in)*(1-in2) → whitens toward 1; high opacity flood → near-white
// mode="multiply": out = in*in2 → darkens toward 0; white flood → no change (multiply(x,1)=x)
// mode="darken": out = min(in,in2) → darken; white flood → min(x,1)=x → no effect
// mode="lighten": out = max(in,in2) → lighten; white flood → max(x,1)=1 → everything white → CRITICAL
// Detection must check all blend modes, not just mode="normal"
HIGH — SA-CSS-FLD-003: feFlood used as in2 for feBlend mode="normal" with flood-opacity="1" replaces the SourceGraphic with the flood color. Detection rules that check only for terminal feFlood miss this pattern because the terminal element is feBlend. Auditors must trace the filter primitive chain and evaluate the mathematical output of each blend mode when one input is a solid-color feFlood. mode="lighten" with a white flood is equally effective: max(source, 1) = 1 (white) at every pixel regardless of source value.
Attack 4: Dynamic flood-opacity mutation at interaction time — CSSOM captures safe initial state (SA-CSS-FLD-004)
A consent element can be initialized with a feFlood at flood-opacity="0" — fully transparent, harmless, identical to no flood — which then has its flood-opacity attribute mutated to 1 at the moment the user is about to interact with the consent UI. The mutation can be triggered by an event handler on a parent element, a MutationObserver watching a different part of the DOM, or a timer fired after the consent panel is scrolled into view.
The CSSOM snapshot captures only the value at the time of inspection. A static scanner that inspects feFlood flood-opacity once on page load sees flood-opacity="0" and passes the check. The mutation occurs after the scan, at interaction time, and the user sees a blank panel. This dynamic activation pattern is the same class of evasion as the CSS animation activation attacks documented for other filter primitives: the filter is safe at scan time and dangerous at interaction time.
<!-- SA-CSS-FLD-004: flood-opacity starts at 0 (safe), mutates to 1 at interaction time -->
<filter id="flood-dynamic">
<!-- Initially transparent — scanner sees flood-opacity=0 → passes -->
<feFlood id="consent-flood" flood-color="white" flood-opacity="0"/>
</filter>
<div id="consent-panel" style="filter:url(#flood-dynamic)">
<p>Requesting: credential access, filesystem write, outbound network.</p>
<button id="allow">Allow</button>
</div>
// Activation: mutation triggered by mousemove near the consent panel
document.getElementById('consent-panel').closest('.modal')
.addEventListener('mousemove', function activateFlood(e) {
const rect = document.getElementById('allow').getBoundingClientRect();
// When cursor is within 40px of the Allow button: activate flood
if (Math.abs(e.clientX - rect.left) < 40 && Math.abs(e.clientY - rect.top) < 40) {
document.getElementById('consent-flood').setAttribute('flood-opacity', '1');
// Consent panel immediately goes blank; user clicks Allow on blank panel
this.removeEventListener('mousemove', activateFlood); // fire once
}
});
// Alternative activation: MutationObserver on a non-consent element
const observer = new MutationObserver(() => {
document.getElementById('consent-flood').setAttribute('flood-opacity', '1');
});
observer.observe(document.querySelector('.unrelated-element'), { attributes: true });
// When any attribute changes on .unrelated-element, consent panel goes blank
// Static scan defense gap:
// Snapshot of feFlood at scan time: flood-opacity="0" → passes all static checks
// Runtime state at interaction: flood-opacity="1" → consent erased
// Dynamic activation requires runtime monitoring (MutationObserver on filter elements)
// or CSP-based restrictions on SVG attribute mutation
HIGH — SA-CSS-FLD-004: flood-opacity initialized to 0 (harmless) then mutated to 1 at interaction time evades static filter analysis. The CSSOM snapshot sees a safe state; the mutation occurs after the audit snapshot. Detection requires monitoring feFlood attribute mutations on consent element filters in real time via MutationObserver, or enforcing a policy that prohibits any feFlood element in consent element filter definitions regardless of its current flood-opacity value — because a feFlood present in the filter graph is a potential dynamic activation surface even if currently transparent.
Summary table
| Attack | Mechanism | What it hides | Severity |
|---|---|---|---|
SA-CSS-FLD-001: feFlood flood-color="white" flood-opacity="1" |
feFlood as sole/terminal filter primitive; output is a solid white rectangle covering the entire filter region; SourceGraphic ignored; consent DOM unchanged; CSSOM returns only filter URL, not flood attributes; requires URL resolution to detect | All consent text, permission scopes, button labels; the panel appears as an empty container with CSS border still visible; screen readers can still access the DOM text (the attack is visual-only) | Critical |
SA-CSS-FLD-002: flood-opacity="0.95" |
Near-opaque white flood composited over consent text; dark #111827 text composited output ≈ rgb(243,243,244); WCAG contrast ≈ 1.1:1; below 4.5:1 AA threshold; bypasses detection rules checking only flood-opacity=1; threshold for 4.5:1 fail: flood-opacity > 0.75 for white flood on dark text | Consent text contrast collapses to near-invisible; text is technically present at very low opacity but not readable; borderline attacks at flood-opacity=0.75 produce ≈ 2:1 contrast (below 4.5:1 AA but marginally legible under ideal conditions) | High |
SA-CSS-FLD-003: feFlood in2 + feBlend mode="normal" |
feFlood not terminal; used as in2 for feBlend mode=normal; blend formula out = in*(1-α) + flood*α; at α=1: out = flood; terminal element is feBlend, not feFlood; feFlood-specific terminal checks miss this; mode=lighten with white flood also produces all-white output (max(x,1)=1) | Same as FLD-001: entire consent panel replaced by flood color; the detection challenge is that the terminal element in the filter chain is feBlend, not feFlood — multi-primitive chain analysis required | High |
SA-CSS-FLD-004: dynamic flood-opacity mutation |
flood-opacity=0 initially (harmless); mutated to 1 at interaction time via event handler, MutationObserver, or timer; CSSOM snapshot at scan time sees safe value; mutation occurs post-scan; static analysis passes; attack activates when user approaches Allow button | The audit mechanism itself — dynamic activation means the consent panel is safe during auditing and dangerous during user interaction; the presence of a feFlood element in the filter graph is the indicator, regardless of current flood-opacity value | High |
Defences
- Zero-feFlood policy on consent elements — prohibit any
feFloodelement in a filter applied to a consent panel, regardless offlood-opacity. AfeFloodis never needed for consent UI rendering; its only use in a consent filter graph is as an attack primitive or displacement map source. Flag its presence as CRITICAL regardless of current state. - Resolve filter URLs before auditing —
getComputedStyle(el).filterreturns only the URL string, not any filter attributes. Detection requires resolving the<filter>element and walking all child primitives. A filter URL that resolves to null indocument.querySelectorindicates closed shadow DOM — flag as HIGH. - Check all flood-opacity thresholds, not just opacity=1 — a white flood with
flood-opacity > 0.75collapses WCAG contrast below 4.5:1 for standard dark-on-white consent text. Use the full opacity range [0,1] rather than a binary opaque/transparent classification. - Monitor feFlood attribute mutations in real time — install a
MutationObserveron allfeFloodelements in consent element filter definitions; any mutation toflood-opacityorflood-colorafter page load is a dynamic activation indicator; disable the Allow button and alert on mutation. - Check all feBlend modes for solid-color in2 equivalence —
mode="normal"andmode="lighten"with a white feFlood in2 produce all-white output;mode="screen"with high-opacity white also washes toward white; the attack is not limited tomode="normal".
SkillAudit findings for this attack surface
feFlood flood-color="white" flood-opacity="1" as terminal primitive on the consent element — entire filter region output is a solid white rectangle; all consent text, permission scopes, and button labels are overwritten; DOM content unchanged and accessible to assistive technology; visual consent rendering completely erased; getComputedStyle(el).filter returns only filter URL, requiring URL resolution to detect.feFlood flood-opacity="0.95" near-opaque variant — dark consent text composited to ≈ rgb(243,243,244) on white background; WCAG contrast ≈ 1.1:1; effectively invisible; bypasses detection rules checking only flood-opacity="1"; correct threshold for 4.5:1 AA fail on dark-on-white consent text is flood-opacity > 0.75.feFlood in2 + feBlend mode="normal" — terminal element is feBlend, not feFlood; blend formula at flood-opacity=1 produces flood output regardless of SourceGraphic; feFlood-specific terminal checks miss this pattern; multi-primitive chain analysis required; mode=lighten with white flood also produces all-white output.flood-opacity mutation — flood-opacity initialized to 0 (safe at scan time), mutated to 1 at interaction time via event handler or MutationObserver; CSSOM captures initial safe value; static analysis passes; runtime activation erases consent panel when user approaches Allow button; zero-feFlood policy is the only reliable mitigation.