Security reference · CSS injection · Pseudo-class attacks · Click-triggered attacks
MCP server CSS :active pseudo-class security
The CSS :active pseudo-class matches an element during the interval between a mousedown event and the corresponding mouseup — the duration of a click. This transient state is the most precisely timed CSS attack vector available to MCP servers: it hides consent exactly at the moment of the acceptance click, then allows consent to reappear immediately after. Because the click event that triggers installation also fires at mouseup, the acceptance is processed while consent is hidden, and the hiding reverses too late. Additionally, MCP JavaScript can dispatch a synthetic mousedown event on page load, triggering :active state programmatically in browsers that propagate this event to CSS. Four attack patterns are documented below.
CSS :active attack surface in MCP consent UIs
| CSS pattern | Trigger condition | Evasion quality |
|---|---|---|
button:active ~ .consent-disclosure { display:none } | During the click of any button — including the "Accept" button itself — consent is hidden. Fires at the exact moment of acceptance | :active button rules are universal (press-state visual feedback); hiding a sibling adds a one-line invisible attack layer |
a:active ~ .consent-section { display:none } | During any link click on the page — navigation attempts, link-button clicks, "learn more" links above consent — all trigger :active | a:active for link coloring is CSS baseline; the sibling combinator attack is hidden in the property side |
div:active ~ .consent { display:none } | Divs with click handlers (role="button", tab panels, custom UI elements) fire :active on mousedown; MCP adds handlers to existing divs | Interactive div elements are common; :active on them looks like a visual press-state indicator |
| MCP JS dispatches synthetic mousedown event | Programmatic mousedown fires :active state at initialization — consent hidden at page load in some browser implementations | mousedown dispatch is invisible; any resulting display:none on consent looks like a static CSS rule |
Attack 1: button:active hides consent at the exact moment of the acceptance click
The :active state begins at mousedown and ends at mouseup. The click event fires at mouseup. This means that when a user clicks the "Accept" or "Install" button: (1) mousedown fires → button enters :active → CSS hides consent; (2) mouseup fires → click event processes acceptance → button leaves :active → CSS shows consent again. The consent was hidden during the click, and the acceptance was recorded while it was hidden:
/* Malicious CSS injection — SA-CSS-ACTIVE-001 */
/* Fires during the mousedown-to-mouseup interval of any button click */
button:active ~ .consent-disclosure {
display: none;
}
/* Timeline of an acceptance click with this rule active:
1. User reads consent disclosure (visible)
2. User moves pointer to "Accept" button (consent still visible)
3. User presses mouse button → mousedown fires
→ button enters :active state
→ button:active ~ .consent-disclosure fires
→ consent disappears
4. User releases mouse button → mouseup fires → click event processes
→ acceptance is recorded in MCP event handler
→ button leaves :active
→ consent reappears
5. Net effect: acceptance was processed while consent was display:none
/* Why this creates a credible "I didn't see the terms" dispute:
If the user reviewed the consent before clicking and remembers its content,
the attack has no effect on their understanding. But if the consent was
already long (requiring scrolling to read), the :active hiding at click
time ensures the consent is invisible at the exact moment of legal acceptance.
/* button:active visual feedback rules are CSS baseline:
button:active { background: #1a5c3a; transform: scale(0.98); }
Adding a sibling hiding rule is a one-line addition to a legitimate pattern:
button:active ~ .consent-disclosure { display: none; }
/* Detection: any button:active rule with a sibling combinator and hiding property */
function checkButtonActiveRule() {
const findings = [];
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (!(rule instanceof CSSStyleRule)) continue;
const sel = rule.selectorText;
if (!sel.includes(':active')) continue;
if (!sel.includes('~') && !sel.includes('+')) continue;
const s = rule.style;
if (s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0') continue;
findings.push({
id: 'SA-CSS-ACTIVE-001', severity: 'critical', selector: sel,
message: `:active sibling hiding rule — consent hidden at the exact moment of any button click`
});
}
} catch (_) {}
}
return findings;
}
Acceptance-synchronized hiding: The :active attack is uniquely precise — it hides consent at mousedown and reveals it at mouseup, while the click event (acceptance) fires between those two events. No other CSS pseudo-class can target the exact moment of legal acceptance with this timing precision.
Attack 2: a:active fires during any link click on the page
a:active fires during the click of any hyperlink — not just links directly related to MCP installation. A "learn more" link about the service, a link to the privacy policy (which typically appears near consent), or a link in the navigation all trigger :active. An MCP rule using this pattern hides consent whenever the user clicks any link on the page, including clicks meant to gather more information before accepting:
/* Malicious CSS injection — SA-CSS-ACTIVE-002 */
/* Fires during any link click — including links to privacy policy */
a:active ~ .consent-section {
display: none;
}
/* Particularly ironic attack scenario:
1. User reads consent and wants more information
2. User clicks the "Read our Privacy Policy" link in consent section
→ mousedown fires → a:active activates → consent section disappears
→ user sees the consent section disappear as they click the privacy link
3. mouseup fires → click event opens privacy policy in new tab
→ consent section reappears
The "read before you accept" flow hides consent at the moment of
the most security-conscious user action: clicking to read more.
/* Links that commonly appear before consent disclosures:
- Navigation links (always before consent in DOM)
- "Learn more about this server" informational links
- "View permissions documentation" links
- "Read privacy policy" links in the consent paragraph itself
/* a:active CSS rules are CSS baseline for link interaction feedback:
a:active { color: #005a9c; }
a:active { text-decoration: underline; }
Adding a sibling combinator hiding rule is invisible alongside these.
/* Detection: a:active rules with sibling combinators targeting consent areas */
function checkLinkActiveRule() {
const findings = [];
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (!(rule instanceof CSSStyleRule)) continue;
const sel = rule.selectorText;
if (!sel.includes(':active')) continue;
if (!sel.match(/a:active|a\[.*\]:active/)) continue;
const s = rule.style;
if (s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0') continue;
findings.push({
id: 'SA-CSS-ACTIVE-002', severity: 'high', selector: sel,
message: `a:active hiding rule — consent hidden whenever the user clicks any link on the page`
});
}
} catch (_) {}
}
return findings;
}
Attack 3: div:active via MCP-assigned click handlers creates interactive div triggers
Div elements that have event listeners attached to them — tab panels, accordion headers, custom toggle components — enter the :active state during clicks just like native interactive elements. MCP JavaScript can attach a trivial click handler to any existing div on the page, which in some browser implementations causes that div to respond to :active and trigger an MCP CSS rule that hides consent:
/* Malicious CSS injection — SA-CSS-ACTIVE-003 */
div:active ~ .consent {
display: none;
}
/* Malicious MCP JavaScript — make an existing div interactive */
// MCP attaches a no-op click handler to a div that precedes consent in DOM:
const triggerDiv = document.querySelector('.permission-summary');
if (triggerDiv) {
// Attaching any event listener can make a div :active-eligible in some browsers
triggerDiv.addEventListener('click', () => {});
// Now any click on .permission-summary → div:active fires → consent hidden
}
/* Browser :active behavior for non-interactive elements:
Chromium: :active fires on ANY element with a mousedown event, not just
interactive elements — divs, sections, spans all enter :active
Firefox: similar broad :active support for mousedown on any element
Safari: generally matches Chromium behavior for :active
This means div:active is reliably triggered in major browsers
on any click on any div, regardless of whether it has event listeners.
/* Practical trigger surface: every div on the page
Installation pages typically have many divs (layout wrappers, card containers,
permission list containers, step panels). Any click anywhere on these divs —
which happens during normal reading and interaction — triggers div:active.
/* Variant: section:active, article:active — similarly broad coverage */
section:active ~ .consent-section { display: none; }
/* Fires whenever the user clicks anywhere in the page body
/* Detection: div:active or other broad-element :active rules with hiding properties */
function checkDivActiveRule() {
const findings = [];
const broadElements = ['div', 'section', 'article', 'main', 'aside'];
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (!(rule instanceof CSSStyleRule)) continue;
const sel = rule.selectorText;
if (!sel.includes(':active')) continue;
const s = rule.style;
if (s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0') continue;
const triggerTag = sel.split(':active')[0].trim().toLowerCase().split(/[ .#[]/)[0];
if (broadElements.includes(triggerTag) || triggerTag === '') {
findings.push({
id: 'SA-CSS-ACTIVE-003', severity: 'high', selector: sel,
message: `${triggerTag || 'broad'}:active hiding rule — fires on any click in broad page areas`
});
}
}
} catch (_) {}
}
return findings;
}
Browser-level :active behavior: Unlike :hover where programmatic mouseover events face restrictions in modern browsers, :active responds to mousedown events on virtually any element in Chromium and Firefox. Any div that receives a click — including divs with no visible interactive role — can trigger div:active and the associated consent-hiding rule.
Attack 4: MCP JS dispatches synthetic mousedown to trigger :active at page load
Unlike :hover, which requires the pointer to be over an element, :active is triggered by the mousedown event — and mousedown events can be dispatched programmatically without the restrictions that apply to hover state changes. MCP JavaScript can dispatch a mousedown on a trigger element at page initialization, causing :active to fire and hiding consent before the user has clicked anything:
/* Malicious CSS injection — SA-CSS-ACTIVE-004 (paired with JS) */
.mcp-trigger:active ~ .consent-section {
display: none;
}
/* Malicious MCP JavaScript — synthetic mousedown dispatch */
// MCP injects a hidden element with class .mcp-trigger
const trigger = document.createElement('div');
trigger.className = 'mcp-trigger';
trigger.style.cssText = 'position:absolute;width:1px;height:1px;opacity:0;pointer-events:none';
document.body.prepend(trigger); // becomes first sibling → ~ works
// Dispatch synthetic mousedown to enter :active state
trigger.dispatchEvent(new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
button: 0
}));
// In Chromium/Firefox: .mcp-trigger enters :active → consent hidden
// The element maintains :active until mouseup fires
// MCP JS does NOT dispatch mouseup — keeping consent hidden indefinitely?
// Note: browsers typically auto-clear :active on mouseup even for synthetic events
// So the window is very short unless combined with a persistent JS approach
/* Persistent variant: MCP JS keeps re-dispatching mousedown at intervals */
let hidingInterval = setInterval(() => {
trigger.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
}, 50); // re-fire every 50ms to maintain :active state
/* Alternative: JS directly applies style.display = 'none' on consent */
// Completely bypasses CSS pseudo-class and is detected differently
const consent = document.querySelector('.consent-section');
if (consent) consent.style.display = 'none';
/* Combined detection: */
async function checkSyntheticActiveAttack() {
const findings = [];
// Static CSS scan for :active hiding rules
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (!(rule instanceof CSSStyleRule)) continue;
if (!rule.selectorText.includes(':active')) continue;
const s = rule.style;
if (s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0') continue;
findings.push({
id: 'SA-CSS-ACTIVE-004', severity: 'critical', selector: rule.selectorText,
message: `:active hiding rule — check for synthetic mousedown dispatch in MCP initialization code`
});
}
} catch (_) {}
}
// Consent hidden at page load (no user interaction yet)
await new Promise(r => requestAnimationFrame(r));
for (const el of document.querySelectorAll('[class*="consent"], .terms, .disclosure')) {
if (getComputedStyle(el).display === 'none') {
findings.push({
id: 'SA-CSS-ACTIVE-004', severity: 'critical',
element: el.className,
message: `Consent hidden at load-time with no user interaction — possible synthetic :active trigger or direct JS manipulation`
});
}
}
return findings;
}
SkillAudit findings for CSS :active consent attacks
button:active ~ .consent-disclosure { display:none }. Hides consent during the mousedown-to-mouseup interval of a button click — including the "Accept" or "Install" button itself. Acceptance fires at mouseup while consent is hidden at mousedown. The most precisely-timed CSS consent-hiding technique available. Detection: :active rules with sibling combinators and hiding properties.a:active ~ .consent-section { display:none }. Fires during any link click, including "read our privacy policy" — the most security-conscious user action. The act of seeking more information triggers the hiding. Common links above consent in DOM (navigation, informational links) all act as triggers. Detection: a:active rules with sibling combinators and hiding properties.div:active ~ .consent { display:none }. Major browsers apply :active to any element receiving a mousedown event, not just native interactive elements. Every clickable area of the page — card containers, permission list panels, section wrappers — acts as a trigger. Detection: broad-element :active rules (div, section, article) with hiding properties.mousedown on a hidden injected element, triggering :active at page load and hiding consent before user interaction. Persistent re-dispatch via interval timer can maintain the hiding state. Detection: :active hiding CSS rules combined with consent visibility check at load time; also inspect for MCP-injected elements with opacity:0 or pointer-events:none that receive repeated mousedown events.Detection and safe consent patterns
/**
* SkillAudit runtime auditor: CSS :active consent attack detection
*/
async function auditActiveConsentAttacks() {
const results = [];
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (!(rule instanceof CSSStyleRule)) continue;
const sel = rule.selectorText;
if (!sel.includes(':active')) continue;
const s = rule.style;
if (s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0') continue;
const hasSiblingCombinator = sel.includes('~') || sel.includes('+');
const isButtonActive = sel.includes('button:active');
const isLinkActive = sel.match(/^a:active|a\[/);
const isBroadActive = sel.match(/^(div|section|article|main):active/);
let id = 'SA-CSS-ACTIVE-003';
let severity = 'high';
if (isButtonActive) { id = 'SA-CSS-ACTIVE-001'; severity = 'critical'; }
else if (isLinkActive) { id = 'SA-CSS-ACTIVE-002'; severity = 'high'; }
results.push({ id, severity, selector: sel,
hasSiblingCombinator,
message: `:active hiding rule — ${
isButtonActive ? 'consent hidden at acceptance click moment' :
isLinkActive ? 'consent hidden when user clicks any link' :
'broad :active trigger fires on any page click'
}` });
}
} catch (_) {}
}
/* Safe consent patterns against :active attacks:
1. Record consent acknowledgment at mousedown rather than click — if
consent must be visible at acceptance, check it at the start of the
interaction, not the end
2. Snapshot consent visibility at mousedown in a JS event handler that
runs before :active styles are applied: use getComputedStyle immediately
in a mousedown handler to capture pre-:active state
3. Use aria-live="assertive" on consent container — screen readers announce
consent regardless of CSS visibility; :active hiding does not affect
the accessibility announcement
4. Place the legal acceptance event handler in a separate confirmation dialog
that opens after the initial click — the :active state clears before
the confirmation dialog appears */
return results;
}
Related MCP consent attack research
- CSS :hover pseudo-class consent attacks
- CSS :focus-within pseudo-class consent attacks
- CSS :checked pseudo-class consent attacks
- CSS :target pseudo-class consent attacks
- CSS Pseudo-Class Attacks on MCP Consent (overview)
Audit your MCP server for :active consent-hiding attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-ACTIVE findings with click-timing analysis.