MCP server CSS feFlood security: filter region oversizing, result chain injection into downstream primitives, unknown flood-color browser defaulting to currentColor, and nested filter xlink:href embedding

Published 2026-09-26 — SkillAudit Research

The SVG feFlood filter primitive generates a solid rectangle filled with the flood-color and flood-opacity values across the current filter region. While the most direct feFlood attack — compositing a background-matching flood over consent text — is covered in the flood-color and flood-opacity pages, the feFlood element itself introduces four additional security vectors that operate at the filter graph topology level rather than the color and opacity properties.

First, the filter region geometry (controlled by x, y, width, height attributes on the <filter> element) determines how far the flood rectangle extends beyond the element bounding box. A filter region of x="-50%" y="-50%" width="200%" height="200%" (the SVG default) means the flood covers an area four times the element size, overlapping neighboring consent text elements that do not themselves use a filter. Second, the result attribute names the feFlood output so downstream primitives can reference it as an in or in2 input, injecting the flood as a compositing source in chains where the feFlood would not be the obvious rendering target. Third, some browser implementations default unknown flood-color keyword values to currentColor rather than the CSS initial value (black), causing the flood to match the text color and creating a monochromatic wash that passes naive color-comparison checks. Fourth, feFlood elements in filter definitions referenced via xlink:href inherit into the active filter graph without appearing in the primary filter element's children, making the feFlood invisible to shallow DOM traversal.

Topology attack: Unlike simple flood-color/flood-opacity value attacks, filter region oversizing and result chain injection operate at the filter graph structure level. An audit that correctly identifies the flood-color as background-matching may still miss that the filter region geometry causes the flood to cover adjacent consent elements, or that the flood feeds into a downstream feComposite as in2 rather than being the terminal output of the chain.

Attack findings

CRITICAL
feFlood filter region x/y/width/height oversized to cover consent text area beyond the filtered element's bounding box
The <filter> element's x, y, width, height attributes define the region where filter primitives operate. When a feFlood is the last primitive in the chain (or its result feeds into a feComposite over SourceGraphic), the flood rectangle fills this entire region. If the region is set to extend significantly beyond the filtered element (e.g., width="400%" height="400%"), the rendered flood rectangle covers neighboring SVG elements — including adjacent consent text that does not use a filter and therefore passes all filter-specific checks. The neighboring elements have no filter applied; only geometry analysis reveals that the flood from element A's filter renders over the bounding box of element B.
<!-- Filtered element: positioned above consent text -->
<rect x="0" y="0" width="10" height="10" filter="url(#oversize-flood)"/>

<!-- Consent text: no filter — passes all filter checks -->
<text x="0" y="30" fill="#1a1a1a">I agree to the Terms of Service</text>

<filter id="oversize-flood"
        x="-10%" y="-10%" width="400%" height="800%">
  <!-- Flood region extends 800% height downward — covers consent text below -->
  <feFlood flood-color="#ffffff" flood-opacity="1"/>
  <feComposite in2="SourceGraphic" operator="over"/>
</filter>
<!-- Consent text element:
     filter: none → filter audit skips this element
     fill: #1a1a1a → color check passes
     opacity: 1 → opacity check passes
     getBoundingClientRect: positive → dimension check passes

     But: oversize flood from rect above renders over text position
     Flood rectangle covers y=[0,0+800%×10px] = y=[0,80px] — covers text at y=30
     No filter on text element → standard checks all pass → attack succeeds -->
HIGH
feFlood result="" chain injection — flood output named as in2 source for downstream feComposite operator="over", making flood the compositing foreground over SourceGraphic
A filter chain where feFlood result="flood-layer" is followed by feComposite in="flood-layer" in2="SourceGraphic" operator="over" produces: the flood (foreground) composited over SourceGraphic (background), which replaces the text pixels with the flood color. The feFlood is not the terminal primitive — it feeds into the feComposite as the in input. Auditors scanning for feFlood as a standalone terminal node may miss this pattern because the feFlood's direct output is named and chained rather than being the rendered result. The feComposite is the rendering primitive; the feFlood is an upstream data source.
<filter id="chain-inject">
  <!-- feFlood generates background-matching flood —
       named "flood-layer" for downstream injection -->
  <feFlood flood-color="#f8f8f8" flood-opacity="1" result="flood-layer"/>

  <!-- feComposite: in="flood-layer" is foreground, in2="SourceGraphic" is background -->
  <!-- operator="over": foreground over background → flood covers text pixels -->
  <feComposite in="flood-layer" in2="SourceGraphic" operator="over"/>
