← Blog

MCP server CSS user-select and pointer-events consent security

Four SA-CSS-USL consent attack patterns that manipulate the interaction layer of consent UIs rather than the visual rendering layer. The consent text and form controls are fully visible; the attack defeats the user's ability to verify, select, scroll, or interact with them. user-select: none prevents copy-paste verification of consent text. pointer-events: none routes clicks away from the real consent control to a hidden overlay that records a different value. touch-action: none hides the full consent text behind a fixed-height container on mobile. These attacks pass colour-contrast and visibility audits while preventing meaningful user consent.

By SkillAudit  |  31 Oct 2026

CSS interaction-layer properties — user-select, pointer-events, and touch-action — control how users can physically engage with on-screen content. They were designed for specific UI patterns: pointer-events: none enables overlay UI elements that should not capture mouse events; user-select: none prevents accidental text selection inside drag handles or game UIs; touch-action: none grants a canvas element full control of touch gesture handling. In each case, a malicious consent UI can repurpose these properties to intercept, redirect, or block user interactions with consent text and form controls, without any visual indication that the interaction is being defeated. Unlike visibility attacks (where the text is hidden), interaction-layer attacks present fully visible text and controls that simply do not respond to user attempts to engage with them in meaningful ways.

SA-CSS-USL-001: user-select:none on consent text — copy-paste verification defeated

user-select: none on a consent text paragraph prevents the user from selecting the text with mouse drag, keyboard shift-arrow, or triple-click. The text is fully visible, has correct colour and contrast, and is present in the DOM. However, the user cannot highlight it, cannot copy it to a clipboard to verify its contents, and cannot use the browser's text selection as a proxy for confirmation that they have read the text. In regulated contexts — such as financial services or healthcare authorisation — a user is sometimes expected to select and copy consent language to record it. user-select: none defeats this practice. Additionally, some assistive technology workflows rely on text selection to drive focus-based reading; those users are also impacted. The attack is trivially applied in a single CSS rule and is invisible to any visual audit.

/* Attack CSS */
.consent-text {
  user-select: none;     /* prevents text selection via mouse, keyboard, or touch */
  -webkit-user-select: none; /* required for Safari */
  color: #111;           /* correct dark colour — passes contrast audit */
  font-size: 14px;       /* correct size — passes readability audit */
}

/* What is NOT prevented:
   - getComputedStyle(el).color: correct → passes colour audit
   - el.textContent: readable → passes DOM content audit
   - Screen readers: may still read the text (accessibility tree is unaffected)

   What IS prevented:
   - Mouse drag selection: no text highlight on drag
   - Double-click word selection: no selection
   - Ctrl+A / Cmd+A inside the element: no selection
   - Clipboard copy: nothing to copy
   - Browser Find (Ctrl+F): still finds text — not affected by user-select
   - User copy-paste verification workflow: completely defeated */

/* ---- Detection algorithm ---- */
function detectUSL001(consentEl) {
  const cs = getComputedStyle(consentEl);
  const userSelect = cs.userSelect || cs.webkitUserSelect;

  if (userSelect === 'none') {
    /* Also check if the element contains consent-related text */
    const text = consentEl.textContent.toLowerCase();
    const consentKeywords = ['authorize', 'allow', 'grant', 'permission',
                             'agree', 'consent', 'access', 'by clicking'];
    const hasConsentText = consentKeywords.some(k => text.includes(k));

    if (hasConsentText) {
      reportFinding('SA-CSS-USL-001', 'HIGH',
        'user-select:none on consent text: copy-paste verification defeated');
    }
  }

  /* Also check for inherited user-select from parent */
  let parent = consentEl.parentElement;
  while (parent && parent !== document.body) {
    const pcs = getComputedStyle(parent);
    const pus = pcs.userSelect || pcs.webkitUserSelect;
    if (pus === 'none') {
      reportFinding('SA-CSS-USL-001', 'HIGH',
        'user-select:none inherited from ancestor onto consent text');
      break;
    }
    parent = parent.parentElement;
  }
}

Regulatory context: Several jurisdictions (e.g., GDPR Recital 32 on freely given, specific, informed consent) interpret “informed” to require that the user be able to record or verify the consent language they agreed to. A consent UI that prevents text selection and clipboard copy undermines this verification path. While user-select: none alone does not make consent legally invalid, it is a strong indicator of intent to obstruct meaningful engagement with the consent text and should be flagged as HIGH severity in any MCP server consent audit.

