Security Guide

MCP server SVG targetX/targetY consent security — feConvolveMatrix kernel anchor offset, edge-zone shift, text-baseline targeting, and JS injection

The targetX and targetY attributes of <feConvolveMatrix> define the kernel anchor point: the cell within the kernel that is aligned with the pixel being computed. The default values are floor(orderX / 2) and floor(orderY / 2) respectively, centering the kernel on the current pixel. A non-default targetX shifts the kernel window relative to the output pixel: with targetX = orderX - 1 (the rightmost cell), the entire kernel window lies to the left of the current pixel. This shifts the edge-erasure zone (the region where the kernel window extends beyond the element boundary under edgeMode="none") from the right side to the center, and can be calculated to erase specific rows of consent text while leaving the "Allow" button area visible.

Attack 1: targetX="orderX-1" + edgeMode="none" — right-edge boundary zone shift (SA-CSS-TXY-001)

For a pixel at column x, the kernel window covers columns [x - targetX, x - targetX + orderX - 1]. With the default targetX = floor(orderX/2), the kernel is centered: the window spans [x - floor(orderX/2), x + ceil(orderX/2)-1]. The right boundary zone starts at column W - floor(orderX/2) inward from the right edge — symmetric with the left boundary zone.

With targetX = orderX - 1 (rightmost anchor), the kernel window for pixel at column x spans [x - (orderX-1), x] — entirely to the left of the current pixel. The boundary zone is now only at the right side: a pixel at column x has its kernel window extending beyond the right boundary only when x > W - 1 (impossible, since x ≤ W-1). Actually the boundary zone is now only on the left: no wait — if the kernel window spans [x-(orderX-1), x], the right extent of the kernel is always at column x ≤ W-1 (never exceeds the right boundary). The left extent is at x - (orderX-1), which goes below 0 when x < orderX-1. So the boundary zone is now orderX-1 pixels on the left, not the default floor(orderX/2) on each side. A wide boundary zone on the left, where the permission description text starts.

Conversely, with targetX = 0 (leftmost anchor), the kernel spans [x, x + orderX - 1] — entirely to the right. The boundary zone is orderX-1 pixels on the right, where the "Allow" / "Deny" button row is typically positioned. With a carefully chosen kernel that erases the boundary zone (such as a purely averaging kernel with edgeMode="none"), the button labels are zero-diluted while the permission text to the left remains partially visible — a targeted attack on the button label specifically.

<!-- SA-CSS-TXY-001: targetX=0 (leftmost anchor) — kernel spans [x, x+orderX-1]
     Right boundary zone = orderX-1 pixels (from right edge inward)
     For a 320px consent element with orderX=160: right 159 pixels in boundary zone
     The right half of the consent panel (including Allow/Deny buttons) is erased -->
<filter id="txy-right">
  <feConvolveMatrix
    order="160 1"
    kernelMatrix="... (160 values of 1) ..."
    divisor="160"
    targetX="0"
    targetY="0"
    edgeMode="none"/>
  <!-- kernel window spans [x, x+159]; for x > 160: window extends beyond right boundary
       edgeMode=none pads right-side out-of-bounds with 0; mean diluted to near-zero
       Right 159 pixels of element: buttons "Allow" / "Deny" erased -->
</filter>

<div id="consent" style="width:320px; filter:url(#txy-right)">
  <p>Requesting permission: file read access.</p>
  <button id="allow">Allow</button>
  <button id="deny">Deny</button>
</div>

// Detection: flag non-default targetX/targetY with edgeMode=none and large order
function auditTargetXY(feConvolveEl) {
  const orderAttr = feConvolveEl.getAttribute('order') ?? '3';
  const parts = orderAttr.trim().split(/\s+/).map(Number);
  const orderX = parts[0];
  const orderY = parts.length > 1 ? parts[1] : parts[0];
  const defaultTX = Math.floor(orderX / 2);
  const defaultTY = Math.floor(orderY / 2);
  const targetX = parseInt(feConvolveEl.getAttribute('targetX') ?? String(defaultTX), 10);
  const targetY = parseInt(feConvolveEl.getAttribute('targetY') ?? String(defaultTY), 10);
  const edgeMode = feConvolveEl.getAttribute('edgeMode') ?? 'duplicate';
  const isNonDefault = (targetX !== defaultTX || targetY !== defaultTY);
  if (isNonDefault && edgeMode === 'none') {
    // Calculate the boundary zone extent based on anchor position
    const leftZone = targetX;                  // kernel extends left of pixel by targetX
    const rightZone = orderX - 1 - targetX;    // kernel extends right of pixel by (orderX-1-targetX)
    const topZone = targetY;
    const bottomZone = orderY - 1 - targetY;
    const maxZone = Math.max(leftZone, rightZone, topZone, bottomZone);
    if (maxZone > 50) {
      return { sev: 'HIGH', note: `feConvolveMatrix targetX=${targetX} (default=${defaultTX}), targetY=${targetY} (default=${defaultTY}) with edgeMode=none: max boundary zone = ${maxZone}px on one side; non-centered anchor shifts erasure zone from symmetric to one-sided; targeted consent-row erasure possible` };
    }
    return { sev: 'MEDIUM', note: `feConvolveMatrix non-default targetX/targetY with edgeMode=none: asymmetric boundary zone; check which side of consent element is within erasure range` };
  }
  return null;
}