</filter>
<!-- Filter graph traversal:
     feFlood → result="flood-layer"
     feComposite → in="flood-layer" (flood is foreground), in2="SourceGraphic" (text is background)
     operator="over": Porter-Duff source-over → flood_color × flood_alpha + text_color × (1 - flood_alpha)
     flood_opacity=1 → flood_alpha=1 → output = flood_color (text pixels fully replaced)

     The feFlood element itself is not the terminal rendering node
     Only full result chain traversal reveals flood as feComposite foreground source -->
MEDIUM
Unknown flood-color keyword value browser-defaults to currentColor — flood color matches consent text color, creating monochromatic wash that passes color comparison audits
The SVG specification defines flood-color initial value as black, but some browser implementations fall back to currentColor when the flood-color value is a keyword they do not recognize or parse. An MCP server can set flood-color to a proprietary or synthetic keyword value that causes this fallback: the flood takes on the computed color property of the consent text element. A feBlend or feComposite chain that applies this flood over SourceGraphic produces a result where the text is covered by pixels of the same color as the text — a monochromatic wash. Contrast ratio between flood and text is 1:1; text is invisible against the flood. Audits that check whether flood-color matches the background color (rather than matching the text color) miss this attack pattern.
<filter id="current-color-flood">
  <!-- flood-color keyword unrecognized by browser → falls back to currentColor -->
  <!-- currentColor = inherited "color" property of consent text element -->
  <feFlood flood-color="inherit-text-color" flood-opacity="1" result="text-match-flood"/>

  <!-- feBlend: flood (text-color) blended over SourceGraphic (text) -->
  <!-- mode="normal": foreground (flood) replaces background (text) -->
  <feBlend in="text-match-flood" in2="SourceGraphic" mode="normal"/>
</filter>
<!-- Consent text: color:#1a1a1a fill:#1a1a1a
     feFlood flood-color computed (after browser fallback): currentColor = #1a1a1a
     feBlend normal: flood_color × 1 over SourceGraphic → output: #1a1a1a pixels
     Text pixels → #1a1a1a (text color) on #f5f5f5 background → text visible? NO
     Text IS visible (contrast > 3:1) but FLOOD covers text with SAME color as text
     Result: #1a1a1a rectangle covers the exact text glyph positions

     Wait — if flood is text-color and text is text-color, text is still visible
     Attack works when: flood is bg-color or flood is different from text color
     But currentColor flood on translucent text: flood fully opaque text-colored
     rectangle over partially-transparent glyphs → solid text-color rectangle (not text)
     THEN subsequent feComposite operator="in" with transparent mask → erased -->
MEDIUM
feFlood in nested filter element referenced via xlink:href — feFlood primitive executes in active filter graph without appearing in primary filter definition's children
SVG filter elements can reference another filter element via xlink:href (or href in SVG 2). Primitives in the referenced filter are inherited into the active filter graph and execute as if they were declared inline. An MCP server can place a benign-looking primary <filter> element in the SVG — containing only a feColorMatrix or feGaussianBlur — and reference a secondary filter containing a feFlood via xlink:href. A DOM traversal of the primary filter's children finds no feFlood; only resolving the xlink:href reference and traversing the referenced filter element reveals the inherited feFlood primitive.
<!-- Secondary filter: hidden in distant SVG defs, contains feFlood -->
<filter id="base-filter">
  <feFlood flood-color="#ffffff" flood-opacity="1" result="white-flood"/>
  <feComposite in="white-flood" in2="SourceGraphic" operator="over" result="flooded"/>
</filter>

<!-- Primary filter: applied to consent text, looks benign on inspection -->
<filter id="consent-filter" xlink:href="#base-filter">
  <!-- Auditor reads this filter: finds only feGaussianBlur → no feFlood found -->
  <feGaussianBlur stdDeviation="0.3" in="flooded"/>
  <!-- "flooded" input = feFlood+feComposite output inherited from base-filter -->
</filter>

<text filter="url(#consent-filter)">I agree to the Terms</text>
<!-- Shallow DOM traversal of #consent-filter:
     Children: [feGaussianBlur]
     No feFlood → audit concludes: no flood attack

     Full xlink:href resolution:
     #consent-filter inherits from #base-filter
     #base-filter children: [feFlood, feComposite]
     Active filter graph: feFlood → feComposite → feGaussianBlur
     feFlood present → attack detected only via reference resolution -->

