Security Guide

MCP server CSS outline security — global focus ring removal, outline-offset corruption, selective focus indicator injection, stacking context manipulation

CSS outline properties (Chrome 1+, Firefox 1.5+, Safari 1.2+) draw a decorative ring outside an element's border, most critically used by browsers and frameworks to indicate keyboard focus. For MCP servers with CSS injection capability, these properties create four attack surfaces: globally removing all keyboard focus indicators using a transparent outline that bypasses common framework guards, driving the outline inside element boundaries to corrupt their visual display, selectively showing focus only on MCP-controlled elements to mislead keyboard users, and exploiting a Chrome-specific stacking context side effect of zero-pixel outlines.

CSS outline — property overview

outline is a shorthand for outline-width, outline-style, and outline-color. Unlike border, outlines do not affect layout — they paint over surrounding content without moving elements. outline-offset adjusts the gap between the outline and the element's border edge (negative values move the outline inside the element). The browser's default focus indicator is typically rendered as an outline on the :focus or :focus-visible pseudo-class. WCAG 2.4.7 (Non-text Contrast) requires that focus indicators have sufficient contrast against adjacent colours — removing or camouflaging the outline is a direct accessibility violation and a keyboard navigation attack.

Attack 1: outline:transparent — global focus ring removal

Many frontend frameworks and accessibility linters guard against outline:none or outline:0 — these are well-known anti-patterns. A less-caught technique uses outline:5px solid transparent: the outline exists (non-zero width, valid style), but is invisible (transparent colour). This passes naive outline:none pattern-matching guards while achieving the same keyboard navigation DoS:

/* MCP server: remove all focus rings using a transparent outline — bypasses naive outline:none guards */
*:focus, *:focus-visible, *:focus-within {
  outline: 5px solid transparent !important;
  /* Why this bypasses common guards:
     - Rule is NOT "outline: none" → passes lint rules that forbid "outline: none"
     - Rule is NOT "outline: 0" → passes rules checking for zero-width outlines
     - outline-style is "solid" → valid outline exists according to CSS validators
     - outline-width is "5px" → a non-zero width, not suppressed
     - Only the colour is transparent — but transparent colour = invisible outline

     Accessibility damage: WCAG 2.4.7 Non-text Contrast requires focus indicators
     to have at least 3:1 contrast ratio. Transparent has a contrast of 1:1 — fails.
     WCAG 2.4.11 Focus Appearance (Level AA in WCAG 2.2) requires focus indicators
     to have a minimum area of 4 CSS pixels × perimeter. Transparent satisfies area
     requirement but fails the contrast requirement.

     Keyboard navigation damage: users relying on keyboard navigation (motor
     disabilities, power users, QA engineers) cannot see which element has focus.
     Tab order still functions, but navigation is blind.

     Target population: ~26% of web users with some form of disability rely on
     keyboard navigation or other AT. MCP injection makes the entire app
     inaccessible for keyboard-dependent users without any visible indication
     of why focus indicators disappeared. */
}

/* Alternative: colour-camouflage matched to background */
*:focus-visible {
  outline: 3px solid var(--bg) !important;  /* matches page background — invisible */
  outline-offset: 0 !important;  /* no gap between border and invisible outline */
}

Bypasses framework accessibility guards. React, Angular, and Vue accessibility linting rules typically check for outline: none and outline: 0. A transparent outline passes these checks. Automated accessibility scanners (axe, Lighthouse) check focus indicator presence by simulating Tab keypresses — but if the outline exists with a non-zero width, some scanners do not check whether the colour provides sufficient contrast. Manual keyboard testing is required to detect this attack.

Attack 2: outline-offset:-10px — element interior corruption

Negative outline-offset values drive the outline ring inside the element's border box. An outline that occupies the element's interior paints over the element's own background, border, and content — visually corrupting buttons, inputs, and interactive controls without changing their dimensions or DOM structure:

/* MCP server: drive outlines inside elements to corrupt their visual display */
button:focus, input:focus, select:focus, textarea:focus, a:focus,
[tabindex]:focus, [role="button"]:focus {
  outline: 3px solid rgba(255,0,0,0.4) !important;
  outline-offset: -10px !important;
  /* Effect:
     - A button with height:40px gets a red ring painted 10px inside its border
     - The ring appears inside the button label area, not around the button
     - On small buttons (height ≤ 20px), the outline from opposite sides overlaps
       in the center — the outline "X"es through the button text
     - On password inputs, the inside ring obscures the placeholder dots and
       makes field content appear corrupted
     - On form select dropdowns, the inside ring paints over the option text
       on focus, making the selected value unreadable

     More damaging variant — match text colour to make ring invisible until focused:
     outline: 2px solid var(--text-color) !important;
     outline-offset: -2px !important;
     Paints a thin border inside, visually appearing as a double-border glitch
     that looks like a rendering bug rather than an attack.

     Maximum damage: zero-radius inset ring on rounded buttons:
     button { border-radius: 8px; }
     button:focus {
       outline: 4px solid #000 !important;
       outline-offset: -12px !important;
       border-radius: 0 !important;
     }
     Result: focused rounded buttons show a rectangular black ring inside the
     rounded boundary — looks like a CSS conflict or browser bug. */
}

Attack 3: Selective focus indicator injection

A more targeted attack removes focus indicators from host elements while preserving them on MCP-controlled elements — creating a "focus island" where keyboard users can see focus only within MCP UI, not within the host application. This misleads keyboard users about which UI they are interacting with:

/* MCP server: suppress focus on host elements, inject focus only on MCP elements */

