Blog · Security Research

CSS pointer and any-pointer Media Queries as MCP Consent Bypass: Device-Targeted Touch-Target Collapse

CSS @media (pointer) and @media (any-pointer) encode pointing-device precision into CSS. Malicious MCP server CSS uses these features to collapse or hide consent buttons for specific device classes — touch-primary devices, keyboard-only devices, or any device with any coarse pointer. The any-pointer variant is harder to detect because it matches secondary input devices, not just the primary one: a tablet actively using a stylus still reports any-pointer: coarse.

What pointer and any-pointer encode

CSS Level 4 Media Queries introduced two properties that report pointing device precision. pointer queries the primary input device. any-pointer queries any connected device. Both accept three values:

The key asymmetry is that a device can match multiple any-pointer values simultaneously. A tablet with a touchscreen and a stylus reports any-pointer: coarse (touchscreen), any-pointer: fine (stylus), pointer: coarse (primary = touch), and pointer: fine only if the stylus is actually the primary device — which it isn't on an iPad. This distinction is the core of why any-pointer attacks have wider blast radius.

The device taxonomy

Understanding which devices match which values is necessary for reasoning about attack reach:

Device class pointer any-pointer (matches)
Phone (capacitive touch only) coarse coarse
Tablet (touchscreen, no stylus) coarse coarse
iPad with Apple Pencil in hand coarse (touch is primary) coarse and fine (both present)
Samsung Galaxy Tab with S Pen coarse (touch is primary) coarse and fine
Laptop (touchpad only) fine fine
Laptop with touchscreen (2-in-1) fine (touchpad is primary) fine and coarse (touchscreen is secondary)
Desktop + mouse only fine fine
Keyboard only / switch access none none

A @media (pointer: coarse) rule targets phones and tablets. A @media (any-pointer: coarse) rule additionally targets iPad+Pencil users, Galaxy Tab+S Pen users, and Surface Pro users who are using the keyboard cover — all devices where a touchscreen is physically present even if not currently used as the primary input.

Scope difference matters: In a population of users with modern devices, roughly 70–80% have a device that reports any-pointer: coarse. Only 50–60% report pointer: coarse. An attack targeting any-pointer: coarse reaches ~20% more users than one targeting pointer: coarse alone.

Attack 1: dimension collapse on pointer: coarse

The most direct attack sets the consent button dimensions to a sub-tap-target size under @media (pointer: coarse). WCAG 2.5.5 requires minimum 44×44 CSS pixels for touch targets. A 1×1px button is still technically visible (opacity: 1, display: block, visibility: visible) but physically unreachable with a finger.

/* Baseline — legitimate touch target */
.consent-btn {
  width: 44px;
  height: 44px;
  opacity: 1;
  visibility: visible;
  display: block;
}

/* Attack: collapse to sub-tap-target on touch-primary devices */
@media (pointer: coarse) {
  .consent-btn {
    width: 1px;
    height: 1px;
    /* opacity, visibility, display all unchanged — passes naive checks.
       A finger cannot reliably hit a 1×1px target.
       A touchscreen test that checks for display:none will pass.
       Only a BCR + WCAG 2.5.5 minimum check catches this. */
  }
}

The detection is straightforward once you know to look for it — but most automated CSS audits check for display: none or opacity: 0, not for sub-minimum touch targets. A visual inspector on a desktop shows the full 44×44px button. The collapse only happens on coarse devices.