HIGH — SA-CSS-TXY-001: Non-default targetX="0" combined with a large orderX and edgeMode="none" creates an asymmetric boundary zone concentrated on the right side of the consent element. With sufficient orderX, the "Allow" and "Deny" button labels are erased while the left-side permission description text remains partially visible. Detection: check for targetX values other than floor(orderX/2) when edgeMode="none" and orderX is large.

Attack 2: targetX=0 + narrow orderY — horizontal text-row targeting (SA-CSS-TXY-002)

The Y-direction boundary zone is similarly controlled by targetY and orderY. With targetY = 0, the kernel window spans rows [y, y + orderY - 1] — only downward from the current pixel. The bottom boundary zone is orderY - 1 rows deep. For a consent element with a known layout (padding at top, one line of text, then the button row below), an attacker can calculate orderY such that the bottom boundary zone starts exactly at the text baseline row and erases the text glyphs via zero-dilution, while the button row further below escapes the zone.

For example: a consent element 80px tall with 10px padding, 20px text line, 10px gap, 40px button. The text baseline is at approximately row 15-25 within the element (0-indexed). With targetY = 0 and orderY = 58, the bottom boundary zone is rows 80-57=23 to 79. The text row at y=15-25 falls partially in this zone. The kernel for y=23 has its full 58-row window extending to row 80, which is outside the element; with edgeMode="none", those rows are zero-padded, diluting the output at y=23 by approximately (80-23)/58 = 57/58 ≈ 98% zero-padding. The text row is nearly fully erased.

<!-- SA-CSS-TXY-002: targetY=0 + orderY sized to reach text baseline from bottom
     Consent element: 80px tall (10px top pad, 20px text, 10px gap, 40px button)
     Text at rows ~15-25. Bottom zone with orderY=58, targetY=0:
       Kernel for y=25: spans rows [25, 82] — rows 80-82 outside → zero-padded
       Boundary zone starts at y = 80 - 57 = 23
       Text at y=25 is in the zone (2px below boundary start) — diluted
     orderY=58 chosen so that text row (≈25) falls into the zero-dilution zone -->
<filter id="txy-row">
  <feConvolveMatrix
    order="1 58"
    kernelMatrix="1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"
    divisor="58"
    targetX="0"
    targetY="0"
    edgeMode="none"/>
</filter>

<div id="consent" style="height:80px; padding-top:10px; filter:url(#txy-row)">
  <p>Allow: read credentials, export tokens.</p>
  <button id="allow">Allow</button>
</div>

// Detection: check whether boundary zone covers known-position text rows
function checkTargetYTextErasure(feConvolveEl, elementHeightPx) {
  const orderAttr = feConvolveEl.getAttribute('order') ?? '3';
  const parts = orderAttr.trim().split(/\s+/).map(Number);
  const orderY = parts.length > 1 ? parts[1] : parts[0];
  const defaultTY = Math.floor(orderY / 2);
  const targetY = parseInt(feConvolveEl.getAttribute('targetY') ?? String(defaultTY), 10);
  const edgeMode = feConvolveEl.getAttribute('edgeMode') ?? 'duplicate';
  if (edgeMode !== 'none') return null;
  // Bottom boundary zone starts at: elementHeight - (orderY - 1 - targetY)
  const bottomKernelExtent = orderY - 1 - targetY;
  const bottomZoneStart = elementHeightPx - bottomKernelExtent;
  if (bottomZoneStart < elementHeightPx * 0.4) {
    return { sev: 'HIGH', note: `feConvolveMatrix targetY=${targetY}, orderY=${orderY}: bottom boundary zone starts at row ${bottomZoneStart}/${elementHeightPx} (top ${Math.round(100*bottomZoneStart/elementHeightPx)}% of element); text rows may be within the zero-dilution zone` };
  }
  return null;
}

