MCP server CSS feOffset security: large dx/dy viewport displacement, overflow:hidden clip of shifted pixels, subpixel offset glyph rendering disruption, and animated offset triggered at consent interaction

Published 2026-09-30 — SkillAudit Research

The SVG feOffset filter primitive shifts the input image by a specified displacement in filter-primitive coordinates. The dx attribute shifts horizontally; dy shifts vertically. After the shift, pixels that move outside the filter region boundary — defined by the filter's x, y, width, height attributes, which default to x="-10%" y="-10%" width="120%" height="120%" — are clipped.

A key property of feOffset is that it operates on the filter's output pixels, not the element's layout box. The element continues to occupy its original position in the document layout. getBoundingClientRect() still reports the element at its layout position with correct dimensions. The shifted pixels may be anywhere in the filter region — or outside it and clipped — but the layout check sees the original box. This decoupling between layout position and pixel position is the foundation of the feOffset attack class.

Layout-pixel decoupling: feOffset moves pixels, not layout boxes. getBoundingClientRect() tracks the layout box. An element with feOffset(dx=500) has a positive bounding rect at its original position, but its pixels have been shifted 500px to the right and clipped by the filter region boundary. The pixels are not where the bounding rect says they are.

Attack findings

CRITICAL
Large dx value shifts all consent text pixels outside filter region boundary
An feOffset dx="500" dy="0" applied to a consent text element with a standard filter region (width="120%") shifts all pixels 500 filter-primitive units to the right. For a consent element with a 300px bounding width, the filter region extends approximately to 360px. A 500px shift places all pixels outside this boundary. The shifted pixels are clipped. The filter outputs a fully transparent result over the element's layout region. The element's bounding rect remains at its original 300px width position. textContent, visibility, opacity, fill, and bounding rect checks all pass. The rendered frame shows nothing where the text should be.
<filter id="f1">
  <feOffset in="SourceGraphic" dx="500" dy="0"/>
</filter>
<text filter="url(#f1)">I agree to the Terms of Service</text>
HIGH
feOffset combined with overflow:hidden parent clips shifted pixels at parent boundary
A consent element inside a container with overflow: hidden and a constrained height. An feOffset dy="-50" shifts the consent text 50px upward into negative filter space. Some browsers clip filter output at the parent element's overflow boundary. The consent text pixels emerge above the container edge, outside the visible area. The element's bounding rect is fully within the container's visible region (the layout box did not move). The shifted pixels are above the container edge and invisible. The attack exploits the interaction between the filter pixel space and the CSS overflow model.
MEDIUM
Subpixel feOffset (dx=0.3) disrupts glyph column rendering at specific DPR
A fractional feOffset dx="0.3" shifts the rendered text 0.3 filter-primitive units horizontally. At a device pixel ratio of 1.0, this is a 0.3-pixel shift — below the rendering threshold. At DPR 2.0, this becomes a 0.6-pixel shift which, combined with subpixel antialiasing, can cause horizontal glyph edges to split across physical pixels. At DPR 3.0 (common on mobile), the 0.9-pixel shift interacts with the font renderer's hinting grid, producing partially-rendered glyph columns that reduce legibility on specific font/size combinations. This variant is device-dependent and DPR-targeted.
MEDIUM
Animated dx/dy synchronized with button activation shifts text at interaction time
At page load, feOffset dx="0" dy="0" — no shift, text visible. A CSS animation or JavaScript mutation changes dx to 500 at the moment the user's mouse enters the consent button area (via mouseenter event). A static audit at DOMContentLoaded records dx="0" — a no-op offset. The attack activates during the button interaction window: the user sees the text, begins to click, and the text shifts away at the critical moment. Detection requires a MutationObserver on the feOffset dx and dy attributes during the interaction window.

Detection algorithm

function detectFeOffsetAttacks(consentEl) {
  const style = getComputedStyle(consentEl);
  const filterVal = style.filter;
  if (!filterVal || filterVal === 'none') return null;

  const filterId = filterVal.match(/url\(["']?#([^"')]+)["']?\)/)?.[1];
  if (!filterId) return null;

  const svgRoot = consentEl.closest('svg') || document;
  const filter = svgRoot.querySelector(`filter#${filterId}`);
  if (!filter) return null;

  const rect = consentEl.getBoundingClientRect();
  // Compute filter region extent in pixels
  const filterRegionWidth = rect.width * 1.2; // default 120%
  const filterRegionHeight = rect.height * 1.2;

  const findings = [];

  for (const offset of filter.querySelectorAll('feOffset')) {
    const dx = parseFloat(offset.getAttribute('dx') || '0');
    const dy = parseFloat(offset.getAttribute('dy') || '0');
    const absDx = Math.abs(dx);
    const absDy = Math.abs(dy);

    if (absDx > filterRegionWidth || absDy > filterRegionHeight) {
      findings.push({ severity: 'critical', offset,
        issue: `feOffset dx=${dx} dy=${dy} moves all pixels outside filter region (width=${filterRegionWidth.toFixed(0)}px height=${filterRegionHeight.toFixed(0)}px) — all pixels clipped, consent text invisible` });
    } else if (absDx > rect.width * 0.8 || absDy > rect.height * 0.8) {
      findings.push({ severity: 'high', offset,
        issue: `feOffset dx=${dx} dy=${dy} shifts most pixels toward filter region boundary — verify pixels remain within visible filter extent` });
    } else if (absDx > 0 && absDx < 1 || absDy > 0 && absDy < 1) {
      findings.push({ severity: 'medium', offset,
        issue: `feOffset subpixel dx=${dx} dy=${dy} may disrupt glyph rendering at specific DPR values — DPR-targeted legibility attack` });
    }
  }

  // MutationObserver for animated offset
  const offsets = filter.querySelectorAll('feOffset');
  const observer = new MutationObserver(() => {
    const postFindings = detectFeOffsetAttacks(consentEl);
    if (postFindings?.length) reportFindings('offset-mutation', postFindings);
  });
  offsets.forEach(o => observer.observe(o, { attributes: true, attributeFilter: ['dx', 'dy'] }));

  return findings.length ? findings : null;
}

Remediation

ControlHow it helps
For every feOffset in a consent element's filter graph, compare the absolute value of dx and dy against the element's bounding dimensions multiplied by the filter region multiplier; if the shift exceeds the filter region extent, flag as critical (all pixels clipped) Large viewport displacement variant — detects shifts that move all pixels outside the filter boundary; requires computing the effective filter region size from the filter element's x/y/width/height attributes against the element's pixel dimensions
Check the overflow property of ancestor elements containing the consent element; if overflow: hidden exists on an ancestor, model whether the shifted filter output pixels exit the ancestor's visible bounds at the dx/dy values present Overflow-clip variant — the attack requires both a non-zero feOffset and an overflow:hidden ancestor; neither alone is conclusive; compound analysis is required
Install a MutationObserver on feOffset dx and dy attributes during the consent interaction window and flag any mutation that changes a near-zero offset to a large-displacement value Animated offset variant — the attack activates at interaction time; static audit sees dx=0; only mutation observation during interaction reveals the shift

SkillAudit computes the effective filter region size for every consent element, compares feOffset displacement against the filter boundary, models overflow-clip interactions with ancestor containers, and monitors attribute mutations during the interaction window. Run a free audit on any MCP server GitHub URL to detect feOffset displacement attacks and the full SVG filter consent attack surface.