← Blog

MCP server CSS container query and media query consent security

CSS @container and @media queries allow rules to be conditioned on container dimensions or device/user-preference capabilities. Malicious consent UIs exploit these conditional mechanisms in two distinct ways: constructing always-true conditions that appear conditional to casual inspection but always evaluate to true in real browser environments; and targeting specific device capability classes (pointer type, hover support, motion preference) to hide consent from the majority of users while appearing functional in testing environments with different capabilities. Four SA-CSS-CNQ patterns cover the primary query-based consent attack vectors.

By SkillAudit  |  30 Oct 2026

A fundamental limitation of CSS rule inspection is that it reveals the rule structure without evaluating the condition. An auditor that parses stylesheets may find @media (pointer: fine) { .consent-text { color: transparent } } and correctly identify it as a conditional rule — but on the device where the audit runs (a desktop with a mouse), the condition evaluates to true and the rule is applied. getComputedStyle will show color: rgba(0,0,0,0) (transparent). The auditor may correctly detect the erasure but fail to identify that it is device-targeted and affects desktop users specifically.

SA-CSS-CNQ-001: @container (min-width:1px) — always-true container query

CSS Container Queries (Level 3) allow rules to be conditioned on the dimensions of a named container ancestor. @container (min-width: 1px) evaluates to true for any container with a computed width of at least 1px — which in practice means every container element in a visible document. An auditor that identifies this rule as “conditional” and defers evaluation will miss that the condition always applies. The attack's value is its plausible deniability: the rule appears to be a responsive layout rule, not a static erasure rule.

/* Attack: always-true container query */
.consent-wrapper {
  container-type: inline-size;  /* designate as query container */
  container-name: consent;
}

@container consent (min-width: 1px) {
  /* This applies when .consent-wrapper width >= 1px */
  /* In practice: ALWAYS, since any visible container is >= 1px wide */
  .consent-text {
    opacity: 0;  /* always applied — not truly conditional */
  }
}

/* Why an auditor might miss this:
   1. Auditor identifies @container rule → marks as "conditional"
   2. Auditor doesn't evaluate the container size
   3. Auditor reports: "consent text in @container rule — check manually"
   4. The 'min-width: 1px' condition appears reasonable (normal responsive pattern)

   Correct detection:
   getComputedStyle(consentTextEl).opacity → '0' (the rule is applied!)
   → auditor should check computed styles regardless of @container presence
   → If opacity:0 is the result, the @container condition is de-facto always true

   Or: source analysis of @container conditions
   min-width: 1px, min-width: 0px, min-height: 1px → flag as likely always-true → HIGH */

Container queries make static rule analysis insufficient: Traditional CSS auditors scan for rules that contain erasure properties (opacity: 0, color: transparent). Container query rules wrap these properties in a conditional that requires container-size evaluation. The correct defense is computed-style inspection (what is the resolved property value?) rather than rule scanning (is there a rule for this property?).

CRITICAL
SA-CSS-CNQ-001 — @container (min-width:1px) erasure rule: always-true condition applied to all containers; appears conditional but is static. Detection: getComputedStyle reports opacity:0 regardless of @container wrapping → CRITICAL (computed style detection, not rule scanning).

SA-CSS-CNQ-002: @media (pointer: fine) — desktop mouse user targeting

@media (pointer: fine) evaluates to true on devices with a fine-grained pointing device: desktop mice, touchpads on laptops. It evaluates to false on touch-primary mobile devices (pointer: coarse). A consent erasure rule inside @media (pointer: fine) targets desktop and laptop users exclusively. Mobile users (who are more likely to be tested on QA mobile devices) see the consent text correctly. Desktop users — who are the primary audience of MCP-enabled Claude desktop applications — see the consent text erased.

/* Targeting desktop mouse users: */
@media (pointer: fine) {
  .consent-text {
    color: transparent;   /* applied only on fine-pointer (mouse/trackpad) devices */
    /* Mobile (touch): unaffected → consent text visible on mobile */
    /* Desktop (mouse): affected → consent text erased */
  }
}

/* Device targeting properties available:
   pointer: fine     → desktop mouse/trackpad (majority of Claude desktop users)
   pointer: coarse   → mobile touch
   hover: hover      → devices supporting hover (desktop)
   hover: none       → touch-only devices
   any-pointer: fine → any fine pointer present (multi-input devices)
   any-hover: hover  → any hover-capable pointer present

   Why this is high severity for MCP servers:
   MCP servers are primarily used via Claude desktop apps on macOS/Windows/Linux.
   These are ALL pointer:fine, hover:hover environments.
   → The attack targets 100% of the primary user population.

   Detection:
   - Run audit on pointer:fine emulated device (Playwright: isMobile:false, hasTouch:false)
   - getComputedStyle(consentTextEl).color → rgba(0,0,0,0) → CRITICAL
   - Also check: does the computed style change under pointer:coarse emulation?
     If yes: device-targeted consent erasure */

Claude desktop users are pointer:fine: The primary environment where MCP server consent dialogs appear is Claude Desktop on macOS, Windows, and Linux. These environments all report pointer: fine and hover: hover to CSS media queries. A consent erasure rule inside @media (pointer: fine) affects every Claude desktop user, which is the complete target user population of MCP tools.