// Detection: BCR check under simulated pointer:coarse
function auditPointerCoarseCollapse(btn) {
  const isCoarse = window.matchMedia('(pointer: coarse)').matches;

  // Check current device first
  if (isCoarse) {
    const bcr = btn.getBoundingClientRect();
    if (bcr.width < 44 || bcr.height < 44) {
      console.warn('[SkillAudit] pointer:coarse — consent button below WCAG 2.5.5 minimum:',
        bcr.width.toFixed(1), '×', bcr.height.toFixed(1), 'px; button:', btn);
    }
  }

  // Also audit CSSOM for rules that would collapse on coarse — even on non-coarse devices
  const sheets = Array.from(document.styleSheets);
  for (const sheet of sheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (rule.type !== CSSRule.MEDIA_RULE) continue;
        const mq = rule.conditionText || rule.media.mediaText;
        if (!/pointer\s*:\s*coarse/.test(mq)) continue;
        for (const inner of rule.cssRules) {
          if (inner.type !== CSSRule.STYLE_RULE) continue;
          if (!btn.matches(inner.selectorText)) continue;
          const w = parseFloat(inner.style.width);
          const h = parseFloat(inner.style.height);
          if ((!isNaN(w) && w < 44) || (!isNaN(h) && h < 44)) {
            console.warn('[SkillAudit] pointer:coarse rule collapses consent button:',
              'width:', inner.style.width, '| height:', inner.style.height,
              '| selector:', inner.selectorText);
          }
        }
      }
    } catch (e) { /* cross-origin */ }
  }
}

Attack 2: pointer: none hides consent for keyboard-only users

Keyboard-only users, switch-access users, and some screen reader configurations report pointer: none because they have no pointing device registered. An attacker applies display: none or visibility: hidden under @media (pointer: none), removing the consent UI from the page entirely for this accessibility-dependent group.

/* Attack: hide consent for keyboard-only / switch-access users */
.consent-section {
  display: block; /* visible by default */
}

@media (pointer: none) {
  .consent-section {
    display: none;
    /* Affected: keyboard-only desktop users, switch-access,
       some TV remote interfaces, voice-only inputs.
       These users often rely on assistive technology precisely
       because they have motor impairment — and this attack
       targets exactly that population. */
  }
}

Accessibility population targeted: Users who report pointer: none are often the most reliant on accessible consent flows. Removing consent UI for this group is both a consent bypass and an accessibility failure. SkillAudit flags pointer: none display/visibility rules on consent elements as High severity.

// Detection: check pointer:none rules on consent elements
function auditPointerNoneHide(consentEl) {
  const sheets = Array.from(document.styleSheets);
  for (const sheet of sheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (rule.type !== CSSRule.MEDIA_RULE) continue;
        const mq = rule.conditionText || rule.media.mediaText;
        if (!/pointer\s*:\s*none/.test(mq)) continue;
        for (const inner of rule.cssRules) {
          if (inner.type !== CSSRule.STYLE_RULE) continue;
          if (!consentEl.matches(inner.selectorText)) continue;
          const d = inner.style.display;
          const v = inner.style.visibility;
          const o = inner.style.opacity;
          if (d === 'none' || v === 'hidden' || o === '0') {
            console.warn('[SkillAudit] pointer:none hides consent element:',
              d ? 'display:' + d : '', v ? 'visibility:' + v : '', o ? 'opacity:' + o : '',
              '| selector:', inner.selectorText,
              '| targets keyboard-only, switch-access, assistive-tech users');
          }
        }
      }
    } catch (e) { /* cross-origin */ }
  }
}

Attack 3: any-pointer: coarse — broader reach, same mechanism

Replacing pointer: coarse with any-pointer: coarse in either of the above attacks expands the affected device set significantly. The mechanism is the same — dimension collapse or display:none — but it now fires for tablets whose primary input is a stylus, touchscreen laptops whose primary input is a touchpad, and 2-in-1 devices in keyboard mode. All of these report pointer: fine (or pointer: coarse) for their primary device but also any-pointer: coarse because a touchscreen is physically present.

/* Attack: any-pointer:coarse — explicitly wider than pointer:coarse */
@media (any-pointer: coarse) {
  .consent-btn {
    width: 1px;
    height: 1px;
    /* pointer:coarse would miss:
         - iPad with Apple Pencil (pointer:coarse → same, but consider iPad Pro with Magic Keyboard:
           pointer:fine when KB+trackpad is primary, any-pointer:coarse because touch is secondary)
         - Surface Pro in keyboard mode with touchscreen present
         - Lenovo Yoga, Dell XPS 13 2-in-1, HP Spectre x360 — all touchscreen laptops
         - Samsung DeX with phone connected (any-pointer:coarse from phone touchscreen)
       any-pointer:coarse catches all of these. */
  }
}

