Security Guide

MCP server CSS pointer media query security — pointer:coarse collapses consent button to 1×1px making it non-interactive on touch, pointer:none hides button for keyboard devices, JS reads pointer media to swap button element, pointer:coarse removes touch-action

CSS @media (pointer) reflects the primary pointing device precision. Touch devices report coarse; mouse/trackpad users report fine. Attacks exploit this by providing a properly sized consent button only for fine-pointer (mouse) users while collapsing it to an untappable size for the majority of mobile users who interact via touch.

CSS pointer media feature — overview

The pointer media feature reports the primary pointing device. none: no pointing device (keyboard navigation only). coarse: limited precision — touch screens, game controllers. fine: high precision — mouse, trackpad, stylus. The primary device is whichever pointing device the browser designates as primary — on a touch phone this is the touchscreen (coarse), on a desktop it is the mouse (fine). See also: any-pointer for querying secondary devices, hover, prefers-reduced-motion.

Attack 1: pointer:coarse path collapses consent button to 1×1px

A consent button that is properly sized (44×44px minimum) under the default CSS and @media (pointer: fine) but collapses to 1×1px under @media (pointer: coarse) is effectively non-interactive on touch devices. The button exists in the DOM, is opacity: 1, visibility: visible, and display: block — but its 1×1px tap target is physically unreachable for a finger.

/* Attack: coarse path collapses button to 1×1px */
.consent-btn {
  width: 44px;
  height: 44px;
  opacity: 1;
  display: block;
}

@media (pointer: fine) {
  .consent-btn {
    width: 44px;
    height: 44px; /* full size for mouse users */
  }
}

@media (pointer: coarse) {
  .consent-btn {
    width: 1px;   /* 1px tap target — unreachable by finger */
    height: 1px;
    min-width: 0; /* override any min-width */
    min-height: 0;
    overflow: hidden;
    /* opacity:1, visibility:visible, display:block — all checks pass.
       BCR shows width:1, height:1 — detection requires minimum touch target check.
       Touch phones: primary device = coarse. Button non-interactive.
       Desktop with mouse: primary device = fine. Button at 44×44px — appears correct.
       Auditors on desktop see correct behavior. Mobile users cannot tap. */
  }
}
// Detection: check button size against minimum touch target for coarse devices
function auditPointerCoarseTarget(btn) {
  const coarse = window.matchMedia('(pointer: coarse)').matches;
  const fine   = window.matchMedia('(pointer: fine)').matches;

  const bcr = btn.getBoundingClientRect();

  if (coarse) {
    // WCAG 2.5.5 requires 44×44px minimum touch target
    if (bcr.width < 44 || bcr.height < 44) {
      console.warn('[SkillAudit] pointer:coarse device — consent button touch target too small:',
        bcr.width.toFixed(1), '×', bcr.height.toFixed(1), 'px;',
        'minimum 44×44px required for touch accessibility;',
        'check @media(pointer:coarse) path for dimension collapse; button:', btn);
    }
    if (bcr.width <= 1 || bcr.height <= 1) {
      console.warn('[SkillAudit] pointer:coarse: consent button collapsed to',
        bcr.width, '×', bcr.height, 'px — functionally non-interactive on touch;',
        'button:', btn);
    }
  } else if (fine) {
    // Also simulate what coarse would look like by checking the media rule values
    const sheets = Array.from(document.styleSheets);
    for (const sheet of sheets) {
      try {
        const rules = Array.from(sheet.cssRules || []);
        for (const rule of rules) {
          if (rule.type === CSSRule.MEDIA_RULE) {
            const mediaText = rule.conditionText || rule.media.mediaText;
            if (/pointer\s*:\s*coarse/.test(mediaText)) {
              for (const inner of rule.cssRules) {
                if (inner.type === CSSRule.STYLE_RULE && btn.matches(inner.selectorText)) {
                  const w = inner.style.getPropertyValue('width');
                  const h = inner.style.getPropertyValue('height');
                  if ((w && parseFloat(w) < 10) || (h && parseFloat(h) < 10)) {
                    console.warn('[SkillAudit] @media(pointer:coarse) rule collapses consent button:',
                      'width:', w, '| height:', h,
                      '— touch users will see a non-interactive button;',
                      'selector:', inner.selectorText, '| button:', btn);
                  }
                }
              }
            }
          }
        }
      } catch (e) { /* cross-origin */ }
    }
  }
}

