Security Guide

MCP server SVG cursor attribute consent security — cursor="none" invisible cursor, custom cursor hotspot offset, cursor="crosshair" non-interactive appearance, and JS mousedown cursor injection

The SVG cursor attribute (and its CSS equivalent) controls the visual appearance of the mouse pointer over an element. Unlike most consent-bypass attacks that hide or obscure consent text, cursor attacks target the user’s ability to aim and click correctly. A hidden cursor leaves the user unable to verify where their click will land; a misaligned custom cursor hotspot registers the click 20px away from where the user aimed; a crosshair cursor on a consent button makes it appear non-interactive, suppressing clicks from users who associate the pointer cursor with clickability. Four attack patterns exploit the SVG cursor attribute against MCP consent UI click targeting.

Attack 1: cursor="none" on consent panel — cursor invisible over consent area (SA-CSS-CUR-001)

Setting cursor="none" on the consent SVG container or panel element makes the mouse cursor invisible over the entire consent area. CSS cursor: none is a legitimate technique for games and custom cursor experiences, but applied to a consent panel it prevents the user from seeing where their click will register. If the consent panel contains both an “Allow” button and a “Deny” button (or a cancel region), the user cannot visually confirm whether their cursor is positioned over the desired target. The cursor="none" SVG attribute and the CSS cursor: none property have the same effect; both can be applied to ancestor elements (the cursor attribute inherits through the SVG element tree). An audit that checks only the immediate consent button element may miss a cursor="none" on a parent <g> or the <svg> root.

<!-- SA-CSS-CUR-001: cursor="none" on consent SVG -- cursor invisible over all consent UI -->
<svg width="400" height="200" cursor="none">
  <!-- Consent panel background -->
  <rect x="0" y="0" width="400" height="200" fill="#fff"/>
  <text x="20" y="40" fill="#111" font-size="14">
    Allow shell execution and credential access?
  </text>
  <!-- Consent button: cursor still "none" (inherited from svg root) -->
  <g id="allow-btn" style="cursor:pointer">
    <!-- Even if style="cursor:pointer" is set, the SVG cursor attribute
         on an ancestor overrides CSS cursor in some browsers -->
    <rect x="20" y="60" width="80" height="32" rx="4" fill="#2563eb"/>
    <text x="60" y="81" text-anchor="middle" fill="#fff" font-size="13">Allow</text>
  </g>
  <!-- Deny / cancel trap at same position range -->
  <g id="deny-btn">
    <rect x="120" y="60" width="80" height="32" rx="4" fill="#e5e7eb"/>
    <text x="160" y="81" text-anchor="middle" fill="#374151" font-size="13">Deny</text>
  </g>
</svg>

// Detection: check cursor attribute on consent element and all ancestors
function detectCursorNone(consentEl) {
  const findings = [];
  let el = consentEl;
  while (el && el.tagName !== 'BODY') {
    const cursorAttr = el.getAttribute ? el.getAttribute('cursor') : null;
    const cursorStyle = el.style ? el.style.cursor : null;
    const cursorComputed = window.getComputedStyle ? getComputedStyle(el).cursor : null;

    if (cursorAttr === 'none' || cursorStyle === 'none' || cursorComputed === 'none') {
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-CUR-001',
        note: `cursor="none" on <${el.tagName}> ancestor of consent element — mouse cursor invisible over consent area; user cannot verify click target position`
      });
    }
    el = el.parentElement;
  }
  return findings;
}

CRITICAL — SA-CSS-CUR-001: cursor="none" applied to the consent SVG element or any ancestor makes the mouse cursor invisible over the entire consent area. The user cannot see where their click will register. Detection must check the SVG cursor attribute and the CSS cursor property on the consent element and every ancestor up to the document root, not only the immediate button element.

Attack 2: Custom cursor URL with misaligned hotspot — click registers 20px from visual center (SA-CSS-CUR-002)

The CSS cursor property accepts a custom image URL with an explicit hotspot: cursor: url(check.svg) 20 20, pointer. The hotspot coordinates (20, 20) define which pixel in the cursor image is the actual click-registration point. If the cursor image shows a visual center (for example, the tip of a checkmark or the center of a circle) at pixel (0, 0) but the hotspot is declared at (20, 20), the actual click registers 20 pixels to the right and below where the user aimed. This can redirect a click that was aimed at the “Allow” button to a “Deny” region or empty space, depending on the layout. The attack is subtle: the cursor image appears to be a reasonable consent-related cursor (a checkmark, a shield icon), but the hotspot offset is large enough to redirect clicks. The SVG cursor attribute supports the same hotspot syntax via a <cursor> element, but the CSS property is more commonly used and more widely supported.

/* SA-CSS-CUR-002: custom cursor with misaligned hotspot */
/* Visual: checkmark image, center at approximately 0,0 */
/* Hotspot: declared at 20,20 — click registers 20px right+down from visual center */
#consent-svg {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath d='M5 12l5 5L19 7' stroke='%232563eb' stroke-width='2' fill='none'/%3E%3C/svg%3E") 20 20, pointer;
  /* Hotspot at (20,20) but checkmark tip is at approx (5,12) -- 15-20px offset */
}

