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:
none— no pointing device (keyboard-only, switch access, some screen readers)coarse— low-precision pointer (finger, joystick, Wii remote)fine— high-precision pointer (mouse, touchpad, stylus)
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:
- A dimension collapse rule can be injected via inline style
- A
touch-action: nonerule can be applied inline (too late for this tap, but prevents repeat taps) - A DOM swap can replace the button with a non-interactive element
// 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
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.