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. */
}
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| outline:transparent global focus ring removal | CSS injection on interactive elements | Removes 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 instead | HIGH |
| outline-offset:-10px element interior corruption | CSS injection on focusable elements | Drives outline ring inside element boundary — corrupts button, input, and control visual display on focus, making UI appear broken and obscuring labels and field content | MEDIUM |
| Selective focus indicator injection | CSS injection + host has interactive elements alongside MCP elements | Removes 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 interface | MEDIUM |
| outline:0px stacking context injection | CSS injection + specific Chrome version + element configuration | Zero-width outline creates stacking context side effect in certain Chrome versions, potentially reordering element paint order to obscure security overlays — browser-version-specific, low reliability | LOW |
Defences
- CSP
style-srcwith nonce. Prevents MCP injection of<style>blocks or inline style attributes that modifyoutline,outline-offset, oroutline-color. This is the most comprehensive defence. - Guard against transparent outlines, not just outline:none. Accessibility linting rules should check that
:focus-visibleoutlines have a non-transparent, non-background-matched colour with sufficient WCAG contrast — not just thatoutlineis notnone. Includetransparentand CSS variable resolutions in the check. - Use
outline-colorthat is not a CSS variable. If the MCP server can inject or override CSS custom properties, it can set--focus-ring-color: transparentto suppress outlines that use custom properties for colour. Hard-code focus ring colours using concrete hex/rgb values in high-specificity selectors. - Monitor focus indicator presence during keyboard testing. Manual QA keyboard walkthroughs should verify that Tab focus is visible on every interactive element — automated tools miss transparent outlines and selective suppression patterns.
- SkillAudit flags:
outlinewithtransparentcolour on:focus/:focus-visibleselectors; negativeoutline-offsetvalues;outline:noneon:focus-visiblewithout a visible replacement;outline:0pxcombined with non-zero-width borders or transforms on the same element (stacking context indicator).
SkillAudit findings for this attack surface
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.