/* The user aims the checkmark tip at the Allow button.
   The click registers at (tip_x + 15, tip_y + 8) -- lands on the Deny button
   if Allow is left of Deny in the layout. */

// Detection: parse cursor CSS for url() with large hotspot offset
function detectCustomCursorHotspotOffset(consentEl) {
  const findings = [];
  const checkEl = (el) => {
    const cursor = getComputedStyle(el).cursor;
    const match = cursor.match(/url\([^)]+\)\s+(\d+)\s+(\d+)/);
    if (match) {
      const hx = parseInt(match[1]);
      const hy = parseInt(match[2]);
      if (hx > 8 || hy > 8) {
        findings.push({
          severity: 'CRITICAL', code: 'SA-CSS-CUR-002',
          note: `Custom cursor on <${el.tagName}> has hotspot offset (${hx}, ${hy}) — click registration displaced from visual cursor center by up to ${Math.sqrt(hx*hx+hy*hy).toFixed(0)}px; aimed clicks may land on wrong element`
        });
      }
    }
  };
  checkEl(consentEl);
  for (const child of consentEl.querySelectorAll('*')) checkEl(child);
  return findings;
}

CRITICAL — SA-CSS-CUR-002: A custom cursor image URL with a large hotspot offset causes clicks that are aimed at the visual cursor center to register at a point displaced by the hotspot distance. Offsets larger than approximately 8 pixels in either axis create a meaningful risk of click misdirection between adjacent consent UI elements. Detection must parse the cursor CSS property for url(...) X Y syntax and flag X or Y values above 8px as CRITICAL if the consent element contains multiple interaction targets within close proximity.

Attack 3: cursor="crosshair" on consent button — removes interactive pointer appearance (SA-CSS-CUR-003)

Users trained by years of browser UX associate the pointer cursor (the hand icon, cursor: pointer) with clickable elements. When a consent button displays the crosshair cursor instead of the pointer cursor, a significant fraction of users will not attempt to click it, interpreting it as a non-interactive or precision-selection element (as used in image editors and drawing tools). The crosshair cursor on a consent button creates confusion about whether clicking is the expected action, which can suppress consent without technically preventing the click. Combined with a cancel action that has the pointer cursor, the attacker can steer users toward the element that looks interactive (the cancel/deny) and away from the one that looks like a crosshair (the allow).

<!-- SA-CSS-CUR-003: crosshair cursor on consent button hides interactive appearance -->
<svg width="400" height="120">
  <!-- Allow button: crosshair cursor -- looks like a precision selection tool -->
  <g id="allow-btn" cursor="crosshair">
    <rect x="20" y="40" width="80" height="32" rx="4" fill="#2563eb"/>
    <text x="60" y="61" text-anchor="middle" fill="#fff" font-size="13">Allow</text>
  </g>
  <!-- Deny button: pointer cursor -- looks clearly clickable -->
  <g id="deny-btn" style="cursor:pointer">
    <rect x="120" y="40" width="80" height="32" rx="4" fill="#e5e7eb"/>
    <text x="160" y="61" text-anchor="middle" fill="#374151" font-size="13">Deny</text>
  </g>
</svg>

// Detection: check that consent button shows pointer cursor, not crosshair
function detectNonPointerCursorOnButton(consentButtonEl) {
  const cursor = getComputedStyle(consentButtonEl).cursor;
  const attrCursor = consentButtonEl.getAttribute('cursor');

  if (cursor === 'crosshair' || cursor === 'default' || cursor === 'text' || cursor === 'help') {
    return [{
      severity: 'HIGH', code: 'SA-CSS-CUR-003',
      note: `Consent button has cursor="${cursor}" — does not show pointer (hand icon); users may not recognise element as clickable; cursor="pointer" expected on interactive consent button`
    }];
  }
  if (attrCursor && attrCursor !== 'pointer' && attrCursor !== 'auto') {
    return [{
      severity: 'MEDIUM', code: 'SA-CSS-CUR-003',
      note: `Consent button SVG cursor attribute="${attrCursor}" — non-standard cursor on interactive element; may suppress user clicks by removing expected interactive affordance`
    }];
  }
  return [];
}

HIGH — SA-CSS-CUR-003: cursor="crosshair" on a consent button suppresses clicks from users who associate the pointer cursor with interactivity. This is a behavioural attack rather than a technical bypass; the user can still click, but may not expect to. Detection should flag any consent button with a non-pointer cursor style as HIGH severity, and any that simultaneously makes a deny/cancel button show a pointer cursor as CRITICAL (asymmetric interactive affordance deliberately steers users toward denial).

Attack 4: JS-injected cursor="crosshair" at mousedown, removed at mouseup (SA-CSS-CUR-004)

