Security Guide
MCP server CSS any-hover media query security — iPad with stylus triggers any-hover:hover layout that has no touch fallback, any-hover:hover restore omits touch-only path, any-hover:none hides consent for TV remote users, JS reads any-hover to swap button
CSS @media (any-hover) checks if ANY connected input device can hover, unlike hover which checks only the primary device. any-hover: hover matches iPads with a stylus even when the user is touching the screen — the stylus enables hover capability even when not in use. This creates a gap: a hover-style consent layout under any-hover: hover may receive no touch fallback.
CSS any-hover media feature — overview
@media (any-hover) accepts hover (at least one device can hover) and none (no device can hover). A device matches any-hover: hover if any connected input — including secondary devices like an attached stylus or Bluetooth mouse — can hover. An iPad with Apple Pencil reports any-hover: hover because the Pencil hovers, but hover: none because touch (the primary device) cannot hover. This asymmetry is the primary attack surface. Related: hover, any-pointer, pointer.
Attack 1: iPad with stylus triggers any-hover:hover — touch path gets no fallback
A consent layout that uses @media (any-hover: hover) to activate a hover-optimized UI assumes the user can hover. An iPad with Apple Pencil reports any-hover: hover — so the hover layout activates — but when the user is touching the screen with their finger (not the Pencil), there is no hover capability. If the hover layout relies on :hover to reveal the consent button and there is no touch fallback, the button is unreachable via touch on that iPad.
/* Attack: any-hover:hover layout applied to iPad+Pencil, no touch fallback */
.consent-btn {
opacity: 0;
pointer-events: none;
}
@media (any-hover: hover) {
/* Activates on iPad with Pencil — device has any hover-capable input */
.consent-wrapper:hover .consent-btn {
opacity: 1;
pointer-events: auto;
/* iPad reports any-hover:hover → this block applies.
User is touching the screen → :hover never fires.
Pencil hover would work but user is not using the Pencil.
Touch users on iPad+Pencil see permanent opacity:0. */
}
}
/* No fallback for touch interaction on any-hover:hover devices */
// Detection: any-hover:hover layout without touch interaction fallback
function auditAnyHoverReveal(consentEl) {
let anyHoverHoverReveal = false;
let hasTouchFallback = false;
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 (/any-hover\s*:\s*hover/.test(mq)) {
for (const inner of rule.cssRules) {
if (inner.type !== CSSRule.STYLE_RULE) continue;
if (/:hover/.test(inner.selectorText) && consentEl.matches(inner.selectorText.replace(/:hover[^\s,]*/g, '*'))) {
if (inner.style.opacity === '1' || inner.style.display !== '') anyHoverHoverReveal = true;
}
}
}
// Check for hover:none or any explicit touch fallback
if (/hover\s*:\s*none/.test(mq) || /pointer\s*:\s*coarse/.test(mq) || /any-pointer\s*:\s*coarse/.test(mq)) {
for (const inner of rule.cssRules) {
if (inner.type !== CSSRule.STYLE_RULE) continue;
if (consentEl.matches(inner.selectorText)) hasTouchFallback = true;
}
}
}
} catch (e) { /* cross-origin */ }
}
if (anyHoverHoverReveal && !hasTouchFallback) {
console.warn('[SkillAudit] consent element revealed via :hover in any-hover:hover block with no touch fallback;',
'iPad+Pencil reports any-hover:hover but user may be touching — consent unreachable via touch;',
'add hover:none or pointer:coarse fallback;',
'element:', consentEl);
}
}
Subtle than hover:none: This attack is harder to detect than a direct hover: none hide because the consent element is shown — but only via a mechanism unavailable to the current input mode. A stylus-capable iPad in touch mode is in a degraded but commonly-used state.
Attack 2: any-hover:hover restore omits touch-only device path
Similar to the hover:hover pattern, an any-hover:hover restore reveals the button for any device with any hover-capable input — but devices that have only touch and no secondary hover device (e.g., a basic phone or iPad without a stylus) report any-hover: none. These devices get the base hidden state. The attack targets a slightly different population than hover: none — specifically devices with no hover-capable secondary input at all.
/* Attack: base hide + any-hover:hover restore, any-hover:none path unaddressed */
.consent-btn {
display: none; /* hidden by default */
}
@media (any-hover: hover) {
.consent-btn {
display: block; /* restored for any device with any hover input */
}
}
/* any-hover:none path (phone/tablet without stylus/mouse): still display:none */
// Detection: base hide + any-hover:hover restore without any-hover:none restore
function auditAnyHoverBaseHide(consentEl) {
let baseHidden = false;
let anyHoverNoneRestores = false;
const sheets = Array.from(document.styleSheets);
for (const sheet of sheets) {
try {
for (const rule of sheet.cssRules) {
if (rule.type === CSSRule.STYLE_RULE && consentEl.matches(rule.selectorText)) {
if (rule.style.display === 'none' || rule.style.opacity === '0') baseHidden = true;
}
if (rule.type === CSSRule.MEDIA_RULE) {
const mq = rule.conditionText || rule.media.mediaText;
if (/any-hover\s*:\s*none/.test(mq)) {
for (const inner of rule.cssRules) {
if (inner.type !== CSSRule.STYLE_RULE) continue;
if (consentEl.matches(inner.selectorText)) anyHoverNoneRestores = true;
}
}
}
}
} catch (e) { /* cross-origin */ }
}
if (baseHidden && !anyHoverNoneRestores) {
console.warn('[SkillAudit] consent element hidden by default with no any-hover:none restore;',
'touch-only devices (phones, basic tablets) will not see consent UI;',
'element:', consentEl);
}
}
Attack 3: any-hover:none hides consent for TV remotes and game controllers
any-hover: none matches devices where no connected input can hover — TV remotes, game controllers, and keyboard-only setups. Hiding consent under this value specifically targets users interacting with TV web browsers and gaming console browsers, which often have accessible consent requirements but no hover capability at all.
/* Attack: hide consent when no device can hover */
.consent-section {
display: block;
}
@media (any-hover: none) {
.consent-section {
display: none;
/* Targets: TV browsers (Samsung Smart TV, LG webOS, Apple TV web views),
game console browsers (PS5, Xbox, Nintendo Switch browser),
keyboard-only desktop users.
These devices often rely on directional navigation + confirm button.
They have no pointer at all, let alone a hover-capable one. */
}
}
// Detection: any-hover:none hide on consent elements
function auditAnyHoverNoneHide(consentEl) {
const isAnyHoverNone = window.matchMedia('(any-hover: none)').matches;
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 (!/any-hover\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 o = inner.style.opacity;
if (d === 'none' || o === '0') {
console.warn('[SkillAudit] any-hover:none hides consent element:',
d ? 'display:' + d : '', o ? 'opacity:' + o : '',
'| targets TV remotes, game controllers, keyboard-only;',
'| selector:', inner.selectorText);
}
}
}
} catch (e) { /* cross-origin */ }
}
}
Attack 4: JS reads any-hover to swap consent button
JavaScript checking window.matchMedia('(any-hover: none)').matches identifies devices without any hover-capable input. Scripts that detect this condition and replace the consent button with a non-interactive element disable consent for TV and game controller browsers. Can also listen for matchMedia change events if the user connects or disconnects a pointing device.
// Attack: JS any-hover:none check + non-interactive swap
if (window.matchMedia('(any-hover: none)').matches) {
const btn = document.querySelector('.consent-btn');
if (btn) {
const fake = document.createElement('span');
fake.textContent = btn.textContent;
fake.style.cssText = btn.getAttribute('style') || '';
// No click or keydown handler — directional navigation reaches it but
// activating it does nothing
btn.parentNode.replaceChild(fake, btn);
}
}
// Change listener: if user plugs in a mouse, re-run check
window.matchMedia('(any-hover: none)').addEventListener('change', e => {
if (e.matches) { /* re-run swap if device loses hover capability */ }
});
// Detection: JS any-hover matchMedia + element manipulation
function auditAnyHoverJS() {
for (const script of document.querySelectorAll('script')) {
const src = script.textContent;
if (!src || !/matchMedia/.test(src) || !/any-hover/.test(src)) continue;
const hasManipulation = [
/replaceChild|createElement|removeChild/,
/pointer-events.*none/,
/display.*none/,
/style\.(opacity|display|visibility)\s*=/,
].some(p => p.test(src));
if (hasManipulation) {
console.warn('[SkillAudit] script uses any-hover matchMedia with element manipulation;',
'verify consent button remains interactive on any-hover:none devices (TV, gamepad, keyboard);',
'script:', script.src || '(inline)');
}
}
}
Findings summary
SkillAudit distinguishes hover from any-hover media query rules and checks for the iPad+Pencil mismatch where any-hover:hover is active but the user is touching. It audits base-hide patterns, :hover-only reveals, and JS matchMedia manipulation. Run a free audit on your MCP server.