Security reference · CSS injection · touch-action · Consent manipulation

MCP server CSS touch-action consent security — mobile swipe suppression consent bypass

CSS touch-action tells the browser which touch gestures to handle natively and which to pass to JavaScript. On mobile, it is a critical gatekeeper for scroll and zoom behavior. Applied to a consent text container, touch-action: none disables vertical scrolling, horizontal scrolling, and pinch-zoom simultaneously — the user cannot read consent text that extends beyond the visible container height. The install button's click event still fires normally because touch-action affects browser-native touch behaviors but not JavaScript pointer events.

touch-action attack surface overview

Attack ID CSS value Gestures suppressed User confusion vector
SA-CSS-TA-001 touch-action: none pan-x, pan-y, pinch-zoom, double-tap-zoom, rotate Consent container has overflow-y: auto and 300 words; user cannot scroll to read remainder on touch device; install button click still fires
SA-CSS-TA-002 touch-action: pan-x pan-y (vertical scroll), pinch-zoom, double-tap-zoom Vertical swipe to scroll through consent does nothing; horizontal swipe triggers carousel navigation away from consent
SA-CSS-TA-003 touch-action: manipulation double-tap-zoom only Consent text is 10px (too small to read); pinch-zoom is technically allowed but overflow:hidden parent clips zoomed content; double-tap-to-zoom is disabled

Why touch-action attacks evade standard consent audits: touch-action has no effect on the consent element's visual rendering or DOM text content. Audits checking color, font-size, opacity, visibility, display, or clip-path find nothing suspicious — all values are correct. overflow-y: auto on the consent container is also correct: it tells the browser scrolling is possible. The attack is that touch-action disables the touch gesture that would trigger that scroll on a mobile device. The overflow setting is a lie: scrolling is not practically possible for touch users.

Background: how touch-action works

The touch-action CSS property is part of the Pointer Events specification. It was introduced to solve a performance problem: browsers had to wait 300ms after a touch event to determine whether the user intended a scroll or a double-tap-to-zoom before firing click events. By declaring touch-action on an element, a page tells the browser which gestures are handled by the page's JavaScript (and should not trigger browser-native behaviors) and which gestures are purely for the browser to handle natively.

The key values are: auto (default — browser handles all touch gestures natively), none (browser handles no touch gestures — all are passed to JS pointer events), pan-x (browser handles only horizontal panning), pan-y (browser handles only vertical panning), manipulation (browser handles pan and pinch-zoom but not double-tap-to-zoom), and combinations like pan-x pan-y. The property is inherited by children and can be set on any element, not just scroll containers.

Attack 1: touch-action: none disables all touch gestures on consent container (SA-CSS-TA-001)

The consent container has overflow-y: auto and contains approximately 300 words — far more than can fit in the visible container height of 80px at font-size 14px. On a desktop browser, the mouse wheel scrolls the container normally. On a touch device, the user's vertical swipe gesture is suppressed by touch-action: none. The browser receives the touchstart, touchmove, and touchend events but does not perform its native pan behavior. No JavaScript handler processes the pan either (the page relies on touch-action:none to suppress the native scroll, with no fallback handler). The user attempts to scroll through the consent, sees no scroll response, and — especially on mobile — may believe the visible text is the full content. The install button at the bottom of the page has its own tap handler and fires normally: touch-action: none does not affect pointer events or click events.

/* Attack: touch-action:none disables scroll/zoom on consent container on touch devices */
.consent-container {
  overflow-y: auto;        /* tells browser scrolling is possible — correct at audit */
  max-height: 80px;        /* visible area shows ~4 lines of 14px text */
  touch-action: none;      /* ← suppresses ALL browser-native touch behaviors */
  /* touch-action:none means:
     - touchstart fires
     - touchmove fires (JS receives the events)
     - BUT the browser does NOT perform native pan/scroll
     - AND the browser does NOT perform pinch-zoom
     - AND the browser does NOT perform double-tap-zoom
     No JS handler compensates — consent simply does not scroll on touch */
}

/* Consent text inside the container */
.consent-text {
  font-size: 14px;
  line-height: 1.6;
  /* ~300 words × ~5 chars avg × (14px / 6 chars per line) = ~350px total height */
  /* Container is 80px — user sees the first 4 lines (~40 words) */
  /* Remaining 260 words are inaccessible on touch */
}

/* Install button — outside the consent container, not affected by touch-action */
.install-button {
  touch-action: manipulation; /* or auto — click fires normally */
  /* tap event fires, click event fires, install proceeds */
}

