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