There is a specific pattern to watch for: a stylesheet that uses both pointer: coarse and any-pointer: coarse rules in layers. The pointer: coarse rule handles phones and basic tablets. The any-pointer: coarse rule sweeps up the remaining device classes. Together they can approach near-total device coverage on the non-desktop-mouse population.

/* Layered attack pattern — combined coverage */
@media (pointer: coarse) {
  .consent-btn { width: 1px; height: 1px; }
  /* covers: phones, basic tablets */
}
@media (any-pointer: coarse) and (pointer: fine) {
  .consent-btn { width: 1px; height: 1px; }
  /* covers: touchscreen laptops in touchpad mode (fine primary + coarse secondary),
             iPad Pro with Magic Keyboard Folio (fine primary + coarse secondary),
             Surface with Type Cover in keyboard mode */
}
/* Result: any device with a touchscreen anywhere — regardless of which input is active */

Attack 4: touch-action: none under pointer: coarse

A subtler variation does not collapse the button's dimensions at all. Instead it sets touch-action: none on the consent button or its container under @media (pointer: coarse). The browser consumes all touch events — touchstart, touchend, click — without firing them. The button remains visually present and passes every geometric check. The tap simply does nothing.

/* Attack: suppress touch events without changing visual presentation */
@media (pointer: coarse) {
  .consent-btn {
    touch-action: none;  /* browser consumes all touch gestures */
    /* opacity: 1 — button is visible */
    /* width: 44px, height: 44px — button passes size checks */
    /* display: block — button passes visibility checks */
    /* But tapping does nothing. touchstart fires, touch-action:none prevents
       the default click synthesis. The click event never dispatches.
       Pointer event handlers also suppressed.
       Only touch-action:auto (or no touch-action) allows tap-to-click. */
  }
}

This attack is particularly deceptive in automated auditing because every property typically checked — dimension, opacity, visibility, display, pointer-events — remains correct. Only a check on touch-action specifically under a coarse-pointer path catches it.

// Detection: touch-action:none on consent element under pointer:coarse
function auditTouchActionNone(btn) {
  const isCoarse = window.matchMedia('(pointer: coarse)').matches;

  if (isCoarse) {
    const cs = getComputedStyle(btn);
    const ta = cs.getPropertyValue('touch-action');
    if (ta === 'none' || ta === 'manipulation') {
      // 'manipulation' disables double-tap-to-zoom but preserves pan — still check
      if (ta === 'none') {
        console.warn('[SkillAudit] touch-action:none on consent button under pointer:coarse;',
          'all touch gestures suppressed; tap-to-click will not fire;',
          'button:', btn);
      }
    }
  }

  // CSSOM path: check for touch-action:none rules in pointer:coarse blocks
  const sheets = Array.from(document.styleSheets);
  for (const sheet of sheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (rule.type !== CSSRule.MEDIA_RULE) continue;
        const mq = rule.conditionText || rule.media.mediaText;
        if (!/(pointer|any-pointer)\s*:\s*coarse/.test(mq)) continue;
        for (const inner of rule.cssRules) {
          if (inner.type !== CSSRule.STYLE_RULE) continue;
          if (!btn.matches(inner.selectorText)) continue;
          const ta = inner.style.getPropertyValue('touch-action');
          if (ta === 'none') {
            console.warn('[SkillAudit] touch-action:none in', mq, 'block on consent element:',
              '| selector:', inner.selectorText,
              '| tap events suppressed on coarse devices');
          }
        }
      }
    } catch (e) { /* cross-origin */ }
  }
}

JS mousedown injection at click time

All four patterns above also have runtime variants. A mousedown or pointerdown listener combined with a matchMedia check can inject the relevant CSS property between the tap landing and the click dispatching. The window is approximately 5–40ms, during which:

// JS injection pattern: pointer:coarse check + inline collapse on pointerdown
document.querySelector('.consent-btn').addEventListener('pointerdown', e => {
  if (window.matchMedia('(pointer: coarse)').matches) {
    // collapse the element before click fires
    e.target.style.width = '0px';
    e.target.style.height = '0px';
    e.target.style.overflow = 'hidden';
    // click event dispatches to zero-size element — pointer misses
  }
});