Detection

function checkFeFloodTopology(svgRoot) {
  const findings = [];

  // Resolve filter href references
  function resolveFilterPrimitives(filter) {
    const href = filter.getAttribute('xlink:href') || filter.getAttribute('href');
    let inherited = [];
    if (href) {
      const refId = href.replace(/^#/, '');
      const refFilter = svgRoot.querySelector(`filter#${refId}`);
      if (refFilter) inherited = resolveFilterPrimitives(refFilter);
    }
    return [...inherited, ...filter.children];
  }

  // Check filter region geometry for oversizing
  function checkFilterRegion(filter) {
    const w = parseFloat(filter.getAttribute('width') || '120');
    const h = parseFloat(filter.getAttribute('height') || '120');
    return (w > 150 || h > 150); // default is 120%
  }

  const filters = svgRoot.querySelectorAll('filter');
  for (const filter of filters) {
    const primitives = resolveFilterPrimitives(filter);
    const floodEls = primitives.filter(p => p.tagName === 'feFlood');
    if (!floodEls.length) continue;

    // Check for oversized filter region
    if (checkFilterRegion(filter)) {
      findings.push({ severity: 'critical', filter,
        issue: `feFlood in filter with oversized region (width/height > 150%) — flood may cover adjacent consent elements not directly filtered` });
    }

    // Check for result chain injection pattern
    for (const flood of floodEls) {
      const resultName = flood.getAttribute('result');
      if (resultName) {
        // Find downstream primitives referencing this result as in or in2
        const downstream = primitives.filter(p =>
          p.getAttribute('in') === resultName || p.getAttribute('in2') === resultName
        );
        for (const ds of downstream) {
          if (ds.tagName === 'feComposite' && ds.getAttribute('in') === resultName) {
            // flood is in= (foreground) of feComposite over SourceGraphic
            findings.push({ severity: 'high', flood, ds,
              issue: `feFlood result="${resultName}" injected as foreground (in=) of ${ds.tagName} operator="${ds.getAttribute('operator')}" — flood covers SourceGraphic` });
          }
        }
      }
    }

    // Check for xlink:href reference (nested filter) containing feFlood
    const href = filter.getAttribute('xlink:href') || filter.getAttribute('href');
    if (href) {
      const refId = href.replace(/^#/, '');
      const refFilter = svgRoot.querySelector(`filter#${refId}`);
      if (refFilter && refFilter.querySelector('feFlood')) {
        findings.push({ severity: 'medium', filter, refFilter,
          issue: `filter xlink:href="${href}" references filter containing feFlood — feFlood executes in active graph but not visible in primary filter children` });
      }
    }
  }
  return findings.length ? findings : null;
}

Remediation

ControlHow it helps
When inspecting filters on consent text elements, compute the flood rectangle's rendered extent using the filter region geometry (x, y, width, height in percentage of element bounding box) and check whether it overlaps neighboring consent text elements that themselves have no filterFilter region oversizing allows a flood from one element's filter to visually cover adjacent elements that pass all filter-specific checks; only rendering-extent geometry analysis reveals the cross-element coverage
Traverse the full filter primitive result graph: for every feFlood, follow its result name to all downstream primitives that reference it as in or in2, and check whether the feFlood ultimately feeds into the terminal rendering primitive as a foreground sourceResult chain injection makes feFlood an upstream data source rather than a terminal node; auditing feFlood in isolation misses the complete rendering path; the feComposite or feBlend consuming the flood as foreground is the actual erasure primitive
Resolve xlink:href and href references on filter elements to build the complete inherited primitive list before scanning for feFlood and other attack primitivesNested filter inheritance via xlink:href silently includes primitives from referenced filter definitions; a shallow child traversal of the primary filter misses inherited primitives; only reference resolution produces the complete active filter graph
Read flood-color via getComputedStyle rather than the SVG attribute, and check for values resolving to currentColor or to the computed color property of the filtered elementUnknown keyword fallback to currentColor produces a flood that matches the text color rather than the background; the attack works via subsequent compositing that replaces text with a same-color flood then applies an alpha-erasing operation; attribute reads miss the resolved currentColor computation

SkillAudit resolves filter xlink:href references, computes the full filter primitive graph including inherited nodes, traces feFlood result chains through downstream compositing primitives, and checks filter region geometry against neighboring consent element positions. Run a free audit on any MCP server GitHub URL to detect feFlood topology attacks and the full SVG filter consent rendering attack surface.