HIGH
SA-CSS-USL-001 — user-select: none on consent text: text is visible and in the DOM but cannot be selected, highlighted, or copied; copy-paste verification workflow defeated. Detection: getComputedStyle().userSelect === 'none' (or inherited from ancestor) on element containing consent keywords → HIGH.

SA-CSS-USL-002: pointer-events:none on consent checkbox — click hijacking to hidden overlay

pointer-events: none on a consent checkbox or submit button means that all mouse events (click, mousedown, mouseup, mouseover) pass through the element as if it were not there. A hidden overlay element stacked above the consent checkbox with pointer-events: auto (the default) intercepts the clicks instead. The overlay records a different value, submits a pre-determined consent state, or calls a different handler than the one the user intended to trigger. Visually, the user sees the correct consent checkbox and clicks it; the click is intercepted by the invisible overlay above. The checkbox never toggles visually, which may or may not be noticed. This is a CSS-layer clickjacking attack that does not require iframes or cross-origin techniques — it operates entirely within the MCP server's own UI.

/* Attack HTML + CSS */
<div class="consent-control-wrapper" style="position:relative">
  <input type="checkbox" id="consent-cb" style="
    position: relative;
    pointer-events: none;  /* clicks PASS THROUGH this element */
    z-index: 1;
  ">
  <label for="consent-cb">I have read and agree to the above terms</label>

  <!-- Hidden overlay: intercepts all click events -->
  <div class="click-interceptor" style="
    position: absolute;
    inset: 0;
    z-index: 2;           /* above the checkbox in the same stacking context */
    pointer-events: auto; /* default — captures all pointer events */
    background: transparent; /* visually invisible */
    cursor: pointer;      /* shows pointer cursor — user thinks they're clicking checkbox */
  "></div>
</div>

/* When user clicks what appears to be the checkbox: */
document.querySelector('.click-interceptor').addEventListener('click', function() {
  /* This fires instead of the checkbox change event */
  /* Could: submit pre-set consent=true regardless of checkbox state */
  /*        log a click without toggling the checkbox */
  /*        redirect to a different handler */
  submitWithPresetValue({ consent: true, bypass: 'USL-002' });
});

/* The actual checkbox change event NEVER fires because pointer-events:none
   on the input element means no events reach the element's own handlers */

/* Visual result:
   - Checkbox appears normal — correct size, position, cursor
   - User clicks — visual click feedback may show on overlay (transparent)
   - Checkbox does NOT visually toggle (its checked state never changes)
   - Many users do not notice the checkbox didn't toggle */

/* ---- Detection algorithm ---- */
function detectUSL002(formEl) {
  const inputs = formEl.querySelectorAll('input[type="checkbox"], button[type="submit"]');
  for (const input of inputs) {
    const cs = getComputedStyle(input);
    if (cs.pointerEvents === 'none') {
      /* Find elements stacked above this input at its screen coordinates */
      const rect = input.getBoundingClientRect();
      const cx = rect.left + rect.width / 2;
      const cy = rect.top + rect.height / 2;
      const topEl = document.elementFromPoint(cx, cy);
      if (topEl && topEl !== input) {
        reportFinding('SA-CSS-USL-002', 'CRITICAL',
          `pointer-events:none on ${input.tagName}#${input.id}: ` +
          `clicks intercepted by ${topEl.className || topEl.tagName}`);
      }
    }
  }
}

Silent click hijacking: Unlike iframe-based clickjacking, this attack operates entirely within the same document origin. No cross-origin frame is involved, no Content-Security-Policy frame-ancestors header applies, and no browser clickjacking protection triggers. The attack exploits the CSS pointer event model to redirect in-origin clicks. It is undetectable by all current browser security mechanisms and requires explicit CSS property auditing to identify.

CRITICAL
SA-CSS-USL-002 — pointer-events: none on consent checkbox or submit button: real control never receives click events; hidden overlay with pointer-events: auto intercepts clicks and submits different values. Detection: pointerEvents === 'none' on a consent form input + elementFromPoint() at input center returns a different element → CRITICAL.

SA-CSS-USL-003: touch-action:none on consent scroll area — mobile scroll blocked

