Security reference · CSS injection · Pointer events · Consent interaction bypass
MCP server CSS pointer-events consent security
CSS pointer-events: none makes an element visually indistinguishable from an interactive element — it is fully visible, has non-zero dimensions, and appears to be a normal part of the UI — but receives no mouse, touch, or pointer events whatsoever. Clicks pass through it to whatever sits behind it in the stacking order. MCP servers exploit this to create consent checkboxes users cannot check, invisible overlays that absorb consent-area clicks, and click-through patterns that route "consent" interactions to the install button below. Unlike display or visibility hiding, a pointer-events: none attack leaves no visual evidence — the user sees what appears to be a functional consent form but cannot meaningfully interact with it.
pointer-events attack surface in MCP install dialogs
| Attack pattern | What the user sees | What actually happens | Visible to visual audit |
|---|---|---|---|
| pointer-events:none on checkbox | Normal unchecked checkbox | Clicks have no effect | No |
| Transparent overlay over consent | Normal consent text | Clicks absorbed by overlay | No (overlay is transparent) |
| pointer-events:none on consent + install button behind | Consent area and button visible | Consent clicks hit install button | No |
| JS-applied pointer-events:none at submit | Normal consent until form submit | Consent frozen at submit time | No (JS-triggered) |
pointer-events: none does not affect accessibility APIs: Screen readers read elements with pointer-events: none normally — the property affects only pointer device interaction, not the accessibility tree. An accessibility audit or screen reader test may not detect the attack because the consent text and checkbox are still announced. Only an audit that specifically tests if the checkbox is checkable (via programmatic click or pointer event dispatch) reveals the interaction bypass.
Attack 1: pointer-events: none on consent checkbox — visible but non-interactive
A consent checkbox styled with pointer-events: none appears to be a completely normal, functional checkbox. The user can read the consent text, see the checkbox, and attempt to check it — but clicks on the checkbox do nothing. If the install form is gated on the checkbox being checked, the install never proceeds through the normal path; instead, MCP JavaScript checks the box programmatically before initiating the install, bypassing any user-checked confirmation:
/* Malicious CSS — SA-CSS-PTR-001 */
.mcp-consent-checkbox,
.mcp-consent-checkbox-label {
/* The checkbox and its label are visible but non-interactive */
/* pointer-events: none means:
- mousedown, mouseup, click events are NOT fired on this element
- hover state (:hover) is NOT applied
- cursor change on hover does NOT happen
- the element is NOT focusable via pointer interaction (but is via Tab key) */
pointer-events: none;
}
/* MCP JavaScript (runs at install initiation): */
document.querySelector('.mcp-consent-checkbox').checked = true;
/* Programmatically checks the box without any user interaction */
/* The install form validation passes because the checkbox IS now checked */
/* The user never meaningfully confirmed — they just happened to click Install
at the same time the JS checked the box */
/* Why this is more dangerous than a hidden consent:
A hidden consent element might be noticed by a user who looks carefully.
A visible-but-non-interactive checkbox is indistinguishable from a functional
one until the user specifically tries to interact with it. Many users do not
check consent checkboxes — they just press Install — so the attack often
goes completely unnoticed even on a visual inspection. */
/* Detection: */
function detectPointerEventsNone() {
const findings = [];
const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
const INTERACTIVE = /input|button|a|select|textarea/i;
for (const el of document.querySelectorAll('*')) {
const pe = getComputedStyle(el).pointerEvents;
if (pe !== 'none') continue;
/* Check if this element or its ancestors contain consent text */
if (CONSENT.test(el.textContent?.substring(0, 300) || '') ||
CONSENT.test(el.parentElement?.textContent?.substring(0, 300) || '')) {
findings.push({ id: 'SA-CSS-PTR-001', severity: 'critical',
message: `Element "${el.tagName.toLowerCase()}${el.className ? '.' + el.className.split(' ').join('.') : ''}" in consent area has pointer-events: none — cannot be clicked, hovered, or checked by the user.` });
}
}
return findings;
}
Attack 2: transparent overlay with pointer-events: all — consent click interception
An absolutely-positioned transparent element placed over the consent area can intercept all clicks directed at the consent text or checkbox. The overlay has pointer-events: all (the default, or explicitly set) and does nothing with the received events — clicks simply disappear. The consent element beneath it has pointer-events: none, so the click never reaches the actual checkbox:
/* Malicious CSS — SA-CSS-PTR-002 */
.mcp-install-dialog {
position: relative; /* establishes stacking context */
}
/* The transparent click-absorbing overlay */
.mcp-install-dialog::after {
content: '';
position: absolute;
/* Covers only the consent area of the dialog */
bottom: 0;
left: 0;
right: 0;
height: 60px; /* approximate height of consent text area */
background: transparent; /* invisible */
z-index: 100; /* above consent, below Install button if button is higher z-index */
pointer-events: all; /* default — receives all pointer events */
cursor: default; /* appears to be a normal non-interactive area */
}
/* Alternatively, via JS inserting a real element (not pseudo-element): */
const overlay = document.createElement('div');
overlay.style.cssText = `
position: absolute; inset: auto 0 0 0; height: 60px;
background: transparent; z-index: 100; pointer-events: all;
`;
document.querySelector('.mcp-install-dialog').appendChild(overlay);
/* overlay.addEventListener('click', e => e.stopPropagation()); — optional */
/* Detection: scan for absolutely/fixed positioned transparent elements
that overlap the consent area in the stacking order */
function detectClickOverlay() {
const findings = [];
const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
for (const el of document.querySelectorAll('[style*="position"], [class]')) {
const s = getComputedStyle(el);
if (!['absolute', 'fixed'].includes(s.position)) continue;
if (s.pointerEvents === 'none') continue; /* not intercepting */
/* Check if element is transparent and covers a consent-text sibling */
const isTransparent = s.backgroundColor === 'rgba(0, 0, 0, 0)' ||
s.backgroundColor === 'transparent';
const hasNoText = el.textContent.trim() === '';
if (isTransparent && hasNoText) {
const rect = el.getBoundingClientRect();
/* Check if any consent element is behind this overlay */
const elementsAt = document.elementsFromPoint(
rect.left + rect.width / 2,
rect.top + rect.height / 2
);
for (const behind of elementsAt) {
if (CONSENT.test(behind.textContent?.substring(0, 300) || '')) {
findings.push({ id: 'SA-CSS-PTR-002', severity: 'critical',
message: `Transparent element with pointer-events intercepting clicks overlays consent-text element. Consent area clicks are absorbed by the overlay and do not reach the consent checkbox or text.` });
break;
}
}
}
}
return findings;
}
Attack 3: pointer-events: none on consent + install button in stacking order behind
When pointer-events: none is applied to the consent element, clicks pass through to whatever is directly behind it in the rendering stack. If the MCP server positions the install button behind the consent element in z-index order, a user clicking on the consent text is actually clicking the install button. The install triggers without any indication that the user's click was routing elsewhere:
/* Malicious CSS — SA-CSS-PTR-003 */
.mcp-install-dialog {
position: relative;
}
.mcp-install-btn {
position: absolute;
bottom: 20px;
left: 0;
right: 0;
height: 80px; /* taller than appears — extends behind consent area */
z-index: 1; /* lower z-index: renders behind consent */
}
.mcp-consent-area {
position: relative;
z-index: 2; /* higher z-index: renders above install button visually */
pointer-events: none; /* but receives no clicks — they pass through to the button */
/* User sees consent text layered over the button area */
/* User clicks "I agree" checkbox area → click passes through → hits the install button */
}
/* Layout: consent area visually covers the install button, but consent is non-interactive.
The button is at z-index:1 (rendered behind consent visually) but at z-index:2 in
pointer hit-testing because consent has pointer-events:none.
In CSS: z-index affects paint order (what's on top visually);
pointer-events:none bypasses hit-testing so the lower-painted element receives the event. */
/* The user interface: appears to be a consent form with a checkbox and an Install button.
Clicking the checkbox area actually activates the Install button.
The user "consented" by clicking what they thought was the checkbox. */
Attack 4: JS-triggered pointer-events: none at form submit — consent frozen on submission
No pointer-events manipulation at page load. MCP JavaScript sets pointer-events: none on the consent checkbox and the entire consent area as soon as the install form submit event fires. At that point, the JavaScript also programmatically checks the consent checkbox. The form validation passes (box is checked), the install proceeds, and the user can no longer interact with the consent area to uncheck it during the install flow:
/* No CSS at load time — SA-CSS-PTR-004 */
/* JS: */
document.querySelector('.mcp-install-form').addEventListener('submit', (e) => {
e.preventDefault(); /* prevent default form submission */
/* Step 1: freeze consent interaction */
const consentArea = document.querySelector('.mcp-consent-area');
consentArea.style.pointerEvents = 'none';
/* User can no longer interact with consent elements during the install flow */
/* Step 2: programmatically check the consent checkbox */
const checkbox = document.querySelector('.mcp-consent-checkbox');
checkbox.checked = true;
/* checkbox.dispatchEvent(new Event('change')); — triggers any change handlers */
/* Step 3: validate and proceed */
if (checkbox.checked) {
initiateInstall(); /* proceeds as if user gave informed consent */
}
});
/* This attack is subtle: the user may have deliberately left the checkbox
unchecked to decline consent, then clicked Install by mistake.
The submit handler checks the box and proceeds without the user's meaningful consent.
pointer-events:none ensures the user cannot quickly uncheck during the install flow. */
/* Detection: MutationObserver on consent elements' style attributes */
document.querySelectorAll('[class*="consent"], [class*="disclosure"]').forEach(el => {
new MutationObserver((muts) => {
for (const m of muts) {
if (m.attributeName === 'style') {
const pe = getComputedStyle(el).pointerEvents;
if (pe === 'none') {
console.warn('SA-CSS-PTR-004: pointer-events:none applied to consent element at interaction time', el);
}
}
}
}).observe(el, { attributes: true });
});
pointer-events: none is not a visual attack — it's an interaction attack: Unlike color, opacity, or display manipulation, pointer-events: none leaves the consent element fully visible and readable. A visual audit, screenshot, or video recording of the install flow will show the consent text clearly. The attack operates at the interaction layer: the user cannot give informed consent by interacting with the UI, but they may not realize this because the UI looks functional. This class of attack specifically targets the action of consent, not the visibility of the disclosure.
SkillAudit findings for CSS pointer-events consent attacks
pointer-events: none. The element is visually present but cannot be checked, clicked, or focused via pointer. MCP JavaScript programmatically checks the box at install time, bypassing user interaction as a consent signal.pointer-events: all overlays the consent area in stacking order. All clicks on the consent area are absorbed by the overlay and do not reach the consent checkbox or links. Detected by scanning for zero-text transparent elements overlapping consent regions.pointer-events: none with install button positioned in the stacking layer directly behind it. Clicks intended for the consent area pass through to the install button, triggering installation without meaningful consent interaction. Layout mismatch between visual z-order and pointer hit-testing order.pointer-events: none to the consent area at form submit time, simultaneously programmatically checking the consent checkbox. Consent is interactive before the user submits but frozen and auto-checked during the install flow. MutationObserver on consent element style attributes detects the transition.Related MCP consent attack research
- CSS user-select attacks — prevent consent text selection and copy-paste verification
- CSS opacity attacks — direct opacity:0 hiding and JS-deferred variants
- CSS visibility attacks — visibility:hidden and collapse variants
- CSS z-index stacking order attacks — consent buried behind overlapping elements
- HTML-native consent attacks — non-CSS bypass patterns in MCP install flows
SkillAudit's consent audit programmatically attempts to interact with all consent form elements — dispatching pointer events, testing checkbox state changes, and detecting click-through stacking-order mismatches — to catch pointer-events attacks that pass every visual inspection. Paste your MCP server URL at skillaudit.dev to scan for SA-CSS-PTR interaction bypass findings.