/* What an element-level audit sees on .consent-text:
   color: #111            → legible ✓
   font-size: 14px        → readable ✓
   visibility: visible    → visible ✓
   display: block         → normal ✓
   opacity: 1             → opaque ✓
   overflow-y: auto       → scrollable ✓  ← this is the misleading signal
   touch-action: none     → NOT checked by most consent audits */

/* The overflow-y:auto claim is technically correct but behaviorally false
   for touch users — scroll is possible only via mouse/keyboard, not touch. */

SA-CSS-TA-001 (High). Detection: check getComputedStyle(el).touchAction on consent containers. A value of none on a container with overflow-y: auto or scroll and content exceeding its visible height is a consent scroll-suppression attack. Cross-reference with user agent to determine whether the user is on a touch device — the attack is only effective on mobile/tablet; desktop users with a mouse wheel are not affected. Flag as HIGH for mobile UA, MEDIUM for desktop-only context.

/* Detection: touch-action:none on consent scroll containers */
function detectTouchActionNone() {
  const CONSENT_KEYWORDS = ['authorize', 'grant', 'access', 'permission', 'agree',
                            'terms', 'third-party', 'transmit', 'install'];
  const findings = [];

  document.querySelectorAll('*').forEach(el => {
    const text = el.textContent.toLowerCase();
    if (!CONSENT_KEYWORDS.some(k => text.includes(k))) return;
    if (el.textContent.trim().length < 80) return; // skip short labels

    const cs = getComputedStyle(el);
    const touchAction = cs.touchAction;
    const overflowY = cs.overflowY;

    if (touchAction === 'none') {
      // Check if the container clips content
      const isScrollContainer = overflowY === 'auto' || overflowY === 'scroll'
                                 || overflowY === 'hidden';
      const hasHiddenContent = el.scrollHeight > el.clientHeight + 5; // +5px tolerance

      if (isScrollContainer && hasHiddenContent) {
        findings.push({
          vuln: 'SA-CSS-TA-001',
          severity: 'HIGH',
          element: el,
          detail: `touchAction:none on consent container with overflow-y:${overflowY}; ` +
                  `${el.scrollHeight - el.clientHeight}px of consent content hidden; ` +
                  `touch scrolling completely suppressed`
        });
      } else if (isScrollContainer) {
        // Overflow container with touch-action:none but content currently fits —
        // flag as MEDIUM (could be exploited with dynamic content injection)
        findings.push({
          vuln: 'SA-CSS-TA-001-potential',
          severity: 'MEDIUM',
          element: el,
          detail: `touchAction:none on overflow consent container — currently fits, ` +
                  `but scroll is suppressed for any future content overflow`
        });
      }
    }
  });

  return findings;
}

// Check all ancestors of consent elements for inherited touch-action:none
function checkInheritedTouchAction(consentEl) {
  let el = consentEl.parentElement;
  while (el && el !== document.body) {
    const ta = getComputedStyle(el).touchAction;
    if (ta === 'none') {
      return {
        vuln: 'SA-CSS-TA-001-inherited',
        detail: `touchAction:none inherited from ancestor ${el.tagName}.${el.className}`,
        ancestor: el
      };
    }
    el = el.parentElement;
  }
  return null;
}

Attack 2: touch-action: pan-x allows horizontal swipe but blocks vertical consent scrolling (SA-CSS-TA-002)

touch-action: pan-x tells the browser to handle horizontal panning natively (for carousels, horizontal scroll containers, etc.) while suppressing vertical panning (pan-y). Applied to a consent container that is vertically scrollable, it allows horizontal swipe gestures but prevents vertical scrolling. The user's attempt to scroll down through the consent text with a vertical swipe does nothing — the browser receives the gesture as a potentially horizontal swipe and routes it to the pan-x handler, but since the gesture is vertical, it does nothing. The interaction is doubly deceptive: if the install flow has a horizontal swipe navigation mechanism (step 1 → step 2 → step 3), the pan-x permission allows that navigation to work normally. A horizontal swipe on the consent text (intended to signal frustration or dismissal) actually advances the install flow to the next step, leaving the consent unread.

/* Attack: touch-action:pan-x blocks vertical consent scrolling;
   horizontal swipe advances the install carousel */

/* Multi-step install carousel structure */
.install-carousel {
  overflow-x: hidden;  /* carousel wrapper */
  display: flex;
}

.install-step {
  flex: 0 0 100%;      /* each step is full width */
  touch-action: pan-x; /* ← browser handles horizontal pan for carousel */
                       /* ← browser DOES NOT handle vertical pan (pan-y) */
                       /* vertical swipe to scroll consent does nothing */
}

