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
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.
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.
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.