MCP server SVG feFlood + feComposite consent security
Four SA-SVG-FFL consent attack patterns using SVG feFlood and feComposite filter primitives to erase consent text. The feFlood primitive produces a solid-color image filling the entire filter region — a white flood on a black consent text produces white-on-white invisibility when combined with feComposite operator="in" which clips the flood to the source graphic alpha channel. Unlike CSS color attacks that operate in the CSSOM, these attacks live entirely in SVG filter XML and require SVG-aware detection logic.
SVG filter primitives form a compositing pipeline: each primitive takes input images, applies a transformation, and outputs a result image that the next primitive can consume. feFlood is the simplest input-generator: it ignores all inputs and produces a solid rectangle of any color and opacity filling the filter primitive subregion. Combined with feComposite, the Porter-Duff compositing operator, feFlood becomes the basis for several powerful consent-text erasure attacks. The key insight is that feComposite operator="in" computes output = source1 × alpha(source2): the alpha channel of the second input acts as a mask for the first. When source1 is a white flood and source2 is dark consent text, the output is the white flood clipped to the exact shape of each text glyph — producing white glyphs on a white background.
SA-SVG-FFL-001: feFlood white + feComposite operator=in glyph erasure
The canonical attack: feFlood generates a pure white image; feComposite in="flood" in2="SourceGraphic" operator="in" clips the white flood to the alpha channel of the source text. The result is white text on a white background. Every pixel that was a text glyph is now white; every pixel that was background remains as-is. The consent text is present in the DOM, the font-size is non-zero, the element has layout dimensions, but the rendered output is invisible.
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="white-glyph">
<feFlood flood-color="white" flood-opacity="1" result="flood"/>
<feComposite in="flood" in2="SourceGraphic" operator="in" result="white-text"/>
</filter>
</defs>
</svg>
<p class="consent-text" style="filter:url(#white-glyph)">
By clicking Allow you grant this MCP server access to your files.
</p>
<!-- SVG feComposite operator="in" formula:
output_rgba(x,y) = in_rgba(x,y) * alpha(in2_rgba(x,y))
flood rgba: (255, 255, 255, 1.0) — solid white everywhere
SourceGraphic at a text glyph pixel: e.g. (0, 0, 0, 1.0) — black opaque
feComposite operator=in result: (255, 255, 255, 1.0) — white opaque
→ glyph pixels are white
SourceGraphic at a background pixel: e.g. (255, 255, 255, 0.0) — transparent
feComposite operator=in result: (255, 255, 255, 0.0) — white transparent
→ background pixels are still transparent (no fill)
Net result: white glyphs on transparent (or white page) background.
The consent text is completely invisible against any white or light background.
Against a dark background the text would appear white — so attackers
always ensure the page background is white or near-white.
DOM inspection reveals:
- element.textContent: full consent text present
- element.offsetWidth, offsetHeight: non-zero
- getComputedStyle(el).filter: "url(#white-glyph)"
- getComputedStyle(el).fontSize: normal (e.g., "16px")
- getComputedStyle(el).color: may report "rgb(0, 0, 0)" — the filter
overrides the rendered color but not the computed CSS color value
Detection requires:
1. Identify elements with filter:url(#...) applied
2. Parse the referenced SVG filter definition
3. Find feFlood primitives with flood-color near white/background-color
4. Find feComposite primitives consuming that feFlood output as "in"
5. If feComposite operator="in" and in2="SourceGraphic": color erasure attack
→ HIGH to CRITICAL depending on whether text is consent-critical -->
CSS color ≠ rendered color: When an SVG filter is applied via filter:url(), getComputedStyle(el).color still reports the CSS color property value (e.g., rgb(0,0,0)), not the actual rendered pixel color. A security audit that reads computedStyle.color and checks contrast ratios will find correct contrast for text that is actually rendered white-on-white by an SVG filter. SVG filter detection requires parsing the <filter> SVG element referenced by the CSS filter property — it cannot be determined from CSSOM alone.
feFlood flood-color="white" + feComposite operator="in" in2="SourceGraphic": white flood clipped to glyph alpha produces white glyphs on white background; CSS color property reports correct value; rendered text invisible. Detection: parse SVG filter for feFlood near-white + feComposite operator=in → CRITICAL.SA-SVG-FFL-002: feFlood + feComposite operator=over total coverage
While operator="in" clips the flood to text glyph shapes, operator="over" is a full-coverage attack: the flood layer is composited over the SourceGraphic unconditionally. A white flood with operator="over" covers every pixel in the filter region with white, obliterating the consent text completely. Unlike operator=in (which preserves background transparency), operator=over produces a solid white rectangle where the consent text was, which can be more visually conspicuous but is equally effective at hiding content.
<filter id="white-cover">
<feFlood flood-color="white" flood-opacity="1" result="flood"/>
<feComposite in="flood" in2="SourceGraphic" operator="over" result="covered"/>
</filter>
<!-- feComposite operator="over" formula:
output = in + in2 * (1 - alpha(in))
When in is the flood (flood-opacity=1.0, fully opaque):
output = flood + SourceGraphic * (1 - 1.0) = flood + SourceGraphic * 0
output = flood
The SourceGraphic is entirely discarded — multiplied by zero.
The entire filter region becomes the flood color (white).
Use case for attackers vs operator=in:
- operator=in: white glyphs on transparent background — invisible on white page,
but visible on dark backgrounds (white text on dark bg is readable)
- operator=over: white rectangle covering all text — invisible everywhere,
even on dark backgrounds (white box appears, text is gone)
- Attackers on light-background pages: either operator works
- Attackers on dark-background pages: must use operator=over for erasure,
but the white rectangle becomes a visible anomaly
Variant using flood-color matching page background:
feFlood flood-color="#1a1a1a" (dark page background color) + operator=over
→ text region becomes same color as page background
→ invisible on dark backgrounds without creating a white rectangle anomaly
Detection:
feFlood flood-color close to page background-color + operator=over
→ total erasure; compare flood-color to document.body background-color
→ if within a 5% luminance delta → CRITICAL SA-SVG-FFL-002 -->
operator=over is background-specific: A white flood with operator="over" is only invisible on white or near-white pages. Attackers must match flood-color to the page background color to avoid creating a visible white rectangle. Detection should therefore compare feFlood flood-color against the effective background color of the filtered element, not just check for white. Flood colors within a luminance distance of 5% from the element background color should be flagged regardless of the exact value.
feFlood + feComposite operator="over": flood layer composited over SourceGraphic with full opacity erases all text; flood-color matched to page background makes region invisible. Detection: feFlood flood-color within 5% luminance of element background + operator=over → CRITICAL.SA-SVG-FFL-003: near-white flood-opacity for threshold evasion
A flood-opacity of 1.0 produces a fully opaque erasure that some automated scanners flag by checking flood-opacity for exact 1.0. Setting flood-opacity to 0.97 or 0.95 evades these threshold checks while still producing consent text that fails WCAG contrast requirements. At flood-opacity=0.97, a white flood over black text produces a pixel value of approximately rgb(247, 247, 247) — luminance 0.937 — while the background is rgb(255, 255, 255) — luminance 1.0. The resulting contrast ratio is about 1.07:1, well below the 4.5:1 WCAG AA minimum and barely distinguishable by any user.
<filter id="near-white-glyph">
<feFlood flood-color="white" flood-opacity="0.97" result="flood"/>
<feComposite in="flood" in2="SourceGraphic" operator="in" result="near-white-text"/>
</filter>
<!-- Near-white flood-opacity calculation:
Original text pixel: rgba(0, 0, 0, 1.0) — black glyph
feComposite operator=in result: applies flood to glyph alpha channel
feFlood output at glyph: rgba(255, 255, 255, 0.97)
feComposite operator=in output: rgba(255, 255, 255, 0.97)
Rendered against white page background:
Composited: white_bg + near_white_text * (1 - alpha_of_bg)
For opaque white background: rgb(255,255,255) + rgba(255,255,255,0.97) * 0
Actually: alpha compositing of rgba(255,255,255,0.97) over white background:
R = 255 * 0.97 + 255 * (1 - 0.97) = 255 * 0.97 + 255 * 0.03 = 255
→ All channels round to 255 → rendered as solid white
Against non-white background (e.g. rgba(240,240,240)):
R = 255 * 0.97 + 240 * 0.03 = 247.35 + 7.2 = 254.55 ≈ 255
→ Still effectively white
WCAG contrast check:
Luminance of rgb(247,247,247) = 0.937
Luminance of rgb(255,255,255) = 1.0
Contrast ratio = (1.0 + 0.05) / (0.937 + 0.05) = 1.05 / 0.987 ≈ 1.06:1
→ Far below 4.5:1 WCAG AA (normal text), 3:1 WCAG AA (large text)
Scanner evasion: a scanner checking flood-opacity > 0.99 misses flood-opacity=0.97.
Correct detection threshold: any flood-opacity >= 0.85 near a background color
produces visually unreadable text via operator=in.
Additional evasion: flood-color as hex near-white (#fefefe, #fdfdfd)
instead of the keyword "white" evades keyword-based scanners. -->
Threshold evasion via near-opaque values: Scanners that check for flood-opacity="1" (exact) or flood-color="white" (keyword) miss near-white flood attacks. Correct detection requires computing the effective rendered color after compositing by accounting for the flood-opacity, the operator, and the page background color. Any feFlood + feComposite pattern that would produce a rendered text contrast ratio below 4.5:1 should be flagged regardless of the exact opacity value.
flood-opacity="0.97" with feComposite operator="in": produces rendered text with ~1.06:1 contrast ratio; evades scanners checking for exact flood-opacity=1.0 or keyword "white". Detection: simulate composited color from feFlood flood-color × flood-opacity against background; check WCAG contrast ratio → HIGH if below 4.5:1.SA-SVG-FFL-004: chained feFlood pairs for double-layer erasure
A more sophisticated attack chains two feFlood + feComposite operations: the first flood creates a white background layer over the filter region; the second flood with operator="in" makes text glyphs white against that background. This separates the background-erasure and glyph-erasure into two distinct primitives, making the attack harder to characterize as a single pattern. Some filters use this to embed innocuous-looking transformations (color shift, brightness) between the two erasure steps to further obscure the intent.
<filter id="double-erase">
<!-- Step 1: create white background layer -->
<feFlood flood-color="white" flood-opacity="1" result="bg-flood"/>
<feComposite in="bg-flood" in2="SourceGraphic" operator="over" result="white-bg"/>
<!-- Step 2: make glyphs white using a second flood + operator=in -->
<feFlood flood-color="white" flood-opacity="1" result="glyph-flood"/>
<feComposite in="glyph-flood" in2="white-bg" operator="in" result="double-erased"/>
</filter>
<!-- Why chain two operations?
Single feFlood operator=in: white glyphs on transparent background.
→ Detectable: a single feFlood + feComposite pattern.
→ On a dark background: renders as white text (visible, not hidden).
Chained pair:
Step 1 (operator=over): turns entire filter region white — background and text both white.
Step 2 (operator=in on white-bg): clips white flood to alpha channel of white-bg result.
Since white-bg has opaque white everywhere (from step 1), the second operator=in
produces white everywhere — identical end result but expressed as two operations.
Security analysis: end-to-end result is the same (white rectangle).
The purpose is to fool checkers that look for specific single-step patterns.
Detection approach for chained primitives:
Don't check individual primitives in isolation.
Simulate the entire filter pipeline: feed test inputs (black text on transparent bg)
through all primitives sequentially and measure the output luminance.
If output luminance of glyph pixels exceeds background by less than WCAG threshold
→ consent erasure attack regardless of implementation complexity.
Implementation: build a virtual filter evaluator using Canvas 2D API:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Draw reference text in black on transparent background
ctx.fillStyle = '#000';
ctx.font = '16px sans-serif';
ctx.fillText('test', 0, 20);
// Apply SVG filter via CSS filter on the canvas element,
// then readback pixels with getImageData and check contrast -->
End-to-end filter simulation: The most reliable detection for chained SVG filter attacks is end-to-end simulation: render a known test image (black text on transparent background) through the filter pipeline using a Canvas element or SVG renderer, then measure the pixel output with getImageData(). This approach is immune to obfuscation via chaining, intermediate transformations, or unconventional operator sequences because it measures the observable rendering effect rather than the structural pattern.
operator="over" creates white background, second feFlood + operator="in" makes glyphs white; end-to-end effect is complete text erasure; each step appears innocuous in isolation. Detection: simulate full filter pipeline with test input and measure output glyph contrast → CRITICAL if below 1.5:1.Summary of SA-SVG-FFL findings
| ID | Severity | Pattern | Mechanism | Detection method |
|---|---|---|---|---|
| SA-SVG-FFL-001 | CRITICAL | feFlood white + feComposite operator=in | White flood clipped to glyph alpha → white glyphs on white background | Parse SVG filter: feFlood near-white + feComposite operator=in |
| SA-SVG-FFL-002 | CRITICAL | feFlood + feComposite operator=over | Flood covers SourceGraphic entirely; flood-color matches page background | feFlood flood-color within 5% luminance of background + operator=over |
| SA-SVG-FFL-003 | HIGH | Near-white flood-opacity=0.97 threshold evasion | Sub-1.0 opacity evades exact-match scanners; rendered contrast ~1.06:1 | Simulate composited color; check WCAG contrast ratio |
| SA-SVG-FFL-004 | CRITICAL | Chained feFlood pairs double-layer erasure | Two-step pipeline hides erasure from single-pattern detection | End-to-end filter simulation via Canvas getImageData |