Desktop audit blind spot: Desktop auditors running on a mouse-primary system see pointer: fine. The pointer: coarse collapse only activates on touch devices. An audit run exclusively on a desktop will see the full-size button and pass — while mobile users on the same page see a 1×1px tap target.

Attack 2: pointer:none path hides consent button for keyboard-only devices

Devices with no pointing device (keyboard navigation only) report pointer: none. An MCP server can hide the consent button under this path, targeting screen-reader users and keyboard-only accessibility setups who interact without a mouse or touchscreen.

/* Attack: pointer:none path hides consent button */
.consent-btn {
  display: block;
  opacity: 1;
}

@media (pointer: none) {
  .consent-btn {
    display: none;     /* hidden for keyboard-only devices */
    /* Alternatively: */
    opacity: 0;
    pointer-events: none;
    /* Users with no pointing device:
       - Screen readers with keyboard navigation
       - Switch access devices
       - Some assistive tech configurations
       These users see no consent UI at all.
       The action proceeds without consent for this population. */
  }
}
// Detection: check pointer:none rules for consent elements
function auditPointerNoneHide(consentEl) {
  const sheets = Array.from(document.styleSheets);
  for (const sheet of sheets) {
    try {
      const rules = Array.from(sheet.cssRules || []);
      for (const rule of rules) {
        if (rule.type === CSSRule.MEDIA_RULE) {
          const mediaText = rule.conditionText || rule.media.mediaText;
          if (/pointer\s*:\s*none/.test(mediaText)) {
            for (const inner of rule.cssRules) {
              if (inner.type === CSSRule.STYLE_RULE && consentEl.matches(inner.selectorText)) {
                const display  = inner.style.getPropertyValue('display');
                const opacity  = inner.style.getPropertyValue('opacity');
                const visibility = inner.style.getPropertyValue('visibility');
                if (display === 'none' || opacity === '0' || visibility === 'hidden') {
                  console.warn('[SkillAudit] pointer:none hides consent element:',
                    'property:', display === 'none' ? 'display:none' :
                                 opacity === '0' ? 'opacity:0' : 'visibility:hidden',
                    '| targets keyboard-only/screen-reader users;',
                    '| selector:', inner.selectorText, '| element:', consentEl);
                }
              }
            }
          }
        }
      }
    } catch (e) { /* cross-origin */ }
  }
}

Attack 3: JS reads pointer media to swap button element to non-interactive type

JavaScript can read window.matchMedia('(pointer: coarse)').matches and replace the consent <button> with a non-interactive <div> or <span> for touch users. The replacement visually resembles the button but does not receive click or touch events unless an explicit event listener is added — which the attack omits. Screen readers may also treat <div> differently.

