← Blog

MCP server SVG feConvolveMatrix targetX targetY consent security

Four SA-CSS-CVT consent attack patterns using the targetX and targetY kernel anchor attributes of feConvolveMatrix to spatially displace consent text from its DOM click target — creating clickjacking-like deception without repositioning the element, while WCAG contrast checks at the original DOM position continue to pass.

By SkillAudit  |  24 Oct 2026

targetX and targetY: the kernel anchor attributes

The feConvolveMatrix SVG filter primitive applies a discrete 2D convolution to each pixel of the source image. For an order×order kernel, the output at position (x, y) is computed as the weighted sum of source pixels in the neighbourhood defined by the kernel, normalised by divisor, plus bias:

result(x, y) = (Σ_{i=0..orderX-1, j=0..orderY-1}
                  kernelMatrix[(orderY-j-1) * orderX + (orderX-i-1)]
                  × src(x - targetX + i, y - targetY + j)
               ) / divisor + bias

The attributes targetX and targetY specify which kernel cell is placed at the output pixel (x, y) — they define the kernel’s anchor point within the kernel grid. The SVG specification defaults are targetX = floor(orderX / 2) and targetY = floor(orderY / 2), which centres the kernel over the output pixel. When these defaults are used, the convolution is symmetric: for each output pixel the kernel is centred on that pixel and samples an equal number of source pixels in each direction. The visual output is centred on the same position as the source.

When targetX and targetY deviate from the centred defaults, the kernel anchor shifts. A left-anchored kernel (targetX=0) places the leftmost kernel column at the output pixel, meaning the kernel extends entirely to the right of the output position. The output pixel at (x, y) therefore samples source pixels at (x, y) through (x+orderX−1, y). If the kernel describes a horizontal averaging (motion blur), the output image appears shifted to the left by (orderX/2) pixels compared to the source — the visual content migrates leftward while the DOM element stays in place.

Spatial deception: DOM position vs. visual position diverge. The DOM element, its bounding box, and its hit-test region all remain at the original CSS-positioned location. The filter output — what the user actually sees — appears displaced by up to (orderX/2) pixels horizontally or (orderY/2) pixels vertically. The user clicks at the perceived (displaced) text location and misses the consent button. Or, in the inverse framing: the user intends to click elsewhere, but the perceived consent text has shifted onto that location, making the user believe they have read the consent text adjacent to where they are clicking. WCAG contrast checks against the original DOM colour value still pass because the DOM colour is unchanged; only the filter output is displaced.

Spatial displacement formula and detection threshold

For a kernel of size orderX×orderY with anchor (targetX, targetY), the visual output is shifted relative to the source by:

shiftX = targetX - floor(orderX / 2)   // positive = output shifts LEFT (kernel extends RIGHT)
shiftY = targetY - floor(orderY / 2)   // positive = output shifts UP   (kernel extends DOWN)

// Equivalently, the perceived text location relative to the DOM position is:
perceivedOffsetX = floor(orderX / 2) - targetX
perceivedOffsetY = floor(orderY / 2) - targetY

Detection heuristic: compute the Manhattan distance of the anchor from the kernel centre. If this exceeds 2 pixels, flag as a spatial deception risk:

function checkTargetXY(feConvolveEl) {
  const orderRaw = feConvolveEl.getAttribute('order') || '3';
  const orderX = parseInt(orderRaw.split(/\s+/)[0]);
  const orderY = parseInt((orderRaw.split(/\s+/)[1] || orderRaw.split(/\s+/)[0]));
  const defaultTX = Math.floor(orderX / 2);
  const defaultTY = Math.floor(orderY / 2);
  const targetX = feConvolveEl.hasAttribute('targetX')
    ? parseInt(feConvolveEl.getAttribute('targetX'))
    : defaultTX;
  const targetY = feConvolveEl.hasAttribute('targetY')
    ? parseInt(feConvolveEl.getAttribute('targetY'))
    : defaultTY;
  const anchorDrift = Math.abs(targetX - defaultTX) + Math.abs(targetY - defaultTY);
  const shiftX = defaultTX - targetX;  // positive = text appears right of DOM
  const shiftY = defaultTY - targetY;  // positive = text appears below DOM
  if (anchorDrift > 2) {
    return {
      risk: anchorDrift > (orderX / 2) ? 'CRITICAL' : 'HIGH',
      shiftX, shiftY,
      anchorDrift,
      detail: `targetX=${targetX} (default ${defaultTX}), targetY=${targetY} (default ${defaultTY})`
    };
  }
  return null;
}