HIGH — SA-CSS-TXY-002: targetY="0" with a large orderY sized to the element height creates a bottom-side boundary zone that can be positioned to cover the text row while leaving the button row outside the erasure zone (or vice versa). The attack requires knowledge of the consent element layout, but MCP clients have consistent UI patterns. Detection: evaluate where the boundary zone starts relative to the element height.

Attack 3: Compound targetX/targetY + asymmetric kernel — specific text region targeting (SA-CSS-TXY-003)

Combining both targetX and targetY offsets with an asymmetric kernel allows an attacker to erase a precise rectangular region within the consent element. The kernel anchor is placed at one corner (e.g., top-left with targetX=0, targetY=0), meaning the kernel window extends only down-right from each pixel. With a kernel that has a large positive value in one cell and zero everywhere else, and with edgeMode="none", only pixels where that specific non-zero kernel cell maps to a valid in-bounds pixel produce non-zero output; all others are zero. This creates a spatial mask that can precisely match the permission text bounding box.

<!-- SA-CSS-TXY-003: Compound targetX/targetY to target text region
     kernel=1 at position (0,0) only (top-left corner of kernel);
     targetX=0, targetY=0 means kernel position (0,0) maps to the current pixel
     → only the current pixel itself contributes (identity at [0,0])
     BUT all other kernel cells are zero (no contribution from neighbors)
     This is actually a non-attack identity kernel — the compound attack is more subtle:
     Use a kernel with non-zero value at a SPECIFIC OFFSET and zero elsewhere -->
<filter id="txy-targeted">
  <feConvolveMatrix
    order="30 10"
    kernelMatrix="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
                  0 0 0 0 0 0 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="1"
    targetX="29"
    targetY="9"
    edgeMode="none"/>
  <!-- All-zero kernel + targetX=29, targetY=9 (bottom-right anchor)
       Kernel window for pixel at (x,y): spans [x-29..x, y-9..y]
       Every kernel value is 0; output = 0/1 + 0 = 0 for all pixels
       Equivalent to full erasure via all-zero kernel, independent of targetX/targetY -->
</filter>

// More subtle: non-zero kernel with strategic targetX/targetY
// places the single non-zero entry outside the element for specific pixel ranges
// Detection: flag targetX or targetY that is not floor(order/2) ± 2
function auditNonCenterAnchor(feConvolveEl) {
  const orderAttr = feConvolveEl.getAttribute('order') ?? '3';
  const parts = orderAttr.trim().split(/\s+/).map(Number);
  const orderX = parts[0], orderY = parts.length > 1 ? parts[1] : parts[0];
  const dTX = Math.floor(orderX / 2), dTY = Math.floor(orderY / 2);
  const tX = parseInt(feConvolveEl.getAttribute('targetX') ?? String(dTX), 10);
  const tY = parseInt(feConvolveEl.getAttribute('targetY') ?? String(dTY), 10);
  const edgeMode = feConvolveEl.getAttribute('edgeMode') ?? 'duplicate';
  // Flag extreme anchor positions (at the corners of the kernel)
  const isCorner = (tX === 0 || tX === orderX - 1) && (tY === 0 || tY === orderY - 1);
  const isEdge = (tX === 0 || tX === orderX - 1 || tY === 0 || tY === orderY - 1);
  if (isEdge && edgeMode === 'none' && (orderX > 10 || orderY > 10)) {
    return { sev: 'HIGH', note: `feConvolveMatrix targetX=${tX}/${orderX-1}, targetY=${tY}/${orderY-1}: anchor at edge/corner of ${orderX}×${orderY} kernel with edgeMode=none; asymmetric boundary zone of ${isCorner ? 'corner' : 'edge'} type; specific row/column erasure targeting possible` };
  }
  return null;
}

HIGH — SA-CSS-TXY-003: Combining extreme targetX/targetY values (anchoring at a corner of the kernel) with a large asymmetric kernel and edgeMode="none" shifts the boundary zone to one side of the consent element, enabling targeted erasure of specific rows or columns. Detection: flag corner and edge anchor positions (targetX=0, targetX=orderX-1, targetY=0, targetY=orderY-1) combined with large order and edgeMode="none".

Attack 4: JS mousedown injects targetX/targetY change (SA-CSS-TXY-004)

JavaScript changes targetX from the default center value to an extreme (0 or orderX-1) at mousedown capture phase. The initial state uses a centered kernel anchor (normal behavior), and with a medium-sized order the boundary zones are symmetric and small enough that consent text is not erased. At mousedown, targetX is shifted to 0, moving the right-side boundary zone from orderX/2 pixels wide to orderX-1 pixels wide. If the element width is less than orderX-1 pixels, the entire element falls into the right boundary zone and the consent text is erased at click time.

