Security Guide

MCP server SVG <filter> region consent security — feFlood expanded region, feOffset clipping, negative coordinates, and filterUnits="userSpaceOnUse"

The x, y, width, and height attributes on the SVG <filter> element define the filter region — the rectangular canvas on which filter primitives operate and outside which all filter output is clipped to transparent. By manipulating these four attributes, attackers can cause filter primitives to cover elements far beyond the filtered element’s own bounds, or force the compositor to discard the filtered element’s own content entirely by clipping it to transparent. Unlike filter primitive attacks (which target the per-primitive inputs and outputs), filter region attacks exploit the coordinate-system rules that govern where filter output is rendered and which surrounding pixels it can affect. Four attack vectors exploit filter region geometry against MCP consent UI.

Attack 1: feFlood with 200% expanded filter region covering adjacent elements (SA-CSS-FLR-001)

The SVG filter region defaults to x="-10%" y="-10%" width="120%" height="120%" relative to the filtered element’s bounding box — a small safety margin that prevents blur halos from being clipped at the element edge. By setting x="-50%" y="-50%" width="200%" height="200%", an attacker expands the filter region to extend 50% of the element’s bounding box beyond each edge in all four directions. For a 200×40px consent text element, this filter region covers a 300×120px area centered on the element. A feFlood fill="white" flood-opacity="1" primitive fills the entire filter region — not just the filtered element’s own 200×40px area, but the full 300×120px canvas including any adjacent consent lines, button labels, or UI elements that fall within the expanded region. The feFlood output is composited back into the document at full opacity, painting over everything within the 300×120px filter region regardless of which element those pixels belong to. A static attribute audit finds filterEl.getAttribute('width') returning "200%" — any filter with width or height exceeding 150% combined with a feFlood child should be flagged as CRITICAL.

<!-- SA-CSS-FLR-001: feFlood with 200% expanded filter region
     x="-50%" y="-50%" width="200%" height="200%": filter region extends 50% beyond each edge
     For a 200x40px consent text element: filter region = 300x120px centered on element
     feFlood fill="white" flood-opacity="1": fills ALL 300x120px with white
     Adjacent consent lines and UI labels within 300x120px area are painted over
     Default filter region is x="-10%" y="-10%" width="120%" height="120%"
     This attack doubles the area beyond what the element itself occupies -->

<defs>
  <filter id="expand-flood"
          x="-50%" y="-50%"
          width="200%" height="200%">
    <feFlood flood-color="white" flood-opacity="1" result="flood"/>
    <feComposite in="flood" in2="SourceGraphic" operator="over"/>
    <!-- feComposite with operator="over": flood paints over SourceGraphic
         The flood fills the entire 200% filter region canvas, not just the element bounds
         Adjacent elements within the expanded region are covered by the white flood -->
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#111"
      filter="url(#expand-flood)">
  This skill requests: shell execution, credential access, filesystem write
</text>