SA-CSS-CVT-001: left-anchor horizontal motion blur — text smears right of DOM position

A 15×15 horizontal motion-blur kernel has all values in the first row set to 1 and all remaining rows set to 0, with divisor="15". With the default anchor (targetX=7), the output pixel at (x, y) averages source pixels (x−7, y) through (x+7, y) — centred, no spatial shift. Setting targetX="0" (left-anchor) moves the kernel anchor to the leftmost column: the output at (x, y) now averages source pixels (x, y) through (x+14, y). The kernel samples 14 pixels to the right of the output point. The output image is the average of the source shifted by 0 through 14 pixels right, which appears as a smear starting at x and extending rightward. The visual centre of the smeared text is at x+7, while the DOM element is at x. The user perceives the consent text as 7 pixels to the right of where the button actually is.

<filter id="cvt001">
  <feConvolveMatrix order="15"
    kernelMatrix="1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
    divisor="15"
    targetX="0" targetY="0"/>
  <!-- targetX=0: left-anchor. Default targetX=floor(15/2)=7.
       Anchor drift = |0-7| = 7 pixels.
       shiftX = floor(15/2) - 0 = +7 (text appears 7px RIGHT of DOM position).
       Output(x,y) = avg(src(x,y), src(x+1,y), ..., src(x+14,y)).
       Text smears 14px to the right of original glyph position.
       Visual centre of smear: x+7.  DOM click target: x.
       User clicks at perceived text centre, misses DOM element by 7px.
       getComputedStyle().color unchanged.  WCAG contrast check at DOM pos: pass.
       anchorDrift=7 > 2 → flag CRITICAL. -->
</filter>

SA-CSS-CVT-002: right-anchor horizontal motion blur — text smears left of DOM position

The same 15-column motion-blur kernel with targetX="14" (right-anchor) reverses the direction. The kernel anchor is placed at column 14 (the rightmost column). For output pixel (x, y), the kernel spans source columns (x−14, y) through (x, y). The motion smear extends 14 pixels to the left. The perceived visual centre of the text is at x−7 while the DOM element registers clicks at x. A user reading the text at its apparent location (x−7) and clicking there misses the button entirely; conversely, the button at x may be adjacent to other UI elements the user did not intend to activate.

<filter id="cvt002">
  <feConvolveMatrix order="15"
    kernelMatrix="1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
    divisor="15"
    targetX="14" targetY="0"/>
  <!-- targetX=14: right-anchor. Default targetX=floor(15/2)=7.
       Anchor drift = |14-7| = 7 pixels.
       shiftX = floor(15/2) - 14 = -7 (text appears 7px LEFT of DOM position).
       Output(x,y) = avg(src(x-14,y), src(x-13,y), ..., src(x,y)).
       Text smears 14px to the left.  Visual centre of smear: x-7.  DOM pos: x.
       User perceives "Allow" text at x-7; DOM button click target is at x.
       Inverse clickjacking: button label appears to be for an element to its left. -->
</filter>

SA-CSS-CVT-003: top-left Gaussian anchor — diagonal shift plus blur

An approximate 9×9 Gaussian kernel with targetX="0" and targetY="0" (top-left anchor, compared to the default centred anchor at (4, 4)) shifts the entire output image diagonally down-right by floor(9/2) = 4 pixels in both axes. The Gaussian blur effect is simultaneously present (the kernel is a low-pass filter), so the text is both displaced and blurred. The DOM click target remains at the original position. The visual text appears 4 pixels down and 4 pixels right: the user perceives the consent button in the wrong position and a WCAG accessibility scan run against the original DOM sees unchanged colour values at the original coordinates, reporting no violation. The spatial deception is orthogonal to any colour or contrast manipulation.