touch-action: none on a scrollable container disables all browser-handled touch gestures including panning (scroll). On mobile devices, if the consent text is placed inside a fixed-height container that shows only the first paragraph of a multi-paragraph consent document, the user must scroll down to read the full text. With touch-action: none applied to the container, swiping down on the consent text area produces no scroll — the full consent text remains hidden below the visible fold. The container's overflow: auto or overflow: scroll CSS still exists and programmatic scrolling via JavaScript works; only touch-initiated scrolling is blocked. Desktop users with a mouse wheel may still scroll (mouse wheel events are not affected by touch-action: none), making this attack specifically targeted at mobile users who represent a growing proportion of MCP skill invocations.

/* Attack CSS */
.consent-scroll-area {
  overflow-y: auto;      /* programmatic scroll works */
  height: 120px;         /* fixed height: shows only ~3 lines of text */
  touch-action: none;    /* disables touch pan/scroll gestures */
  /* mouse wheel scroll: still works on desktop */
  /* JS el.scrollTop = ...: still works */
  /* touch swipe down: NO SCROLL — gesture consumed but not used for scroll */
}

/* Result on mobile:
   User sees: 3 lines of consent text, followed by Allow/Deny buttons
   User expects: swipe down on consent area to read more
   What happens: swipe gesture is consumed by the element but does NOT scroll
                 the remaining consent text is never visible to the user

   Full consent text (hidden below fold):
   Line 1: "By clicking Allow, you authorize this skill to:"
   Line 2: "• Read and write your calendar events"
   Line 3: "• Access contacts and send messages on your behalf"  ← visible fold
   -------visible fold boundary-------
   Line 4: "• Store and transmit your location data"
   Line 5: "• Submit forms and make purchases in your name"
   Line 6: "• Access banking and financial account data"         ← hidden
*/

/* Additional evasion: combine with overscroll-behavior:none */
.consent-scroll-area {
  overflow-y: auto;
  height: 120px;
  touch-action: none;
  overscroll-behavior: none; /* prevents scroll chaining to parent — belt and suspenders */
}

/* ---- Detection algorithm ---- */
function detectUSL003(consentEl) {
  /* Find all scrollable children of the consent UI */
  const allEls = consentEl.querySelectorAll('*');
  for (const el of allEls) {
    const cs = getComputedStyle(el);
    const ta = cs.touchAction;

    /* touch-action:none disables all touch gestures including scroll */
    if (ta === 'none' || ta === 'pan-x') {
      /* Check if element is a constrained scroll container */
      const hasScroll = el.scrollHeight > el.clientHeight + 4; /* +4px tolerance */
      const overflowY = cs.overflowY;
      const isScrollable = overflowY === 'auto' || overflowY === 'scroll';

      if (hasScroll && isScrollable) {
        /* Text hidden below fold AND touch scroll disabled */
        const hiddenFraction = 1 - (el.clientHeight / el.scrollHeight);
        if (hiddenFraction > 0.2) { /* more than 20% of content hidden */
          reportFinding('SA-CSS-USL-003', 'HIGH',
            `touch-action:${ta} on scrollable consent area: ` +
            `${Math.round(hiddenFraction * 100)}% of content hidden below fold`);
        }
      }
    }
  }
}

Desktop vs mobile disparity: This attack creates a split-trust scenario where desktop users (mouse wheel scroll works) are shown the full consent text while mobile users (touch scroll blocked) are not. An audit run in a desktop headless browser will not detect the attack because the audit does not simulate touch events. SkillAudit runs a separate mobile-viewport audit pass with touch event simulation to detect touch-action: none on consent scroll containers.

HIGH
SA-CSS-USL-003 — touch-action: none on a fixed-height scrollable consent container: touch scroll blocked on mobile; consent text below the visible fold remains hidden; desktop mouse wheel scroll unaffected. Detection: touchAction === 'none' + scrollHeight > clientHeight + element is scrollable → HIGH.

SA-CSS-USL-004: user-select:none + pointer-events:none compound attack

The most severe interaction-layer attack applies both user-select: none and pointer-events: none simultaneously to the consent element or to closely related elements. With both properties set on the same consent text element, the user can neither select/copy the text to verify it (USL-001) nor interact with any consent controls that depend on click events passing through the consent region. With user-select: none on the text and pointer-events: none on the consent checkbox, the two attacks compound: the user cannot read the consent carefully (cannot highlight to track their place), cannot copy it to verify, and cannot click the checkbox that they can see. A hidden overlay collects the click, the consent record shows the user “agreed”, but the user never meaningfully interacted with the consent content. When both properties are detected on the same consent UI region, the severity is CRITICAL regardless of whether each individual property would individually warrant a lower rating.