/* Step 2 of the install carousel: consent step */
.install-step.step-consent {
  touch-action: pan-x; /* inherited from .install-step */
  overflow-y: auto;    /* tells auditor scroll is possible */
  /* The consent text inside is ~300 words; container is 200px tall */
  /* Vertical scroll is blocked by touch-action:pan-x */
  /* Horizontal swipe from this step → JS carousel advances to step 3 (install button) */
}

/* JS carousel handler */
let startX = 0;
document.querySelector('.install-carousel').addEventListener('touchstart', e => {
  startX = e.touches[0].clientX;
});
document.querySelector('.install-carousel').addEventListener('touchend', e => {
  const deltaX = startX - e.changedTouches[0].clientX;
  if (Math.abs(deltaX) > 50) {
    // Horizontal swipe detected — advance the carousel
    // Note: browser panned horizontally (pan-x allowed)
    // User may have intended to swipe to dismiss consent,
    // or made an accidental horizontal swipe while trying to scroll vertically
    advanceCarouselStep();
  }
});

/* The consent container */
.consent-body {
  /* Normal styles */
  font-size: 14px;
  line-height: 1.6;
  color: #1a1a1a;
  /* Vertical scroll: BLOCKED by touch-action:pan-x on parent */
  /* Horizontal swipe: ENABLED — inadvertently advances the install flow */
  /* User attempting vertical scroll makes a slight horizontal deviation
     on a touch device — this can trigger the carousel advance */
}

/* touch-action:pan-x also suppresses pinch-zoom on the consent text.
   A user who cannot scroll the text might try pinching to zoom in
   to read the text at smaller size (common behavior when overflow is suspected).
   pan-x suppresses pinch-zoom as well. */

/* Behavior matrix:
   Gesture        | Browser handles? | Result for user
   ---------------|-----------------|----------------------------------
   Vertical swipe | NO (pan-y off)  | Nothing — cannot scroll consent
   Horizontal swipe | YES (pan-x on) | Carousel advances to install button
   Pinch zoom     | NO              | Cannot zoom to read small text
   Double-tap     | NO              | Cannot double-tap zoom
   Tap (click)    | YES             | JS click fires — install button works */

The carousel double-trap: When the install flow uses a horizontal carousel (step 1: intro, step 2: consent, step 3: install), touch-action: pan-x on the steps serves a legitimate purpose for horizontal carousel navigation. This makes the attack harder to detect as malicious — the pan-x setting looks intentional for the carousel. The consent bypass is a side effect of the pan-x configuration, and a user attempting to scroll the consent vertically may accidentally swipe horizontally enough to advance to the install step.

/* Detection: touch-action:pan-x blocking vertical scroll on consent containers */
function detectTouchActionPanX() {
  const CONSENT_KEYWORDS = ['authorize', 'grant', 'permission', 'agree', 'terms'];
  const findings = [];

  document.querySelectorAll('*').forEach(el => {
    const text = el.textContent.toLowerCase();
    if (!CONSENT_KEYWORDS.some(k => text.includes(k))) return;
    if (el.textContent.trim().length < 80) return;

    const cs = getComputedStyle(el);
    const touchAction = cs.touchAction;
    const overflowY = cs.overflowY;

    // pan-x alone blocks vertical scroll (pan-y) and pinch-zoom
    // pan-x pan-y would allow both — this is not an attack
    if (touchAction === 'pan-x') {
      const hasHiddenContent = el.scrollHeight > el.clientHeight + 5;
      if (overflowY === 'auto' || overflowY === 'scroll') {
        findings.push({
          vuln: 'SA-CSS-TA-002',
          severity: 'HIGH',
          element: el,
          detail: `touchAction:pan-x on vertically-scrollable consent container; ` +
                  `pan-y (vertical scroll) is blocked; ` +
                  `${el.scrollHeight - el.clientHeight}px of consent text unreachable on touch; ` +
                  `horizontal swipe may trigger navigation`
        });
      }
    }

    // Check parent elements for inherited pan-x
    let ancestor = el.parentElement;
    while (ancestor && ancestor !== document.body) {
      const ancestorTA = getComputedStyle(ancestor).touchAction;
      if (ancestorTA === 'pan-x') {
        const hasHiddenContent = el.scrollHeight > el.clientHeight + 5;
        if (hasHiddenContent) {
          findings.push({
            vuln: 'SA-CSS-TA-002-inherited',
            severity: 'HIGH',
            consentElement: el,
            ancestorElement: ancestor,
            detail: `touchAction:pan-x on ancestor ${ancestor.tagName} blocks vertical ` +
                    `scroll on consent container; ${el.scrollHeight - el.clientHeight}px hidden`
          });
        }
        break;
      }
      ancestor = ancestor.parentElement;
    }
  });

  return findings;
}

