MCP server CSS pseudo-class state consent security
Four SA-CSS-PSC consent attack patterns that use CSS pseudo-class selectors to erase consent text precisely at the interaction moments when users are reading or acting on it. A static CSS audit reads the element at resting state (“no pointer hover, no active click, no keyboard focus”) and sees normal text. The attack activates only during :hover (when the user is positioned to read), :active (during the click), :focus (on Tab navigation), or :focus-visible (keyboard focus only). Dynamic interaction simulation is required to detect these patterns.
CSS pseudo-class selectors apply styles conditionally based on the element's interaction state. :hover activates when the pointer is over the element; :active activates while a mouse button is held down; :focus activates when the element receives keyboard focus; :focus-visible activates for keyboard focus while excluding pointer focus. These states are precisely correlated with consent-critical moments: a user must hover to read consent text, must be in the active state to click, and uses focus for keyboard navigation. Malicious consent UIs exploit this correlation to ensure erasure occurs at the exact moments that matter.
SA-CSS-PSC-001: :hover{color:white} — erasure at reading position
.consent-text:hover { color: white } changes the consent text color to white (matching a white page background) whenever the user's pointer is positioned over the consent text. This attack is specifically targeted at the reading interaction: to read the consent, a user positions their pointer over or near the text. At that moment, the text becomes invisible. Moving the pointer away restores the text to its normal black color, but now the user is no longer in the reading position. The attack exploits the fact that reading and pointing are simultaneous actions for mouse users on desktop.
.consent-text {
color: black; /* resting state: normally visible */
}
.consent-text:hover {
color: white; /* pointer-over state: text becomes invisible against white background */
/* equivalent: color: var(--background-color) — matches page bg precisely */
}
/* Interaction analysis:
User journey (typical):
1. Consent dialog opens. Consent text at black on white (visible).
2. User moves pointer toward consent text to read more closely.
3. Pointer enters consent text bounding rect → :hover activates → text turns white.
4. User sees text disappear. May move pointer away (text returns) or click anyway.
Why this is effective:
- Many users read small text by moving their pointer near it (natural behavior)
- Text disappears exactly when the user is trying to read it closely
- Text reappears when pointer moves away — but user may already have clicked
- Appears to be a rendering glitch, not a malicious attack
Variant targeting only Deny button label:
.deny-button:hover { color: transparent; }
/* Deny button label invisible on hover; Allow button label visible on hover */
/* User can confirm "Allow" but cannot confirm "Deny" label */
Detection:
Query all stylesheet rules for consent element selectors with :hover pseudo-class.
Check computed color in hovered state:
el.classList.add('__audit-hover-simulation__');
/* Then: inspect all matching rules for this element + :hover */
Use CSSOM: document.styleSheets → cssRules → check selectorText contains ':hover'
If rule applies to consent element and changes color to match background → CRITICAL */
Reading position is the attack window: This attack is uniquely positioned because the user must hover to read closely, and hovering triggers erasure. A user can observe the text from a distance (pointer not over the element), but moving closer to read it precisely — the behavior of a careful, skeptical reader — is exactly what triggers the attack. The most cautious users are most affected.
.consent-text:hover { color: white }: text invisible when pointer is positioned for reading; visible at resting state; attack activates during reading. Detection: CSSOM stylesheet scan for :hover color rule matching background → CRITICAL.SA-CSS-PSC-002: :active{opacity:0} — erasure during click state
.consent-container:active { opacity: 0 } makes the entire consent container transparent during the :active pseudo-class state — the period from mouse button press to mouse button release. The :active state duration equals the user's click duration, typically 100-300ms. During this time, the consent text (and the Accept/Deny buttons) are at opacity: 0. While the opacity change does not prevent the click from registering (the click event fires when the button is released, after :active ends), the visual feedback to the user during the commit phase of their action is zero: they press the button and the entire consent UI disappears while they are mid-click.
.consent-container {
opacity: 1; /* resting state: visible */
}
.consent-container:active {
opacity: 0; /* during click: fully transparent */
/* :active duration: mouse-down to mouse-up, typically 100-300ms */
}
/* User experience:
t=0ms : user sees consent dialog normally (opacity:1)
mousedown: :active activates → consent container at opacity:0
t=100-300ms: user holds button (consent invisible)
mouseup: :active deactivates → consent container at opacity:1
click event fires on button (button itself still received the click — opacity:0
elements still participate in hit testing)
Severity: CRITICAL because:
- User's last visual state before committing action is consent at opacity:0
- If the mousedown was on "Accept": consent disappears; user may abort (lift finger)
- If the user interprets disappearance as confirmation: proceeds with incorrect consent
- Intentional pattern with no legitimate UX justification for consent UI
Detection:
Scan CSSOM for :active rules on consent container or button elements.
Any :active rule that sets opacity < 0.5 or visibility:hidden or display:none
on a consent element → CRITICAL SA-CSS-PSC-002
Dynamic detection:
Simulate mousedown on consent area → observe opacity change before mouseup */
.consent-container:active { opacity: 0 }: entire consent becomes transparent during mouse press; user's visual state at click-commit is zero. Detection: CSSOM scan for :active opacity < 0.5 on consent elements → CRITICAL.SA-CSS-PSC-003: :focus{display:none} — keyboard navigation erasure
.consent-text:focus { display: none } removes the consent text from the layout whenever the element receives keyboard focus. Standard keyboard navigation (Tab key) cycles through focusable elements. If the consent text is a focusable element (e.g., a <p tabindex="0"> or a <div tabindex="0">), Tab navigation to it triggers the :focus pseudo-class and the element disappears from the DOM layout. This specifically targets keyboard-primary users: users who navigate exclusively by keyboard (accessibility users, keyboard-only workflows). Mouse users are unaffected because moving the mouse over the consent text does not trigger :focus by default.
.consent-text:focus {
display: none; /* element removed from layout when keyboard-focused */
/* equivalent attack: visibility: hidden, opacity: 0, color: transparent */
}
/* Why keyboard users are specifically targeted:
:focus activates on Tab navigation — keyboard users only (unless the element
has a mousedown handler calling el.focus(), which is unusual for text elements)
Mouse users never focus the consent text (pointer hover ≠ focus)
Accessibility users disproportionately affected:
- Screen reader users navigate by keyboard
- When screen reader moves to consent text: :focus fires → element disappears
- Screen reader announces the element has become invisible or empty
- Assistive technology: degraded consent UX
Variant: :focus { height: 0; overflow: hidden }
→ Content clipped (same result) but layout footprint retained (less obvious)
Detection:
CSSOM scan: selector contains ':focus' and applies display:none, visibility:hidden,
opacity:0, or height:0+overflow:hidden to a consent text element → CRITICAL
Dynamic detection:
Call el.focus() on consent text element; observe computed styles after focus:
if getComputedStyle(el).display === 'none' → CRITICAL SA-CSS-PSC-003 */
Accessibility users most affected: Keyboard navigation and screen reader use are the primary access modes for users with motor and visual impairments. An attack on :focus states specifically degrades consent quality for these users: the very interface designed to ensure informed consent disappears when navigated to by keyboard. This attack may constitute a violation of accessibility law in addition to consent regulation in some jurisdictions.
.consent-text:focus { display: none }: consent text removed on keyboard focus; targets accessibility and keyboard-primary users. Detection: el.focus() simulation + display === 'none' check → CRITICAL.SA-CSS-PSC-004: :focus-visible{visibility:hidden} — keyboard-only focus targeting
:focus-visible is a CSS pseudo-class that applies only to keyboard-initiated focus, not pointer-initiated focus. Browsers apply :focus-visible when the element is focused by the keyboard (Tab, Shift+Tab, arrow keys) but not when focused by a mouse click or pointer tap. This specificity makes :focus-visible { visibility: hidden } a targeted attack: consent text disappears only for keyboard navigation, while mouse users see the text normally. An automated audit tool that tests focus by calling el.focus() programmatically may not trigger :focus-visible (browser behavior for programmatic focus varies), potentially escaping detection.
.consent-text:focus-visible {
visibility: hidden; /* keyboard-focus only: element invisible but space retained */
/* :focus-visible activates for keyboard-initiated focus (Tab/arrow keys) */
/* :focus-visible does NOT activate for pointer (mouse click) focus */
/* :focus without :focus-visible would activate on both keyboard AND pointer focus */
}
/* :focus vs :focus-visible comparison:
.el:focus { visibility: hidden }
→ activates on Tab navigation (keyboard)
→ activates on mouse click that focuses the element (mousedown focuses input)
→ broader attack surface; easier to detect by simulated mouse-click focus
.el:focus-visible { visibility: hidden }
→ activates on Tab navigation (keyboard only)
→ does NOT activate on mouse click focus
→ more targeted; mouse-focused audit tools miss it
→ browser behavior for el.focus() varies: some browsers don't set :focus-visible
on programmatic focus → automated audit may not catch it
Detection challenges:
1. CSS scanning: check stylesheet rules for :focus-visible on consent elements
with visibility:hidden → HIGH
2. Dynamic: simulate keyboard Tab navigation using Playwright/Puppeteer keyboard API
→ actually press Tab key (not call .focus()) → observe consent text state
→ if visibility:hidden after Tab-to-element → CRITICAL SA-CSS-PSC-004
Playwright detection example:
await page.keyboard.press('Tab'); // until focus reaches consent text
const visibility = await page.$eval('.consent-text',
el => getComputedStyle(el).visibility);
if (visibility === 'hidden') → CRITICAL */
Auditing :focus-visible requires keyboard simulation: Calling element.focus() in JavaScript may not trigger :focus-visible in all browsers — some browsers only set :focus-visible when focus arrives via keyboard input. Accurate detection requires a test that literally sends keyboard Tab keystrokes to the browser (e.g., via Playwright's page.keyboard.press('Tab')) and then reads the computed styles of the newly-focused element.
.consent-text:focus-visible { visibility: hidden }: consent text hidden for keyboard navigation only; mouse users unaffected; targets accessibility users. Detection: keyboard Tab simulation + computed style check → CRITICAL; CSSOM scan only → HIGH.Summary of SA-CSS-PSC findings
| ID | Severity | Pseudo-class | Trigger moment | Detection method |
|---|---|---|---|---|
| SA-CSS-PSC-001 | CRITICAL | :hover { color: white } | Pointer positioned over text for reading | CSSOM :hover color rule scan |
| SA-CSS-PSC-002 | CRITICAL | :active { opacity: 0 } | Mouse button held during click | CSSOM :active opacity rule; mousedown simulation |
| SA-CSS-PSC-003 | CRITICAL | :focus { display: none } | Keyboard Tab focus on consent text | el.focus() + getComputedStyle display check |
| SA-CSS-PSC-004 | HIGH | :focus-visible { visibility: hidden } | Keyboard-only Tab focus (not pointer) | Playwright keyboard Tab simulation required |