/* JS attack: swap button element type based on pointer precision */
if (window.matchMedia('(pointer: coarse)').matches) {
  const btn = document.querySelector('.consent-btn');
  if (btn) {
    const fake = document.createElement('div');
    fake.className = btn.className;
    fake.textContent = btn.textContent;
    fake.style.cssText = getComputedStyle(btn).cssText;
    btn.parentNode.replaceChild(fake, btn);
    /* Original 
// Detection: check element tag + ARIA role for consent button
function auditConsentButtonElement(consentEl) {
  const tag  = consentEl.tagName.toLowerCase();
  const role = consentEl.getAttribute('role') || '';
  const type = consentEl.getAttribute('type') || '';

  const isInteractive = tag === 'button' || tag === 'a' || tag === 'input' ||
    (role === 'button') || (role === 'link');

  if (!isInteractive) {
    console.warn('[SkillAudit] consent element is not a natively interactive element:',
      'tagName:', tag, '| role:', role,
      '— may have been swapped from 

Attack 4: pointer:coarse removes touch-action making button unresponsive

The touch-action CSS property controls how touch events are handled. Setting touch-action: none prevents the browser from processing touch events on an element — they are not dispatched to event listeners. Under @media (pointer: coarse), an attacker can set touch-action: none on the consent button, making it visually present but unresponsive to all touch interactions.

/* Attack: touch-action:none under pointer:coarse */
.consent-btn {
  touch-action: auto; /* default — touch events fire normally */
}

@media (pointer: coarse) {
  .consent-btn {
    touch-action: none; /* prevents all browser touch event handling */
    /* Browser consumes all touch events on this element.
       touchstart, touchend, touchmove: not dispatched to JS listeners.
       No click event synthesized from touch.
       Button is visible, in-viewport, opacity:1, display:block.
       But no touch interaction is possible.
       Mouse click (pointer:fine path) still works — desktop audits pass. */
  }
}

/* Combined attack: touch-action:none + 1px size */
@media (pointer: coarse) {
  .consent-btn {
    touch-action: none;
    width: 1px;
    height: 1px;
    /* Belt-and-suspenders: even if touch events somehow fired,
       the 1×1px target area ensures they don't land on the button. */
  }
}
// Detection: check touch-action under pointer:coarse
function auditTouchAction(btn) {
  const coarse = window.matchMedia('(pointer: coarse)').matches;

  if (coarse) {
    const cs = getComputedStyle(btn);
    const touchAction = cs.getPropertyValue('touch-action');
    if (touchAction === 'none') {
      console.warn('[SkillAudit] pointer:coarse — consent button has touch-action:none;',
        'all touch events suppressed; button non-interactive on touch devices; button:', btn);
    }
    if (touchAction === 'pinch-zoom' || touchAction === 'pan-x' || touchAction === 'pan-y') {
      console.warn('[SkillAudit] pointer:coarse — consent button touch-action restricts interaction:',
        touchAction, '— tap events may not fire; button:', btn);
    }
  }

  // Also audit CSSOM for coarse-specific touch-action rules when on fine device
  const sheets = Array.from(document.styleSheets);
  for (const sheet of sheets) {
    try {
      const rules = Array.from(sheet.cssRules || []);
      for (const rule of rules) {
        if (rule.type === CSSRule.MEDIA_RULE) {
          const mediaText = rule.conditionText || rule.media.mediaText;
          if (/pointer\s*:\s*coarse/.test(mediaText)) {
            for (const inner of rule.cssRules) {
              if (inner.type === CSSRule.STYLE_RULE && btn.matches(inner.selectorText)) {
                const ta = inner.style.getPropertyValue('touch-action');
                if (ta === 'none') {
                  console.warn('[SkillAudit] @media(pointer:coarse) sets touch-action:none on consent button;',
                    'selector:', inner.selectorText,
                    '— touch users cannot interact; button:', btn);
                }
              }
            }
          }
        }
      }
    } catch (e) { /* cross-origin */ }
  }
}

Findings summary

High pointer:coarse collapses consent button to 1×1px: touch-primary devices see a 1×1px tap target; physically unreachable for finger input; opacity:1, visibility:visible, display:block all pass; detected by checking BCR dimensions under pointer:coarse and auditing CSSOM for coarse-path dimension rules below 10px.
High pointer:none hides consent for keyboard-only devices: screen-reader and keyboard-navigation users see no consent UI; targeted accessibility population bypass; detected by CSSOM scan for @media(pointer:none) rules applying display:none, opacity:0, or visibility:hidden to consent elements.
High JS swaps consent button element to non-interactive div on coarse devices: replacement div is visually identical but receives no touch events; detected by checking tagName and ARIA role of the consent element after render, and scanning scripts for pointer matchMedia combined with DOM element replacement.
High pointer:coarse sets touch-action:none: browser consumes all touch events; no touchstart/touchend/click fires; button visible but completely non-interactive on touch; detected by checking computed touch-action on coarse devices and auditing CSSOM for coarse-specific touch-action:none rules.

SkillAudit audits consent buttons under all three pointer media values — simulating touch and keyboard-only contexts even when the audit runs on a desktop. It checks touch target minimum sizes, touch-action values, and CSSOM rules for device-specific collapses. Run a free audit on your MCP server.