<filter id="cvt003">
  <feConvolveMatrix order="9"
    kernelMatrix="1 1 1 1 1 1 1 1 1
                  1 2 2 2 2 2 2 2 1
                  1 2 3 3 3 3 3 2 1
                  1 2 3 4 4 4 3 2 1
                  1 2 3 4 5 4 3 2 1
                  1 2 3 4 4 4 3 2 1
                  1 2 3 3 3 3 3 2 1
                  1 2 2 2 2 2 2 2 1
                  1 1 1 1 1 1 1 1 1"
    divisor="196"
    targetX="0" targetY="0"
    edgeMode="duplicate"/>
  <!-- order=9: default anchor = (floor(9/2), floor(9/2)) = (4, 4).
       targetX=0, targetY=0: top-left anchor.
       Anchor drift = |0-4| + |0-4| = 8 pixels (Manhattan distance).
       shiftX = +4 (text appears 4px RIGHT of DOM).
       shiftY = +4 (text appears 4px BELOW DOM).
       Kernel is all-positive, sum=divisor → low-pass (blur) as well as shifted.
       Combined effect: consent text is blurred AND displaced diagonally.
       WCAG check at (x,y): samples original unfiltered colours → pass.
       Spatial deception check: anchorDrift=8 >> threshold=2 → CRITICAL. -->
</filter>

WCAG spatial-analysis gap: WCAG 2.1 Success Criterion 1.4.3 (Contrast Minimum) tests the luminance ratio between the foreground and background colour at representative text pixels. After a kernel anchor shift, the text is moved to a different pixel location, not recoloured. The WCAG check, applied at the DOM text node coordinates, samples the pre-filtered colours of the original source pixels and finds correct contrast. The displaced rendered output is never sampled. Spatial deception detection requires computing the expected rendered position of the output — applying the anchor offset formula — and then checking whether the rendered text overlaps the correct click target, not just whether the contrast is adequate.

SA-CSS-CVT-004: JS mousedown kernel anchor shift at click confirmation time

The initial filter state is an identity kernel at the centred default anchor: order=3, kernelMatrix="0 0 0 0 1 0 0 0 0", divisor="1", no explicit targetX or targetY (defaulting to (1, 1) — centred). Consent text renders correctly with zero spatial shift. On mousedown on the confirm button, JavaScript replaces the kernel with a 15-column motion-blur kernel and sets targetX="0". The kernel update propagates synchronously before the click event fires. The confirmation button’s visual label has shifted 7 pixels to the right at the moment of confirmation; any consent framing text in the same filtered container is also displaced. Static analysis of the DOM before interaction sees only the identity kernel with a centred anchor and reports no finding. Runtime detection via MutationObserver on the targetX, targetY, and order attributes is required.

<svg style="position:absolute;width:0;height:0">
  <defs>
    <filter id="cvt004">
      <feConvolveMatrix id="cvt004K" order="3"
        kernelMatrix="0 0 0  0 1 0  0 0 0"
        divisor="1"/>
      <!-- Initially: identity kernel, centred anchor (default targetX=1, targetY=1).
           No spatial shift.  Text renders at DOM position.  WCAG: pass. -->
    </filter>
  </defs>
</svg>
<div style="filter:url(#cvt004)">
  <p>Grant this MCP server persistent background file access.</p>
  <button id="cvt004Btn">Allow</button>
</div>
<script>
const MOTION_BLUR_15 =
  "1 1 1 1 1 1 1 1 1 1 1 1 1 1 1  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  " +
  "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0";

document.getElementById("cvt004Btn").addEventListener("mousedown", () => {
  const k = document.getElementById("cvt004K");
  k.setAttribute("order", "15");
  k.setAttribute("kernelMatrix", MOTION_BLUR_15);
  k.setAttribute("divisor", "15");
  k.setAttribute("targetX", "0");   // ← left-anchor: 7px rightward visual shift
  // Text and button label now displaced 7px right at confirmation moment.
  // DOM element stays at original position.  Click registers at DOM position.
  // getComputedStyle().color: original dark colour — no observable change.
});
</script>

Detection: MutationObserver on targetX, targetY, and order

Because SA-CSS-CVT-004 fires the anchor change at mousedown rather than on page load, static analysis is insufficient. A MutationObserver must watch the targetX, targetY, and order attributes on every feConvolveMatrix element that is an ancestor filter of a consent element. When any of these attributes mutates, recompute the anchor-drift check. If the drift exceeds the threshold, block the pending confirmation event.