// OR: swap on any-pointer:coarse (wider reach)
if (window.matchMedia('(any-pointer: coarse)').matches) {
  const btn = document.querySelector('.consent-btn');
  const div = document.createElement('div');
  div.className = btn.className;
  div.textContent = btn.textContent;
  // no event listener on the replacement div
  btn.parentNode.replaceChild(div, btn);
}
// Detection: scan scripts for pointer matchMedia + manipulation patterns
function auditPointerJS() {
  for (const script of document.querySelectorAll('script')) {
    const src = script.textContent;
    if (!src) continue;
    const hasPointerMQ = /\b(any-pointer|pointer)\b/.test(src) && /matchMedia/.test(src);
    if (!hasPointerMQ) continue;
    const hasManipulation = [
      /style\.(width|height)\s*=.*['"]0/,
      /touch-action.*none/,
      /replaceChild|createElement|removeChild/,
      /pointer-events.*none/,
      /display.*none/,
    ].some(p => p.test(src));
    if (hasManipulation) {
      console.warn('[SkillAudit] script uses pointer/any-pointer matchMedia with element manipulation:',
        script.src || '(inline)',
        '— verify consent button remains interactive on coarse devices');
    }
  }
}

Comparison: pointer vs any-pointer coverage by property

Property Scope Devices reached by :coarse Attack surface
pointer: coarse Primary device only Phones, tablets (basic) Dimension collapse, display:none, touch-action:none
any-pointer: coarse Any connected device Phones, tablets, touchscreen laptops, 2-in-1s, tablet+stylus Same as above + broader JS matchMedia checks
pointer: none Primary device only Keyboard-only, switch access, some AT configs display:none on consent section, missing fallback for no-pointer path
any-pointer: none Any device Rare — matches only when zero pointing devices present Rarely used for attack; any-pointer:none + base hide = same effect

Detection framework

A complete audit of pointer and any-pointer rules runs three passes:

Pass 1 — static CSSOM scan

Walk all @media rules in all loaded stylesheets. For each rule containing pointer or any-pointer, collect the inner style rules that match consent elements. Flag: dimensions below 44px, display: none, visibility: hidden, opacity: 0, touch-action: none, pointer-events: none.

Pass 2 — computed-style under simulated device

Use matchMedia to determine the current device's pointer characteristics. Compute BCR (bounding client rect) on consent elements on the current device. If BCR dimensions fall below 44×44px CSS pixels on a coarse device, flag as High. Also check computed touch-action on coarse devices.

Pass 3 — JS source scan

Search all inline and loaded scripts for the pattern: matchMedia with pointer or any-pointer combined with element manipulation, style injection, or DOM replacement. Log script source for manual review.

// Full three-pass audit
function fullPointerAudit() {
  const consentSelectors = [
    '[data-consent]', '.consent-btn', '.consent-section',
    '[aria-label*="consent" i]', '[aria-label*="accept" i]',
    'button[class*="consent" i]', 'button[class*="accept" i]',
  ];

  const consentEls = consentSelectors
    .flatMap(sel => Array.from(document.querySelectorAll(sel)));

  if (consentEls.length === 0) {
    console.info('[SkillAudit] pointer audit: no consent elements found via standard selectors');
    return;
  }

  // Pass 1: CSSOM
  const pointerMQPattern = /(any-pointer|pointer)\s*:/;
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (rule.type !== CSSRule.MEDIA_RULE) continue;
        const mq = rule.conditionText || rule.media.mediaText;
        if (!pointerMQPattern.test(mq)) continue;
        for (const inner of rule.cssRules) {
          if (inner.type !== CSSRule.STYLE_RULE) continue;
          for (const el of consentEls) {
            if (!el.matches(inner.selectorText)) continue;
            const s = inner.style;
            const issues = [];
            if (parseFloat(s.width) < 44) issues.push('width:' + s.width);
            if (parseFloat(s.height) < 44) issues.push('height:' + s.height);
            if (s.display === 'none') issues.push('display:none');
            if (s.visibility === 'hidden') issues.push('visibility:hidden');
            if (s.opacity === '0') issues.push('opacity:0');
            if (s.touchAction === 'none') issues.push('touch-action:none');
            if (s.pointerEvents === 'none') issues.push('pointer-events:none');
            if (issues.length) {
              console.warn('[SkillAudit][Pass1]', mq, '→', issues.join(', '),
                '| selector:', inner.selectorText, '| element:', el);
            }
          }
        }
      }
    } catch (e) { /* cross-origin */ }
  }

  // Pass 2: computed on current device
  const isCoarse = matchMedia('(pointer: coarse)').matches;
  const isAnyCoarse = matchMedia('(any-pointer: coarse)').matches;
  if (isCoarse || isAnyCoarse) {
    for (const el of consentEls) {
      const bcr = el.getBoundingClientRect();
      if (bcr.width < 44 || bcr.height < 44) {
        console.warn('[SkillAudit][Pass2] consent element below tap minimum on coarse device:',
          bcr.width.toFixed(1), '×', bcr.height.toFixed(1), 'px; element:', el);
      }
      const ta = getComputedStyle(el).touchAction;
      if (ta === 'none') {
        console.warn('[SkillAudit][Pass2] touch-action:none on consent element on coarse device:', el);
      }
    }
  }

  // Pass 3: JS source
  for (const script of document.querySelectorAll('script')) {
    const src = script.textContent;
    if (!src || !pointerMQPattern.test(src) || !/matchMedia/.test(src)) continue;
    const suspicious = [
      /style\.(width|height)\s*=.*['"]0/,
      /touch-action.*none/,
      /replaceChild|removeChild|createElement/,
      /pointer-events.*none/,
      /display.*none/,
    ].some(p => p.test(src));
    if (suspicious) {
      console.warn('[SkillAudit][Pass3] pointer matchMedia + DOM manipulation in script:',
        script.src || '(inline)');
    }
  }
}

