Security Guide
MCP server CSS any-pointer media query security — tablet with stylus triggers any-pointer:coarse collapse even when stylus is active, multi-device fine+coarse allows attacker to collapse under either value, any-pointer:coarse hides consent for secondary-coarse-device users, JS swaps button on any-coarse devices
CSS @media (any-pointer) checks if ANY connected pointing device matches the specified precision — unlike pointer which only checks the primary device. A tablet with a stylus reports any-pointer:coarse (touchscreen) AND any-pointer:fine (stylus) simultaneously. Attacks exploit this broader matching to affect users who are actively using a precise input device.
CSS any-pointer media feature — overview
any-pointer matches if ANY connected input device has the specified pointer precision. Values: none, coarse, fine. A device can match multiple values simultaneously. A modern tablet with a stylus matches any-pointer: coarse (touchscreen), any-pointer: fine (stylus), pointer: coarse (primary = touch), and any-hover: hover (stylus hovers). A laptop with a touchpad and Windows Ink stylus reports both any-pointer: coarse and any-pointer: fine. Related: pointer, hover, prefers-contrast.
Attack 1: tablet with stylus — any-pointer:coarse collapse fires even when stylus is active
A tablet (e.g., iPad with Apple Pencil, Samsung Galaxy Tab with S Pen) reports any-pointer: coarse because the touchscreen is always present as a secondary input device. A @media (any-pointer: coarse) rule that collapses the consent button applies to this tablet — even when the user is holding the stylus and interacting with fine-precision input. The consent button collapses for stylus users on any tablet with a touchscreen.
/* Attack: any-pointer:coarse collapse targets stylus users on tablets */
.consent-btn {
width: 44px;
height: 44px;
opacity: 1;
}
@media (any-pointer: coarse) {
.consent-btn {
width: 1px; /* collapse on any device with any coarse pointer */
height: 1px;
/* iPad with Apple Pencil: any-pointer:coarse matches (touchscreen present).
The collapse fires even when user is drawing/tapping with Apple Pencil.
Apple Pencil = fine pointer. But the coarse rule matches first.
User is actively using fine input but gets the coarse-collapse path.
This is different from @media(pointer:coarse) which only matches
when touch is the PRIMARY device. any-pointer is broader. */
}
}
/* Contrast: pointer:coarse vs any-pointer:coarse */
/* @media (pointer: coarse) { } — matches only when TOUCH is primary device */
/* @media (any-pointer: coarse) { } — matches when ANY device is coarse, including */
/* a laptop with Windows Ink stylus that has a touchscreen as secondary input */
// Detection: differentiate any-pointer from pointer
function auditAnyPointerVsPointer(btn) {
const primaryCoarse = window.matchMedia('(pointer: coarse)').matches;
const anyCoarse = window.matchMedia('(any-pointer: coarse)').matches;
const primaryFine = window.matchMedia('(pointer: fine)').matches;
const anyFine = window.matchMedia('(any-pointer: fine)').matches;
// If primaryFine but anyCoarse: user has fine primary + coarse secondary
// (e.g., stylus tablet, or laptop with touchscreen + touchpad)
if (primaryFine && anyCoarse) {
// Check if any-pointer:coarse rule collapses consent button
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 (/any-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');
const display = inner.style.getPropertyValue('display');
if ((w && parseFloat(w) < 10) || (h && parseFloat(h) < 10) || display === 'none') {
console.warn('[SkillAudit] any-pointer:coarse collapses consent button on device',
'with fine primary + coarse secondary pointer (tablet+stylus, laptop+touchscreen);',
'width rule:', w, '| height rule:', h, '| display:', display,
'| current device: pointer:fine + any-pointer:coarse;',
'| selector:', inner.selectorText, '| button:', btn);
}
}
}
}
}
}
} catch (e) { /* cross-origin */ }
}
}
// Current device state for reference
console.info('[SkillAudit] pointer state — pointer:coarse:', primaryCoarse,
'| pointer:fine:', primaryFine,
'| any-pointer:coarse:', anyCoarse,
'| any-pointer:fine:', anyFine);
}
Broader impact than pointer:coarse: @media (any-pointer: coarse) matches a wider set of devices than @media (pointer: coarse). Any laptop with a touchscreen, any tablet with a stylus, any 2-in-1 device — all report any-pointer: coarse because their touchscreen is always present as a secondary device, regardless of which input device is currently being used.
Attack 2: multi-device any-pointer:fine rule targeting touchpad+phone context
A device that has both a touchpad (fine) and a paired phone (which counts as a coarse secondary device) reports any-pointer: fine AND any-pointer: coarse. An attacker can write conflicting rules under both values. A @media (any-pointer: fine) rule that applies a small button size — expecting only desktop mice — will also match a phone user who happens to have Bluetooth keyboard or any fine secondary pointer registered, triggering an unexpected size collapse.
/* Attack: any-pointer:fine used as a "desktop-only" gate but matches more devices */
.consent-btn {
width: 44px; /* default: full touch size */
height: 44px;
}
@media (any-pointer: fine) {
.consent-btn {
width: 20px; /* smaller "desktop" size — but this triggers on any device with any fine pointer */
height: 20px;
/* Intended: "if user has a mouse, shrink button to desktop-appropriate size"
Actual: ANY device with ANY fine pointer gets this rule.
A phone with a paired Bluetooth mouse: any-pointer:fine matches.
Button collapses from 44px to 20px.
Touch interaction on a 20px target on a coarse primary is still inadequate.
Auditors checking "desktop" behavior see 20px and flag nothing — it looks like
a intentional desktop size reduction, not a targeted collapse. */
}
}
// Detection: check any-pointer:fine rules for consent size collapse
function auditAnyPointerFineSize(btn) {
const anyFine = window.matchMedia('(any-pointer: fine)').matches;
if (!anyFine) return;
const bcr = btn.getBoundingClientRect();
const primaryCoarse = window.matchMedia('(pointer: coarse)').matches;
// If primary device is coarse but any fine matches, check resulting size
if (primaryCoarse && (bcr.width < 44 || bcr.height < 44)) {
console.warn('[SkillAudit] any-pointer:fine: consent button too small on coarse-primary device:',
bcr.width.toFixed(1), '×', bcr.height.toFixed(1), 'px;',
'device has both coarse (primary) and fine (secondary) pointers;',
'the any-pointer:fine rule is shrinking the button below touch minimum;',
'button:', btn);
}
}
Attack 3: any-pointer:coarse hides consent via display:none
Rather than collapsing to a small size, a direct hide via display: none under @media (any-pointer: coarse) removes the consent button from the layout entirely. Any device that has any coarse pointer — which includes most modern laptops with touchscreens and all tablets — sees no consent UI.
/* Attack: any-pointer:coarse hides consent entirely */
.consent-section {
display: block; /* visible by default */
}
@media (any-pointer: coarse) {
.consent-section {
display: none; /* hidden if ANY coarse pointer present */
/* Affected devices:
- All phones (primary = touch coarse)
- All tablets (primary = touch coarse, secondary = stylus fine)
- All laptops with touchscreens (primary = touchpad fine, secondary = touch coarse)
- Any 2-in-1 device
Effectively: majority of modern devices — both consumer and enterprise.
Desktop with only mouse + keyboard: not affected (no coarse device). */
}
}
// Detection: audit any-pointer:coarse display:none rules
function auditAnyPointerCoarseHide(consentEl) {
const anyCoarse = window.matchMedia('(any-pointer: coarse)').matches;
if (anyCoarse) {
const cs = getComputedStyle(consentEl);
if (cs.getPropertyValue('display') === 'none') {
// Check if this comes from an any-pointer:coarse rule
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 (/any-pointer\s*:\s*coarse/.test(mediaText)) {
for (const inner of rule.cssRules) {
if (inner.type === CSSRule.STYLE_RULE && consentEl.matches(inner.selectorText)) {
if (inner.style.getPropertyValue('display') === 'none') {
console.warn('[SkillAudit] any-pointer:coarse hides consent element via display:none;',
'affects all devices with any coarse pointer (phones, tablets, touchscreen laptops);',
'selector:', inner.selectorText, '| element:', consentEl);
}
}
}
}
}
}
} catch (e) { /* cross-origin */ }
}
}
} else {
// On non-coarse device: can still audit CSSOM for the rule
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 (/any-pointer\s*:\s*coarse/.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');
if (display === 'none' || opacity === '0') {
console.warn('[SkillAudit] any-pointer:coarse rule hides consent (not currently active on this device):',
display === 'none' ? 'display:none' : 'opacity:0',
'| will affect touch devices, tablets, touchscreen laptops;',
'| selector:', inner.selectorText, '| element:', consentEl);
}
}
}
}
}
}
} catch (e) { /* cross-origin */ }
}
}
}
Attack 4: JS reads any-pointer:coarse to swap consent button
JavaScript can check window.matchMedia('(any-pointer: coarse)').matches to detect any device with a coarse pointer. On a match, the consent button can be replaced with a non-interactive element or a visually similar element that omits the click handler. This affects a much broader device population than a pointer: coarse check would — including stylus tablet users and laptop+touchscreen users.
/* JS attack: any-pointer:coarse swap — broader reach than pointer:coarse */
if (window.matchMedia('(any-pointer: coarse)').matches) {
const btn = document.querySelector('.consent-btn');
if (btn) {
// Replace with visually identical but non-interactive element
const fake = document.createElement('div');
fake.setAttribute('class', btn.getAttribute('class'));
fake.setAttribute('style', btn.getAttribute('style') || '');
fake.textContent = btn.textContent;
// No event listener — div is non-interactive
btn.parentNode.replaceChild(fake, btn);
/* any-pointer:coarse matches more devices than pointer:coarse:
- All phones: affected (same as pointer:coarse)
- All tablets (even with stylus): affected (touchscreen counts as coarse)
- Touchscreen laptops using mouse/touchpad: affected (touchscreen = secondary coarse)
For comparison: pointer:coarse only matches when touch is the PRIMARY device.
any-pointer:coarse reaches touchscreen laptop users who are using a mouse. */
}
}
// Can also listen for changes (if user plugs in mouse to phone via USB-C)
window.matchMedia('(any-pointer: coarse)').addEventListener('change', (e) => {
if (e.matches) {
// Trigger the swap when any coarse device connects
document.querySelector('.consent-btn')?.style.setProperty('pointer-events', 'none');
}
});
// Detection: JS source analysis for any-pointer matchMedia
function auditAnyPointerJS() {
const scripts = document.querySelectorAll('script');
for (const script of scripts) {
if (!script.textContent) continue;
if (/any-pointer/.test(script.textContent)) {
const hidePatterns = [
/display\s*:\s*['"]?none/,
/visibility\s*:\s*['"]?hidden/,
/opacity\s*:\s*['"]?0/,
/pointer-events\s*:\s*['"]?none/,
/replaceChild|createElement|removeChild/,
];
const hasHide = hidePatterns.some(p => p.test(script.textContent));
if (hasHide) {
console.warn('[SkillAudit] script uses any-pointer matchMedia combined with element hiding or DOM manipulation;',
'any-pointer:coarse affects broader device population than pointer:coarse;',
'verify consent button remains interactive on tablets and touchscreen laptops;',
'script element:', script.src || '(inline)');
}
}
}
}
auditAnyPointerJS();
Findings summary
SkillAudit distinguishes between pointer and any-pointer media query rules in consent CSS, checking for collapses that affect multi-input-device setups (tablet+stylus, touchscreen laptop+mouse). It audits both current device state and CSSOM rules for non-active device configurations. Run a free audit on your MCP server.