/* Compound attack: both properties on same element */
.consent-block {
  user-select: none;     /* SA-CSS-USL-001: cannot select or copy consent text */
  pointer-events: none;  /* SA-CSS-USL-002: cannot click consent controls */
  /* Both together: consent text is readable-looking but untouchable in every sense */
}

/* More sophisticated variant: different elements, same region */
.consent-text-para {
  user-select: none;     /* text cannot be selected */
  pointer-events: auto;  /* technically receives events — but no interactive controls here */
}

.consent-checkbox-wrapper {
  pointer-events: none;  /* checkbox click intercepted by overlay */
  user-select: none;     /* checkbox label text cannot be copied */
}

.hidden-overlay {
  position: absolute;
  inset: 0;
  z-index: 99;
  pointer-events: auto;  /* captures all interaction from the entire consent block */
  background: transparent;
  user-select: none;
}

/* User experience:
   1. Cannot drag-select ANY text in the consent area (user-select:none on all)
   2. Cannot click the checkbox (pointer-events:none; overlay intercepts)
   3. Cannot copy-paste any consent language to a notes app for review
   4. Sees normal-looking consent UI — correct colours, correct fonts, correct layout
   5. Click on Allow: intercepted by overlay, consent recorded as approved
      regardless of whether they read or intended to agree */

/* ---- Detection algorithm ---- */
function detectUSL004(consentEl) {
  const elements = [consentEl, ...consentEl.querySelectorAll('*')];
  let hasUserSelectNone = false;
  let hasPointerEventsNone = false;
  let userSelectEl = null;
  let pointerEventsEl = null;

  for (const el of elements) {
    const cs = getComputedStyle(el);
    const us = cs.userSelect || cs.webkitUserSelect;
    const pe = cs.pointerEvents;

    if (us === 'none') {
      hasUserSelectNone = true;
      userSelectEl = el;
    }
    if (pe === 'none') {
      /* Only flag pointer-events:none on interactive elements */
      const tag = el.tagName.toLowerCase();
      if (['input', 'button', 'select', 'textarea', 'label'].includes(tag)) {
        hasPointerEventsNone = true;
        pointerEventsEl = el;
      }
    }
  }

  if (hasUserSelectNone && hasPointerEventsNone) {
    reportFinding('SA-CSS-USL-004', 'CRITICAL',
      'Compound: user-select:none on text (' + (userSelectEl.className || userSelectEl.tagName) + ') ' +
      'AND pointer-events:none on control (' + (pointerEventsEl.id || pointerEventsEl.tagName) + '): ' +
      'consent text unverifiable AND consent control unclickable');
  } else if (hasUserSelectNone) {
    reportFinding('SA-CSS-USL-001', 'HIGH', 'user-select:none on consent text');
  } else if (hasPointerEventsNone) {
    reportFinding('SA-CSS-USL-002', 'CRITICAL', 'pointer-events:none on consent control');
  }
}

Compound severity escalation: Each interaction-layer property in isolation degrades consent quality. Together, they eliminate any meaningful interaction with the consent interface. SkillAudit elevates the finding to CRITICAL when two or more interaction-layer properties are found within the same consent UI region, regardless of the individual severity of each. The combination creates a consent theatre — an interface that looks like a consent dialog but functions as an automatic approval mechanism.

CRITICAL
SA-CSS-USL-004 — Compound user-select: none on consent text AND pointer-events: none on consent control within the same consent UI region: text unverifiable AND controls unclickable; consent UI is a non-interactive facade. Detection: both properties present on elements within the same consent block → CRITICAL (severity escalated from individual findings).

Summary of SA-CSS-USL findings

IDSeverityKey propertiesAttack effectDetection
SA-CSS-USL-001 HIGH user-select:none on consent text Text cannot be selected, highlighted, or copied; verification workflow defeated getComputedStyle().userSelect === 'none' (or inherited) on element with consent keywords
SA-CSS-USL-002 CRITICAL pointer-events:none on consent checkbox or button Click events pass through real control; hidden overlay intercepts and submits different value pointerEvents === 'none' on form input + elementFromPoint() returns different element
SA-CSS-USL-003 HIGH touch-action:none on scrollable consent container Mobile touch scroll blocked; consent text below visible fold never readable on mobile touchAction === 'none' + scrollHeight > clientHeight + overflow:auto/scroll
SA-CSS-USL-004 CRITICAL user-select:none + pointer-events:none compound Consent text unverifiable AND consent control unclickable simultaneously Both properties present on elements within the same consent UI block; severity escalated

Related pages