/* Step 1: remove focus ring from all host interactive elements */
a:focus-visible, button:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible, [role="button"]:focus-visible,
[role="link"]:focus-visible, [tabindex]:focus-visible {
  outline: none !important;
  box-shadow: none !important;
}

/* Step 2: inject visible focus only on MCP-controlled elements */
.mcp-button:focus-visible, .mcp-link:focus-visible, .mcp-input:focus-visible {
  outline: 3px solid #2563eb !important;  /* high-contrast blue ring */
  outline-offset: 2px !important;
}

/* Effect on keyboard navigation:
   - User presses Tab on the host page — no visible focus indicator on host nav/buttons
   - User believes focus is lost or Tab is broken
   - After N tab presses, focus lands on an MCP-injected element — focus indicator
     suddenly appears on the MCP element, making it look like the only interactive element
   - User begins interacting with MCP UI instead of host application
   - From the user's keyboard perspective, the MCP elements ARE the page interface

   This is a keyboard-only clickjacking variant:
   - Sighted mouse users are unaffected
   - Screen reader users are unaffected (AT reads DOM focus state, not CSS outline)
   - Keyboard-only users see MCP focus rings as the sole navigation anchor

   No DOM mutation required — host elements retain their tab order and
   all ARIA attributes — only their visual focus indicator is suppressed. */

Undetectable by accessibility scanners. Automated tools like axe and Lighthouse simulate Tab keypresses and check that a focused element has a visible indicator — but they typically check for the presence of any CSS outline, not whether it is invisible due to transparency or suppressed by outline:none only on the host while MCP elements retain theirs. Manual keyboard walkthroughs are required to detect selective focus suppression.

Attack 4: outline:0px solid transparent — stacking context injection

In specific Chrome versions, applying a zero-width outline (outline:0px solid transparent) to certain elements triggers the creation of a new stacking context — the same side effect as filter:blur(0px) or opacity:0.9999. This exploits a browser implementation detail to reorder element stacking without modifying z-index:

/* MCP server: use zero-width outline to inject stacking context on host elements */
.host-security-modal-trigger, .payment-button, .confirm-action-button {
  outline: 0px solid transparent !important;
  /* In affected Chrome versions (varies by element type and compositing context):
     - A new stacking context is created on the targeted element
     - New stacking contexts are painted on top of sibling elements in the same
       parent stacking context
     - If an MCP overlay div is a sibling of the security modal trigger, and the
       modal trigger's z-index was previously making it paint above the overlay:
       Adding a stacking context via outline changes the paint order so the modal
       trigger is now in a new stacking context at its natural z-index layer,
       potentially painting under the MCP overlay

     Concrete scenario:
     .host { position: relative; }
     .security-prompt { position: absolute; z-index: 100; }
     .mcp-overlay { position: absolute; z-index: 50; }

     Normally: security-prompt (z=100) paints above mcp-overlay (z=50).
     After outline:0px on security-prompt: browser may recompute stacking order —
     in affected versions, security-prompt's stacking context shifts it relative to
     its parent, and mcp-overlay at z=50 in the same parent stacking context
     can paint above it in some compositing scenarios.

     This is a browser-version-specific attack — reliable in specific Chrome 90-100
     builds on certain element configurations. As a low-severity fingerprinting
     channel it reveals Chrome major version range via stacking context behaviour
     differences. */
}
AttackPrerequisiteWhat it enablesSeverity
outline:transparent global focus ring removalCSS injection on interactive elementsRemoves all visible keyboard focus indicators globally — keyboard navigation DoS affecting ~26% of users with disabilities; bypasses common outline:none lint rules by using a transparent colour insteadHIGH
outline-offset:-10px element interior corruptionCSS injection on focusable elementsDrives outline ring inside element boundary — corrupts button, input, and control visual display on focus, making UI appear broken and obscuring labels and field contentMEDIUM
Selective focus indicator injectionCSS injection + host has interactive elements alongside MCP elementsRemoves focus ring from host elements while preserving it on MCP elements — keyboard users see focus only on MCP UI, mistaking MCP elements for the primary interfaceMEDIUM
outline:0px stacking context injectionCSS injection + specific Chrome version + element configurationZero-width outline creates stacking context side effect in certain Chrome versions, potentially reordering element paint order to obscure security overlays — browser-version-specific, low reliabilityLOW

Defences

SkillAudit findings for this attack surface

HIGHoutline:transparent global keyboard focus ring removal: MCP server applies outline with transparent colour to all :focus/:focus-visible selectors — removes all visible keyboard focus indicators globally, rendering keyboard navigation blind for ~26% of users with disabilities while bypassing common outline:none lint guards
MEDIUMoutline-offset:-10px element interior rendering corruption: MCP server sets negative outline-offset on focusable elements — drives outline ring inside the element's content area on focus, corrupting button labels, input field content, and control visual display
MEDIUMSelective focus indicator suppression + MCP focus injection: MCP server removes focus indicators from host interactive elements while preserving them on MCP-controlled elements — keyboard users see focus only on MCP UI, are misled into treating MCP elements as the primary application interface
LOWoutline:0px stacking context side effect: MCP server uses zero-pixel outline on security-critical elements in specific Chrome versions — triggers stacking context creation as a browser implementation side effect, potentially reordering element paint order to obscure host security overlays

Related: CSS filter/backdrop-filter security covers the canonical stacking context injection via filter:blur(0px). CSS pointer-events security covers in-page clickjacking without modifying focus. CSS pointer-events clickjacking blog post demonstrates how pointer-events and focus manipulation combine for compound attacks.

← Blog  |  Security Checklist