function monitorConvolveAnchors(consentEl) {
  // Collect all feConvolveMatrix elements in ancestor filters
  const filterIds = new Set();
  let el = consentEl;
  while (el) {
    const f = getComputedStyle(el).filter;
    const m = f.match(/url\(["']?#([^"')]+)["']?\)/g) || [];
    m.forEach(u => filterIds.add(u.replace(/url\(["']?#|["']?\)/g, '')));
    el = el.parentElement;
  }
  document.querySelectorAll('feConvolveMatrix').forEach(fc => {
    const filterId = fc.closest('filter')?.id;
    if (!filterIds.has(filterId)) return;
    const obs = new MutationObserver(() => {
      const result = checkTargetXY(fc);
      if (result && result.risk === 'CRITICAL') {
        // Block pending confirmation
        document.removeEventListener('click', confirmationHandler, true);
        console.warn('[SkillAudit] SA-CSS-CVT: spatial anchor shift detected', result);
      }
    });
    obs.observe(fc, { attributes: true,
      attributeFilter: ['targetX', 'targetY', 'order', 'kernelMatrix'] });
  });
}

Attack summary

IDAnchorKernel typeVisual shiftWCAG impactSeverity
SA-CSS-CVT-001 targetX=0 (left) 15-col horizontal motion blur Text 7px right of DOM click target Contrast at DOM pos: pass; spatial mismatch undetected Critical
SA-CSS-CVT-002 targetX=14 (right) 15-col horizontal motion blur Text 7px left of DOM click target Contrast at DOM pos: pass; spatial mismatch undetected Critical
SA-CSS-CVT-003 targetX=0, targetY=0 (top-left) 9×9 Gaussian blur Text 4px right and 4px down; also blurred Contrast at DOM pos: pass; diagonal spatial deception High
SA-CSS-CVT-004 Identity→targetX=0 (mousedown) Identity→15-col motion blur 7px rightward shift at click time Static: no finding; runtime: CRITICAL on anchor mutation Critical

SkillAudit findings for this attack surface

CRITICAL SA-CSS-CVT-001: feConvolveMatrix order=15 horizontal motion-blur kernel with targetX="0" on consent element ancestor — left-anchor shifts output 7px right of DOM position; perceived consent text visually misaligned from its click target; anchorDrift=|0−7|=7 exceeds threshold 2; WCAG contrast check at original DOM coordinates passes unchanged; spatial deception equivalent to CSS transform without transform: detected only via anchor-drift formula.
CRITICAL SA-CSS-CVT-002: feConvolveMatrix order=15 horizontal motion-blur kernel with targetX="14" on consent element ancestor — right-anchor shifts output 7px left of DOM position; perceived consent text displaced leftward; inverse clickjacking: button label appears associated with content to its left; anchorDrift=|14−7|=7; detection: compute shiftX = floor(order/2) − targetX = −7 (negative = leftward displacement).
HIGH SA-CSS-CVT-003: feConvolveMatrix order=9 Gaussian-approximate kernel with targetX="0" and targetY="0" — top-left anchor shifts output diagonally 4px right and 4px down; simultaneous Gaussian blur reduces legibility; anchorDrift=|0−4|+|0−4|=8; blur and spatial displacement compound: text is both illegible and misplaced; WCAG accessibility scan at original DOM coordinates reports no violation for either colour or position.
CRITICAL SA-CSS-CVT-004: JS mousedown replaces identity kernel (centred anchor, no shift) with 15-column motion-blur kernel and sets targetX="0" — 7px spatial shift fires at confirmation moment; static DOM analysis: identity kernel at default centred anchor, anchorDrift=0, no finding; runtime detection required: MutationObserver on targetX, targetY, order, and kernelMatrix attributes; recompute anchor-drift on every mutation; block confirmation if drift exceeds threshold after mousedown.

Related: feConvolveMatrix edgeMode consent security (SA-CSS-CVM)  |  feConvolveMatrix bias consent security  |  feConvolveMatrix order consent security  |  feConvolveMatrix consent security (overview)

← Blog  |  Security Checklist