// Detect horizontal-swipe handlers on install carousel elements
// that sit alongside consent elements — carousel advance triggered by swipe
function detectCarouselConsentBypass() {
  // Check for touchend/touchstart handlers on consent step parents
  // This requires runtime event listener inspection — SkillAudit instruments
  // addEventListener before script execution to capture all handlers.
  // Pattern: touchend handler with deltaX check on an element that is a
  // sibling or ancestor of the consent element.
  const consentEls = document.querySelectorAll('[data-consent], .consent, .terms-text');
  consentEls.forEach(el => {
    const parent = el.closest('[data-carousel], .carousel, .install-steps, .step-container');
    if (parent) {
      // Flag: consent is inside a touch carousel that could advance on horizontal swipe
      console.warn('[SkillAudit] SA-CSS-TA-002-carousel: consent element inside touch carousel; ' +
                   'horizontal swipe on consent may advance install flow without reading consent');
    }
  });
}

Attack 3: touch-action: manipulation disables double-tap-to-zoom on small consent text (SA-CSS-TA-003)

touch-action: manipulation allows pan and pinch-zoom but disables double-tap-to-zoom — the most common way mobile users zoom in to read text that is too small for comfortable reading. The attack pairs this with a consent container sized to display exactly one line of consent text at a small font size (font-size: 10px, below most comfortable reading thresholds on mobile), with overflow: hidden on the parent container. When the user double-taps the consent text to zoom in, the browser does not respond — double-tap-to-zoom is suppressed by manipulation. Pinch-zoom is allowed by manipulation, but the overflow: hidden parent clips the zoomed content to the original container boundaries. The consent is shown at an unreadable size, zoom is effectively blocked, and the install button tap fires normally.

/* Attack: touch-action:manipulation + small font + overflow:hidden clips zoomed content */

/* The consent "container" structure */
.consent-wrapper {
  overflow: hidden;    /* clips children — including zoomed content */
  height: 20px;        /* exactly one line at 10px font */
  width: 100%;
}

.consent-single-line {
  font-size: 10px;     /* below comfortable reading threshold for most mobile users */
  line-height: 20px;
  white-space: nowrap; /* single line, truncated with overflow:hidden on parent */
  touch-action: manipulation; /* ← allows pinch-zoom but disables double-tap-zoom */
  /* Full text: "By tapping Install you authorize full file system and network access." */
  /* Visible: "By tapping Install you authorize full file system a…" (truncated) */
}

/* What happens when the user tries to zoom in to read the consent: */

/* Option 1: Double-tap to zoom */
/* Standard mobile behavior: double-tap zooms the browser viewport onto the tapped element.
   touch-action:manipulation disables this.
   User double-taps consent text → nothing happens.
   User assumes the text is intentionally small decorative legalese. */

/* Option 2: Pinch-zoom */
/* touch-action:manipulation ALLOWS pinch-zoom.
   User pinches out on the consent text → viewport scales up.
   BUT: the browser viewport zoom scales the page, not just the element.
   After viewport zoom, the text is larger... but scrolling/position may shift.
   More critically: if the parent .consent-wrapper has overflow:hidden,
   the zoomed text may be clipped at the parent boundary in some browsers.
   On iOS Safari, pinch-zoom is a viewport-level operation that can reveal
   content hidden by overflow:hidden — but on Android Chrome, element-level
   overflow:hidden clips even zoomed content in some layout configurations. */

/* Option 3: Attempt to select/copy text to read it */
/* user-select:none on the consent text (often paired with touch-action:manipulation)
   prevents long-press text selection on mobile.
   The user cannot copy the consent text to paste it elsewhere to read it. */

.consent-single-line {
  font-size: 10px;
  touch-action: manipulation;
  user-select: none;   /* bonus: prevents text selection/copy on mobile */
  -webkit-user-select: none;
}

/* The install button — functions correctly */
.install-button {
  font-size: 16px;     /* legible */
  touch-action: manipulation; /* allows tap — consistent use of manipulation value */
  /* tap → JS click handler → install proceeds */
}

/* The manipulation value is commonly recommended for buttons and interactive elements
   because it eliminates the 300ms click delay. Its presence on the consent text
   therefore looks like a performance optimization rather than an attack. */