CRITICAL
SA-CSS-CNQ-002 — @media (pointer:fine) erasure targeting desktop mouse users: affects 100% of Claude desktop app users; passes on mobile test environments. Detection: audit on pointer:fine device + getComputedStyle check → CRITICAL.

SA-CSS-CNQ-003: @media (hover: hover) — hoverable device targeting

@media (hover: hover) evaluates to true on devices where the primary pointing device supports hover (desktop mice, trackpads). Combined with @media (pointer: fine), the compound query @media (pointer: fine) and (hover: hover) targets desktop mouse users with sub-pixel precision. A consent erasure rule inside this compound query is device-class-selective: it applies to desktop users and not to mobile users, not to touchscreen laptops in tablet mode, and not to voice-only interfaces.

/* Precise desktop targeting: */
@media (pointer: fine) and (hover: hover) {
  .consent-text {
    visibility: hidden;
    /* Targets: desktop computers with mouse/trackpad
       Excludes: mobile, tablet, touch-first laptop modes */
  }
}

/* Standalone hover:hover targeting: */
@media (hover: hover) {
  .consent-text {
    display: none;
  }
}

/* Detection methodology:
   The correct audit approach tests the consent UI under emulated device conditions:
   1. pointer:fine + hover:hover (desktop) → should see consent text
   2. pointer:coarse + hover:none (mobile) → should see consent text
   If text is visible in (2) but not (1): device-targeted attack → CRITICAL

   Using Playwright:
   // Desktop emulation
   const context = await browser.newContext({ isMobile: false, hasTouch: false });
   // Check getComputedStyle for consent text visibility

   // Mobile emulation
   const mobileContext = await browser.newContext({ isMobile: true, hasTouch: true });
   // Compare computed styles — difference indicates device-targeted attack */
CRITICAL
SA-CSS-CNQ-003 — @media (hover:hover) erasure targeting hoverable desktop devices: compound with pointer:fine precisely targets desktop users. Detection: cross-device emulation comparison (desktop vs mobile computed styles) → CRITICAL.

SA-CSS-CNQ-004: @media (prefers-reduced-motion: no-preference) — majority erasure animation

@media (prefers-reduced-motion: no-preference) evaluates to true when the user has not enabled the “reduce motion” accessibility setting in their OS. This is the default state for most users: on macOS, Windows, and Linux, the “Reduce Motion” setting is off by default. A consent erasure animation inside this media query fires for the overwhelming majority of users (those who haven't specifically opted into reduced motion) while leaving the consent text visible for users who have enabled the accessibility setting. The attack exploits the accessibility preference inversion: the protective CSS convention is “hide animations for reduced-motion users”; this attack hides consent for non-reduced-motion users (the majority).

/* Attacker intent: fire erasure animation for users without reduced-motion enabled */
@media (prefers-reduced-motion: no-preference) {
  /* This applies to users who have NOT enabled Reduce Motion (the majority) */
  .consent-text {
    animation: eraseConsent 0.1s ease-out 3s forwards;
    /* After 3 second delay: consent text fades to opacity:0 */
    /* User has 3 seconds to read, then text disappears */
  }

  @keyframes eraseConsent {
    from { opacity: 1; }
    to   { opacity: 0; }
  }
}

/* Why this is inverted from the expected pattern:
   Correct accessibility pattern:
   @media (prefers-reduced-motion: reduce) { .animated { animation: none } }
   → disable animation for reduced-motion users

   Attack pattern:
   @media (prefers-reduced-motion: no-preference) { .consent { animation: eraseConsent } }
   → fire erasure animation for non-reduced-motion users (majority)

   Detection:
   Parse stylesheet for @media (prefers-reduced-motion: no-preference) rules
   that contain animation declarations on consent-text elements → HIGH
   Also: getAnimations() on consent text element after 3s timeout
   → if animation running → check final keyframe opacity/color → CRITICAL */

Inverting the accessibility convention: The CSS accessibility best practice is to put animations inside @media (prefers-reduced-motion: no-preference) to opt-in users to animation, and use the absence of animation as the baseline. Attackers exploit this convention by putting erasure animations inside the same query — which causes them to fire for the majority (non-reduced-motion) users while appearing to follow accessibility guidelines in structure.

HIGH
SA-CSS-CNQ-004 — @media (prefers-reduced-motion:no-preference) erasure animation: fires for majority of users; mimics accessibility pattern structurally. Detection: parse query for animation rules on consent elements + check final keyframe opacity → HIGH.

Summary of SA-CSS-CNQ findings

IDSeverityQuery conditionTargetsDetection
SA-CSS-CNQ-001 CRITICAL @container (min-width:1px) All containers (always true) getComputedStyle regardless of @container wrapping
SA-CSS-CNQ-002 CRITICAL @media (pointer:fine) Desktop mouse/trackpad users (100% of Claude desktop users) pointer:fine emulation + getComputedStyle check
SA-CSS-CNQ-003 CRITICAL @media (hover:hover) Hoverable desktop devices; compound targets desktop exclusively Cross-device emulation comparison (desktop vs mobile)
SA-CSS-CNQ-004 HIGH @media (prefers-reduced-motion:no-preference) Majority of users (reduced-motion off by default) Parse query for animation on consent + final keyframe check

Related pages