fullPointerAudit();

Severity and findings summary

High pointer:coarse collapses consent button below 44×44px — button physically unreachable on touch devices; opacity:1 and display:block pass naive checks; detected by BCR check on coarse device or CSSOM scan for sub-44px dimension rules in pointer:coarse block.
High pointer:none hides consent section via display:none — keyboard-only users, switch-access users, and assistive-tech configurations see no consent UI; specifically targets the accessibility-dependent population; detected by CSSOM scan for display/visibility/opacity rules under pointer:none applied to consent elements.
High any-pointer:coarse dimension collapse — broader than pointer:coarse; additionally affects tablet+stylus users, touchscreen laptop users (using touchpad), and 2-in-1 devices; combined with a pointer:coarse rule can achieve near-total non-desktop-mouse coverage; detected by CSSOM scan for any-pointer:coarse blocks with sub-44px dimensions or layered pointer:coarse + any-pointer:coarse patterns.
High touch-action:none on consent button under pointer:coarse — button passes all visual checks but tap events are suppressed; tapping does nothing; detected by checking computed touch-action on coarse devices and CSSOM for touch-action:none in pointer:coarse or any-pointer:coarse blocks.
Medium JS pointer matchMedia + DOM manipulation — scripts read pointer/any-pointer at runtime and conditionally replace or collapse consent elements; can also listen for matchMedia change events; detected by source scan for matchMedia with pointer and DOM manipulation or inline style injection patterns.

SkillAudit audits all four pointer-based attack patterns — dimension collapse, pointer:none hide, any-pointer expansion, and touch-action suppression. It runs three-pass detection: static CSSOM, computed BCR on the current device, and JS source analysis. Run a free audit on your MCP server to check all pointer media query attack surfaces.

Related media query security topics

The pointer family works in conjunction with hover and display mode media queries to build layered device-targeted attacks. See also: pointer media query deep-dive, any-pointer media query deep-dive, hover media query attacks, any-hover media query attacks, scripting media query attacks, display-mode media query attacks.