// Detection: read filter x/y/width/height; flag width/height > 150% with feFlood child
function detectExpandedFilterRegion(svgEl) {
  const findings = [];
  for (const filterEl of svgEl.querySelectorAll('filter')) {
    const w = parseFloat(filterEl.getAttribute('width') || '120%');
    const h = parseFloat(filterEl.getAttribute('height') || '120%');
    const hasFeFlood = filterEl.querySelector('feFlood') !== null;
    if (hasFeFlood && (w > 150 || h > 150)) {
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-FLR-001',
        note: `<filter width="${w}%" height="${h}%"> with feFlood — filter region expands ${w-100}% / ${h-100}% beyond element bounds; feFlood fills entire extended region including adjacent elements; filter width/height > 150% with feFlood child is CRITICAL`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-FLR-001: A <filter> with width="200%" height="200%" and a feFlood child paints a white rectangle over a 300×120px area centered on the 200×40px consent text element, covering adjacent consent lines and UI labels within that expanded region. The attack is detectable statically: read filterEl.getAttribute('width') and filterEl.getAttribute('height'), parse as percentages, and flag any filter with width or height exceeding 150% that also contains a feFlood child as CRITICAL.

Attack 2: feOffset moves SourceGraphic outside filter region → clipped to transparency (SA-CSS-FLR-002)

The SVG filter region defines the rectangular canvas on which filter primitives operate; any filter output that falls outside this rectangle is clipped to transparent before being composited back into the document. When feOffset dx="500" dy="0" is applied to SourceGraphic, the rendered content of the consent text element is shifted 500px to the right within the filter canvas. If the filter region is defined as x="0%" y="0%" width="100%" height="100%" (covering only the element’s own bounding box), the shifted content at x + 500 lies entirely outside the filter region rectangle, and the compositor clips it to transparent. The result: the filtered element’s own content is discarded, and because the filter has no other primitive producing visible output in the filter region (such as a flood or merge of the original), the element renders as a fully transparent rectangle. Critically, this element’s computed opacity is 1, its fill is normal, and its visibility is visible — standard CSSOM checks find nothing suspicious. The filter region itself (100% width and height) looks benign on a surface audit; the attack lives in the feOffset displacement moving content outside it.

<!-- SA-CSS-FLR-002: feOffset moves SourceGraphic outside filter region → transparent
     Filter region: x="0%" y="0%" width="100%" height="100%" (element bounds only)
     feOffset dx="500": shifts SourceGraphic 500px right within the filter canvas
     For a 200px-wide element: shifted content spans x=500 to x=700
     Filter region only covers x=0 to x=200 — shifted content is entirely outside
     Compositor clips anything outside filter region to transparent
     Result: the consent text element renders as fully transparent
     Computed opacity=1; fill="normal"; visibility="visible" — CSSOM checks miss this -->

<defs>
  <filter id="offset-clip"
          x="0%" y="0%"
          width="100%" height="100%">
    <feOffset in="SourceGraphic" dx="500" dy="0" result="shifted"/>
    <!-- "shifted" result occupies x=500 to x=700 within the filter canvas
         Filter region covers x=0 to x=200 only
         "shifted" is entirely outside the filter region → clipped to transparent
         No feFlood, feComposite, or feMerge produces content within the 0-200px region
         The filter output for the entire filter region is transparent -->
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#111"
      filter="url(#offset-clip)">
  Requesting: shell execution, credential access, filesystem write
</text>

// Detection: for each feOffset in a filter, compare displacement to filter region dimensions
function detectOffsetOutsideRegion(svgEl) {
  const findings = [];
  for (const filterEl of svgEl.querySelectorAll('filter')) {
    const fw = parseFloat(filterEl.getAttribute('width') || '120');
    const fh = parseFloat(filterEl.getAttribute('height') || '120');
    for (const offsetEl of filterEl.querySelectorAll('feOffset')) {
      const dx = Math.abs(parseFloat(offsetEl.getAttribute('dx') || '0'));
      const dy = Math.abs(parseFloat(offsetEl.getAttribute('dy') || '0'));
      // feOffset displacement in userSpaceOnUse or proportional estimate
      if (dx > 200 || dy > 200) {
        findings.push({
          severity: 'CRITICAL', code: 'SA-CSS-FLR-002',
          note: `feOffset dx="${offsetEl.getAttribute('dx')}" dy="${offsetEl.getAttribute('dy')}" in filter with region ${fw}%x${fh}% — large displacement likely moves SourceGraphic outside filter region; compositor clips displaced content to transparent; element may render invisible with opacity=1`
        });
      }
    }
  }
  return findings;
}

CRITICAL — SA-CSS-FLR-002: A feOffset dx="500" inside a <filter width="100%" height="100%"> moves the consent text element’s rendered content entirely outside the filter region, causing the compositor to clip it to transparent. The element remains in the DOM with opacity=1, normal fill, and visibility=visible — all standard CSSOM checks pass. Detection must inspect feOffset children of filter elements, extract dx and dy attribute values, and flag any displacement exceeding 200px as CRITICAL when the filter region does not expand to accommodate the offset.

Attack 3: negative width/height on <filter> — implementation-defined clipping behavior (SA-CSS-FLR-003)

The SVG specification does not define behavior for negative values of the width or height attributes on a <filter> element. In the Chromium rendering engine (used by Chrome, Edge, Brave, Opera, and Arc — representing the majority of desktop browser market share), a filter element with width="-100%" or height="-100%" causes the filter region to collapse to zero area. A zero-area filter region means no filter output lies within the region, and the entire filtered element is clipped to transparent — the same effect as the feOffset displacement attack, but triggered by a single malformed attribute. In Firefox, negative filter dimensions produce different or undefined behavior; rendering outcomes may vary. The attack is rated MEDIUM rather than CRITICAL because it relies on implementation-specific behavior: a technically non-conforming SVG attribute whose effect is browser-dependent. However, because Chromium-based browsers hold a dominant market share, a negative-dimension filter that collapses to transparency in Chromium renders consent text invisible for the majority of users even if Firefox handles the value differently.

<!-- SA-CSS-FLR-003: negative width/height on <filter> — implementation-defined behavior
     x="-200%" y="-200%": negative x/y extends region in negative coordinate direction
     width="-100%" height="-100%": negative dimensions — NOT defined by SVG spec
     Chromium behavior: negative filter width/height collapses region to zero area
     Zero-area filter region → all filter output outside region → clipped to transparent
     Filtered element renders as fully transparent in Chromium (majority market share)
     Firefox behavior: undefined / may differ
     Static audit: parseFloat(filterEl.getAttribute('width')) returns -100 (negative) -->

<defs>
  <filter id="negative-dims"
          x="-200%" y="-200%"
          width="-100%" height="-100%">
    <!-- Chromium: region collapses to zero area → element transparent
         Any filter primitive here has no effect because the region is empty
         The attack works even with no filter primitives — the region itself is the weapon -->
    <feGaussianBlur stdDeviation="0"/>
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#111"
      filter="url(#negative-dims)">
  This skill requests: shell execution, credential access
</text>

// Detection: flag any filter with negative width or height attribute value
function detectNegativeFilterDimensions(svgEl) {
  const findings = [];
  for (const filterEl of svgEl.querySelectorAll('filter')) {
    const wAttr = filterEl.getAttribute('width');
    const hAttr = filterEl.getAttribute('height');
    const w = wAttr !== null ? parseFloat(wAttr) : null;
    const h = hAttr !== null ? parseFloat(hAttr) : null;
    if ((w !== null && w < 0) || (h !== null && h < 0)) {
      findings.push({
        severity: 'MEDIUM', code: 'SA-CSS-FLR-003',
        note: `<filter width="${wAttr}" height="${hAttr}"> has negative dimension — SVG spec does not define behavior for negative filter width/height; Chromium collapses region to zero area making filtered element transparent; implementation-defined behavior across browsers; negative filter dimension on consent element is MEDIUM`
      });
    }
  }
  return findings;
}

MEDIUM — SA-CSS-FLR-003: A <filter width="-100%" height="-100%"> is not defined by the SVG specification. In Chromium (the rendering engine for Chrome, Edge, and other browsers representing the majority of desktop market share), negative filter dimensions collapse the filter region to zero area, causing the filtered element to render as fully transparent. Detection is straightforward: parse filterEl.getAttribute('width') and filterEl.getAttribute('height') as numbers and flag any negative value as MEDIUM. Rate as CRITICAL if the implementation behavior can be confirmed as transparency-producing in the target browser.

Attack 4: filterUnits="userSpaceOnUse" with large absolute coordinates encompassing the entire viewport (SA-CSS-FLR-004)

The filterUnits attribute on <filter> controls the coordinate system used for interpreting the x, y, width, and height attributes. The default value, filterUnits="objectBoundingBox", interprets these values as fractions of the filtered element’s own bounding box — so width="120%" means 120% of the element width. When filterUnits="userSpaceOnUse", the filter region coordinates are interpreted in the SVG viewport’s user coordinate system (i.e., pixels). Setting x="0" y="0" width="2000" height="2000" with filterUnits="userSpaceOnUse" defines a filter region that covers the entire 2000×2000px SVG viewport. A feFlood fill="white" flood-opacity="1" primitive fills all 2000×2000px with white. Every element in the SVG that references this filter will composite a 2000×2000px white rectangle back into the document, completely covering the entire SVG viewport including all consent text from any element, not just the filtered element itself. The attack is CRITICAL because: (a) a single filter definition applied to any one element in an SVG can hide the consent text of all other SVG elements; (b) the filterUnits="userSpaceOnUse" attribute is a clear attack signal that no legitimate consent UI requires; (c) a static attribute read of filterEl.getAttribute('filterUnits') returns the value unambiguously.

<!-- SA-CSS-FLR-004: filterUnits="userSpaceOnUse" with large absolute coordinates
     filterUnits="userSpaceOnUse": x/y/width/height in SVG viewport pixel coordinates
     x="0" y="0" width="2000" height="2000": filter region = entire 2000x2000px viewport
     feFlood fill="white" flood-opacity="1": fills all 2000x2000px with white
     Any element using this filter composites a white rectangle over the entire viewport
     ALL consent text in the SVG is hidden regardless of which element uses the filter
     Default filterUnits="objectBoundingBox" — switching to userSpaceOnUse is suspicious
     filterEl.getAttribute('filterUnits') returns "userSpaceOnUse" — detectable statically -->

<defs>
  <filter id="viewport-flood"
          filterUnits="userSpaceOnUse"
          x="0" y="0"
          width="2000" height="2000">
    <feFlood flood-color="white" flood-opacity="1" result="flood"/>
    <feComposite in="flood" in2="SourceGraphic" operator="over"/>
    <!-- The filter region is 2000x2000 viewport pixels
         feFlood fills the entire 2000x2000 region with white
         feComposite composites the flood over SourceGraphic — flood wins everywhere
         The white rectangle covers the full viewport when composited back into the document -->
  </filter>
</defs>

<!-- Only one element needs to reference this filter to hide the entire viewport -->
<rect x="0" y="0" width="1" height="1"
      filter="url(#viewport-flood)" opacity="0.001"/>
<!-- The rect itself is nearly invisible, but its filter output (2000x2000 white flood)
     is composited at full opacity over the entire SVG viewport -->

// Detection: flag filterUnits="userSpaceOnUse" with feFlood and large width/height
function detectUserSpaceFlood(svgEl) {
  const findings = [];
  for (const filterEl of svgEl.querySelectorAll('filter')) {
    const units = filterEl.getAttribute('filterUnits') || 'objectBoundingBox';
    if (units !== 'userSpaceOnUse') continue;
    const w = parseFloat(filterEl.getAttribute('width') || '0');
    const h = parseFloat(filterEl.getAttribute('height') || '0');
    const hasFeFlood = filterEl.querySelector('feFlood') !== null;
    if (hasFeFlood && (w > 500 || h > 500)) {
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-FLR-004',
        note: `<filter filterUnits="userSpaceOnUse" width="${w}" height="${h}"> with feFlood — filter region covers ${w}x${h}px of SVG viewport in absolute coordinates; feFlood fills entire viewport area; all SVG consent text covered regardless of which element references this filter`
      });
    } else if (hasFeFlood) {
      findings.push({
        severity: 'HIGH', code: 'SA-CSS-FLR-004',
        note: `<filter filterUnits="userSpaceOnUse"> with feFlood — userSpaceOnUse converts filter region to viewport coordinates; feFlood fills absolute-coordinate region; review width/height for viewport-scale coverage`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-FLR-004: A <filter filterUnits="userSpaceOnUse" width="2000" height="2000"> with a feFlood child composites a 2000×2000px white rectangle over the entire SVG viewport when any element references the filter — hiding all consent text in the SVG regardless of which element is filtered. Detection must check filterEl.getAttribute('filterUnits') for "userSpaceOnUse", then flag any such filter that also contains a feFlood child with width or height exceeding 500px as CRITICAL.

Summary table

AttackMechanismWhat it hidesSeverity
SA-CSS-FLR-001: feFlood 200% expanded filter region filter x="-50%" y="-50%" width="200%" height="200%" with feFlood flood-color="white" — filter region extends 50% of element bounding box beyond each edge; for a 200x40px element the filter canvas is 300x120px; feFlood fills all 300x120px; adjacent consent lines and UI labels within the expanded region are painted over; detectable via filterEl.getAttribute('width') returning "200%" All consent text within the 200% expanded filter region including adjacent elements not directly filtered; feFlood fills the entire extended canvas area Critical
SA-CSS-FLR-002: feOffset clipping SourceGraphic to transparency filter x="0%" y="0%" width="100%" height="100%" with feOffset dx="500" — SourceGraphic shifted 500px right lands outside the 100% filter region; compositor clips displaced content to transparent; no flood or merge produces visible content inside the region; element renders invisible; computed opacity=1, fill=normal, visibility=visible; only feOffset dx/dy attribute inspection detects the attack The filtered consent text element only (not adjacent elements); entire element rendered transparent; standard CSSOM property checks (opacity, fill, visibility) find nothing suspicious Critical
SA-CSS-FLR-003: negative filter width/height collapses region filter width="-100%" height="-100%" — negative dimensions not defined by SVG spec; Chromium collapses filter region to zero area making filtered element transparent; Firefox behavior undefined/may differ; MEDIUM because implementation-dependent; majority-market Chromium makes consent text invisible for most users; parseFloat on attribute returns negative value — detectable statically The filtered consent text element (Chromium-based browsers); transparency caused by zero-area filter region; no filter primitive required to achieve the effect in Chromium Medium
SA-CSS-FLR-004: filterUnits="userSpaceOnUse" viewport-scale feFlood filter filterUnits="userSpaceOnUse" x="0" y="0" width="2000" height="2000" with feFlood flood-color="white" — userSpaceOnUse converts coordinates to SVG viewport pixels; 2000x2000px filter region covers entire viewport; feFlood fills full viewport area; any element referencing this filter composites a 2000x2000 white rectangle; all SVG consent text hidden; filterEl.getAttribute('filterUnits') returns "userSpaceOnUse" — detectable statically All consent text in the entire SVG viewport regardless of which element references the filter; single filter definition applied to any one element (even a near-invisible rect) hides all consent text Critical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-FLR-001: <filter width="200%" height="200%"> with feFlood — filter region extends 100% beyond element bounds in all directions; feFlood fills the full 300x120px expanded canvas covering adjacent consent lines and UI labels; filter width/height > 150% with feFlood child requires static attribute inspection.
CRITICAL SA-CSS-FLR-002: feOffset dx="500" inside <filter width="100%"> — SourceGraphic displaced 500px outside the 100% filter region; compositor clips displaced content to transparent; filtered consent element renders invisible with computed opacity=1; feOffset dx/dy value inspection against filter region dimensions required.
MEDIUM SA-CSS-FLR-003: <filter width="-100%" height="-100%"> — negative filter dimensions collapse region to zero area in Chromium (majority market share); filtered element renders transparent; SVG-spec-undefined behavior; parseFloat attribute inspection flags negative values as MEDIUM.
CRITICAL SA-CSS-FLR-004: <filter filterUnits="userSpaceOnUse" width="2000" height="2000"> with feFlood — absolute-coordinate 2000x2000px filter region covers entire SVG viewport; feFlood flood covers all consent text regardless of which element references the filter; filterUnits="userSpaceOnUse" attribute inspection with feFlood child and large dimension check required.

Related: SVG filter chain consent attacks  |  SVG feFlood consent attacks  |  SVG structural elements consent bypass synthesis

← Blog  |  Security Checklist