SA-CSS-TA-003 (High). touch-action: manipulation is the most commonly-recommended touch-action value for interactive elements — it eliminates the 300ms tap delay on mobile. Its use on the consent container looks like a performance best practice. Detection requires checking font-size concurrently: if touch-action: manipulation appears on an element with font-size below 11px (or below 16px on mobile where the browser would normally auto-zoom), flag as SA-CSS-TA-003. The double-tap suppression combined with sub-threshold font size is the attack combination.

/* Detection: touch-action:manipulation + small font + overflow:hidden on consent */
function detectTouchActionManipulation() {
  const CONSENT_KEYWORDS = ['authorize', 'grant', 'access', 'permission',
                            'agree', 'terms', 'install'];
  const MOBILE_FONT_THRESHOLD_PX = 11; // double-tap zoom is commonly triggered below 11px
  const ACCESSIBLE_FONT_THRESHOLD_PX = 16; // WCAG AA minimum for body text on mobile

  const findings = [];

  document.querySelectorAll('*').forEach(el => {
    const text = el.textContent.toLowerCase();
    if (!CONSENT_KEYWORDS.some(k => text.includes(k))) return;
    if (el.textContent.trim().length < 10) return;

    const cs = getComputedStyle(el);
    const touchAction = cs.touchAction;
    const fontSize = parseFloat(cs.fontSize); // in px
    const overflowParent = findOverflowHiddenAncestor(el);

    if (touchAction === 'manipulation') {
      const isTooSmall = fontSize < MOBILE_FONT_THRESHOLD_PX;
      const isSmallForMobile = fontSize < ACCESSIBLE_FONT_THRESHOLD_PX;

      if (isTooSmall && overflowParent) {
        findings.push({
          vuln: 'SA-CSS-TA-003',
          severity: 'HIGH',
          element: el,
          detail: `touchAction:manipulation disables double-tap-zoom; ` +
                  `fontSize:${fontSize}px (below ${MOBILE_FONT_THRESHOLD_PX}px threshold); ` +
                  `overflow:hidden parent clips pinch-zoom; consent unreadable on mobile`
        });
      } else if (isTooSmall) {
        findings.push({
          vuln: 'SA-CSS-TA-003-partial',
          severity: 'HIGH',
          element: el,
          detail: `touchAction:manipulation disables double-tap-zoom on ` +
                  `${fontSize}px consent text — too small to read without zoom on mobile`
        });
      } else if (isSmallForMobile && overflowParent) {
        findings.push({
          vuln: 'SA-CSS-TA-003-medium',
          severity: 'MEDIUM',
          element: el,
          detail: `touchAction:manipulation on ${fontSize}px consent text with ` +
                  `overflow:hidden parent — double-tap-zoom suppressed, pinch-zoom clipped`
        });
      }
    }
  });

  return findings;
}

function findOverflowHiddenAncestor(el) {
  let parent = el.parentElement;
  while (parent && parent !== document.body) {
    const cs = getComputedStyle(parent);
    if (cs.overflow === 'hidden' || cs.overflowX === 'hidden' || cs.overflowY === 'hidden') {
      return parent;
    }
    parent = parent.parentElement;
  }
  return null;
}

// Unified touch-action consent scan
function runTouchActionConsentScan() {
  const results = [
    ...detectTouchActionNone(),
    ...detectTouchActionPanX(),
    ...detectTouchActionManipulation()
  ];
  return results.filter(Boolean);
}

SkillAudit detection

HIGH SA-CSS-TA-001: touch-action: none on a consent container with overflow-y: auto and content exceeding the visible height disables all touch gestures — users cannot scroll to read consent text on mobile devices. SkillAudit checks getComputedStyle(el).touchAction on consent-bearing elements and flags none combined with hidden scroll content.
HIGH SA-CSS-TA-002: touch-action: pan-x on a consent container blocks vertical scroll (pan-y) while allowing horizontal carousel swipe — users cannot scroll down through consent text on mobile, and a horizontal swipe may advance the install flow to the install button. SkillAudit detects pan-x on vertically-scrollable consent containers and flags the carousel-advance interaction.
HIGH SA-CSS-TA-003: touch-action: manipulation disables double-tap-to-zoom on consent text below 11px font size, while an overflow: hidden parent clips pinch-zoom, rendering consent text unreadable on mobile without any effective zoom path. SkillAudit combines font-size measurement, touch-action value, and ancestor overflow inspection to detect this pattern.

Run SkillAudit to detect SA-CSS-TA patterns in any MCP server before install. SkillAudit evaluates touch-action values on consent containers in combination with overflow settings, font-size, and scroll content height — catching mobile-specific consent suppression attacks that desktop-only CSS audits miss entirely.