A JavaScript mousedown listener sets cursor="crosshair" on the consent button (or the whole consent SVG container) at the start of the click. A mouseup listener removes the attribute. Before the click, the button correctly shows a pointer cursor; during the click window (50–200ms), it shows the crosshair; after the click, it returns to the pointer. The practical effect is that as the user presses the mouse button over the Allow button, the cursor shape changes to a crosshair, which may cause the user to abort the click or hesitate, preventing the consent. A static DOM audit before the click finds no cursor attribute; a static audit after finds none. Only a MutationObserver watching for cursor attribute changes on consent elements detects this.

<!-- SA-CSS-CUR-004: JS-injected cursor="crosshair" at mousedown, removed at mouseup
     Before click: cursor=pointer (normal)
     During click window: cursor=crosshair (confusing -- user may abort)
     After click: cursor removed -- back to default
     Static DOM before/after: no cursor attribute -->

<script>
(function(svgEl) {
  svgEl.addEventListener('mousedown', () => {
    svgEl.setAttribute('cursor', 'crosshair');
  }, true);
  svgEl.addEventListener('mouseup', () => {
    svgEl.removeAttribute('cursor');
  }, true);
})(document.getElementById('consent-svg'));
</script>

// Detection: MutationObserver watching cursor attribute on consent elements
function monitorCursorInjection(consentEl, onFinding) {
  const observer = new MutationObserver(mutations => {
    for (const m of mutations) {
      if (m.attributeName === 'cursor') {
        const newVal = (m.target).getAttribute('cursor');
        if (newVal && newVal !== 'pointer') {
          onFinding({
            severity: 'CRITICAL', code: 'SA-CSS-CUR-004',
            note: `cursor attribute dynamically set to "${newVal}" on <${m.target.tagName}> during consent interaction — cursor changed during click window; may cause user to abort or misaim click; static scan misses temporal injection`
          });
        }
      }
    }
  });
  observer.observe(consentEl, {
    attributes: true,
    attributeFilter: ['cursor'],
    subtree: true,
    attributeOldValue: true
  });
  return observer;
}

CRITICAL — SA-CSS-CUR-004: JS-injected cursor="crosshair" at mousedown changes the cursor shape during the click window and may cause the user to abort the click, preventing the consent. The attribute is removed before mouseup; the static DOM has no cursor attribute before or after. A MutationObserver watching cursor attribute changes on all consent elements must be established before user interaction begins.

Summary table

AttackMechanismWhat it bypassesSeverity
SA-CSS-CUR-001: cursor="none" invisible cursor cursor="none" on consent SVG or ancestor element — cursor invisible over entire consent area; user cannot see where click will land; may click wrong target or cancel element; cursor attribute inherits through SVG tree Opacity/visibility checks (element fully visible); click-through tests (element is clickable); only ancestor-chain cursor attribute + getComputedStyle cursor check detects Critical
SA-CSS-CUR-002: custom cursor hotspot offset cursor: url(image) 20 20 -- hotspot at (20,20) but visual cursor center at (0,0); click registers 20px away from where user aimed; may redirect click to deny/cancel element adjacent to allow button Visual inspection of element layout (button visible); cursor appears as valid consent-related icon; only CSS cursor property hotspot coordinate parsing and offset-magnitude check detects Critical
SA-CSS-CUR-003: cursor="crosshair" non-interactive appearance cursor="crosshair" on consent button — removes pointer (hand) cursor that signals clickability; users may not click; deny/cancel button may retain pointer cursor creating asymmetric affordance All DOM checks (element present, visible, normal opacity); click handler present; only cursor affordance check detects; deny button simultaneously having pointer cursor escalates to CRITICAL High
SA-CSS-CUR-004: JS mousedown cursor injection mousedown listener sets cursor="crosshair"; mouseup removes; cursor changes during 50-200ms click window; user may abort click; static DOM before/after clean; attribute injected and removed within click event sequence Static DOM scan finds no cursor attribute; only MutationObserver on cursor attribute changes on consent elements detects temporal injection Critical

SkillAudit findings for this attack surface

CRITICAL SA-CSS-CUR-001: cursor="none" on consent SVG element or ancestor — cursor invisible over consent area; user cannot verify click target; SVG cursor attribute check on full ancestor chain required; CSS cursor: none also detected via getComputedStyle traversal.
CRITICAL SA-CSS-CUR-002: custom cursor URL on consent element has hotspot offset >8px in either axis — click registers displaced from visual cursor center; may redirect aimed clicks to wrong consent target; CSS cursor property parsed for url(…) X Y hotspot coordinates.
HIGH SA-CSS-CUR-003: consent button has non-pointer cursor (crosshair, default, text, etc.) — removes interactive affordance; asymmetric cursor where deny/cancel has pointer cursor while allow does not escalates to CRITICAL.
CRITICAL SA-CSS-CUR-004: cursor attribute dynamically injected at mousedown and removed at mouseup — cursor changes during click window; static DOM finds no cursor attribute; MutationObserver on cursor attribute on consent elements and ancestors required.

Related: SVG pointer-events consent attacks  |  Dynamic and temporal SVG consent hijack synthesis  |  SVG SMIL animate consent attacks

← Blog  |  Security Checklist