<!-- SA-CSS-TXY-004: JS mousedown shifts targetX from center to extreme
     Initial: targetX=15 (center of 31-wide kernel) — symmetric 15px zones; normal text
     At mousedown: targetX=0 — right zone becomes 30px; elements < 30px wide fully erased
     For a typical "Allow" button (≈60px wide): right 30px erased — button text gone -->
<filter id="txy-dynamic">
  <feConvolveMatrix id="txy-cm"
    order="31 1"
    kernelMatrix="1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"
    divisor="31"
    targetX="15"
    targetY="0"
    edgeMode="none"/>
</filter>

<div id="consent" style="filter:url(#txy-dynamic)">
  <p>Allow: filesystem access, credential read, exec.</p>
  <button id="allow">Allow</button>
</div>
<script>
document.getElementById('consent').addEventListener('mousedown', () => {
  document.getElementById('txy-cm').setAttribute('targetX', '0');
  // Right boundary zone expands from 15px to 30px
  // Any narrow element ≤ 30px wide is now fully within erasure zone
}, { capture: true });
</script>

// Detection: MutationObserver on targetX and targetY
const cmEl = document.getElementById('txy-cm');
new MutationObserver(muts => {
  for (const m of muts) {
    if (m.attributeName === 'targetX' || m.attributeName === 'targetY') {
      const orderAttr = cmEl.getAttribute('order') ?? '3';
      const parts = orderAttr.trim().split(/\s+/).map(Number);
      const orderX = parts[0], orderY = parts.length > 1 ? parts[1] : parts[0];
      const dTX = Math.floor(orderX / 2), dTY = Math.floor(orderY / 2);
      const tX = parseInt(cmEl.getAttribute('targetX') ?? String(dTX), 10);
      const tY = parseInt(cmEl.getAttribute('targetY') ?? String(dTY), 10);
      const rightZone = orderX - 1 - tX;
      const bottomZone = orderY - 1 - tY;
      if (rightZone > 30 || bottomZone > 30) {
        console.warn('[SA-CSS-TXY-004] targetX/targetY changed; asymmetric boundary zone:', { rightZone, bottomZone, tX, tY });
      }
    }
  }
}).observe(cmEl, { attributes: true, attributeFilter: ['targetX', 'targetY', 'order'] });

HIGH — SA-CSS-TXY-004: Shifting targetX from center to 0 at mousedown doubles the right-side boundary zone, erasing content in the right portion of the consent element at click time. For a narrow "Allow" button, this erases the button label text while the click event is already registered. Detection requires a MutationObserver on targetX and targetY for all <feConvolveMatrix> elements on consent-containing ancestors, with a check on the resulting boundary zone widths.

Summary table

AttacktargetX / targetYMechanismEffectSeverity
SA-CSS-TXY-001 targetX=0 Kernel window extends only right; right boundary zone = orderX-1; button labels erased Right-side erasure (button area) High
SA-CSS-TXY-002 targetY=0 Bottom boundary zone covers text row; zero-dilution of consent text baseline Text row erasure (layout-targeted) High
SA-CSS-TXY-003 Corner anchor (0,0 or orderN-1) Combined asymmetric zones; specific rectangular region of consent erased Targeted region erasure High
SA-CSS-TXY-004 JS mousedown → targetX=0 Right boundary zone doubles at click time; narrow buttons erased at interaction Click-time erasure of button labels High

Defences

SkillAudit findings for this attack surface

HIGH SA-CSS-TXY-001: feConvolveMatrix targetX="0" + large orderX + edgeMode=none — kernel window extends only right; right boundary zone = orderX-1 pixels; button label area erased; flag targetX=0 with orderX>30 and edgeMode=none.
HIGH SA-CSS-TXY-002: feConvolveMatrix targetY="0" + large orderY + edgeMode=none — bottom boundary zone covers consent text row; zero-dilution erases text baseline; flag targetY=0 with orderY/elementHeight ratio > 0.5.
HIGH SA-CSS-TXY-003: Corner-anchor targetX/targetY (values 0 or order-1) on large kernel with edgeMode=none — asymmetric boundary zone targeted at specific row or column of consent element; flag corner anchors on kernels with order > 10.
HIGH SA-CSS-TXY-004: JS mousedown injects targetX shift to extreme value — right/left boundary zone doubles at click time; button label erased at interaction; MutationObserver on targetX and targetY with zone-width check required.

Related: kernelMatrix consent attacks  |  divisor consent attacks  |  feConvolveMatrix order attacks  |  preserveAlpha consent attacks

← Blog  |  Security Checklist