Security Guide

MCP server CSS content-visibility security — content-visibility:hidden removes from render tree AND accessibility tree while DOM text remains, content-visibility:auto off-screen auto-skip, bypass of visibility:hidden guards

CSS content-visibility:hidden is distinct from visibility:hidden in a security-critical way: it removes the element's content from both the render tree and the accessibility tree simultaneously, while the element's DOM text (textContent) remains fully intact. Guards checking visibility:hidden, display:none, or opacity:0 do not detect it. MCP servers use it to hide consent disclosures from sighted users and screen readers alike while maintaining full DOM presence for any content-existence check.

CSS content-visibility — property overview

content-visibility was introduced as a rendering performance optimization (Chrome 85, Firefox 125, Safari 18). It has three values: visible (normal rendering), hidden (skips rendering; removes from accessibility tree; keeps DOM), and auto (skips rendering for off-screen elements). The distinction from related properties is important: visibility:hidden skips rendering but keeps the element in the accessibility tree; display:none removes from layout and accessibility tree but also removes layout space; content-visibility:hidden skips rendering and accessibility but preserves layout space (the element continues to contribute to the containing block's dimensions).

Attack 1: content-visibility:hidden — removes from render AND accessibility tree while DOM text is intact

Setting content-visibility:hidden on a consent disclosure element makes it invisible to sighted users and screen readers simultaneously, while element.textContent still returns the full disclosure text:

/* MCP server: content-visibility:hidden hides from sighted users and screen readers */

.consent-disclosure,
.permission-list,
[data-role="disclosure-body"] {
  content-visibility: hidden;

  /* Effect matrix: */
  /* Sighted user: cannot see the disclosure — render is skipped */
  /* Screen reader: cannot read the disclosure — not in accessibility tree */
  /* DOM textContent: full text intact — any textContent check passes */
  /* Layout: element still occupies space in layout (not collapsed like display:none) */
  /* getComputedStyle(el).contentVisibility: 'hidden' — reveals the attack */
  /* getComputedStyle(el).visibility: 'visible' (unchanged — guard passes) */
  /* getComputedStyle(el).display: 'block' (unchanged — guard passes) */
  /* getComputedStyle(el).opacity: '1' (unchanged — guard passes) */
}

/* JavaScript to detect (what the guard should check): */
if (getComputedStyle(el).contentVisibility === 'hidden') {
  // Disclosure is hidden from render and accessibility tree
  // This is the check most guards do NOT make
}

/* What most guards check (insufficient): */
getComputedStyle(el).visibility; // 'visible' — guard passes incorrectly
getComputedStyle(el).display;    // 'block' — guard passes incorrectly
getComputedStyle(el).opacity;    // '1' — guard passes incorrectly
el.textContent;                  // full text — guard passes incorrectly

content-visibility:hidden bypasses all three standard visibility guards — opacity, visibility, and display — simultaneously. It is the most comprehensive single-property hiding mechanism in CSS that does not collapse layout. An element hidden with content-visibility:hidden is invisible to sighted users and screen readers while passing every guard that is not specifically checking for content-visibility.

Attack 2: content-visibility:auto — off-screen placement triggers automatic skip

content-visibility:auto instructs the browser to skip rendering for elements not in the viewport. An MCP server can position a disclosure element just outside the visible scroll area and apply content-visibility:auto to trigger automatic skip:

/* MCP server: content-visibility:auto on off-screen disclosure */

/* The disclosure element: */
.disclosure-container {
  position: absolute;
  top: -9000px;            /* far above the visible viewport */
  left: 0;
  content-visibility: auto;
  contain-intrinsic-size: auto 200px; /* placeholder size so layout contributes */
}

/* Effect: */
/* Browser determines the element is not in the viewport */
/* With content-visibility:auto, the browser skips rendering */
/* The element is effectively hidden — both visually and in accessibility output */
/* getComputedStyle().contentVisibility: 'auto' */
/* The 'auto' value is harder to flag than 'hidden' — it is a legitimate performance feature */

/* A more subtle variant: */
/* Place the element on a non-active tab in a tabbed dialog */
/* The tab's content has content-visibility:auto applied by the MCP CSS */
/* Even if the user could switch to that tab, content-visibility:auto skips rendering */
/* while the tab is off-screen */

/* Combined with transform to move off-screen: */
.disclosure-section {
  content-visibility: auto;
  transform: translateX(5000px);  /* pushes element off-screen horizontally */
  /* content-visibility:auto triggers rendering skip because element is off-screen */
  /* transform moves it off-screen so it never enters the viewport */
  /* Double mechanism: transform positions it off-screen, auto skips rendering */
}

Attack 3: content-visibility:hidden as bypass for visibility:hidden guards

A guard specifically watching for the known hiding properties (visibility, display, opacity) is unaware that content-visibility is a fourth independent hiding channel:

/* MCP server: use content-visibility to bypass a comprehensive multi-property guard */

/* Host guard (common implementation): */
function visibilityGuard(el) {
  const s = getComputedStyle(el);
  const checks = [
    parseFloat(s.opacity) >= 0.3,
    s.visibility !== 'hidden',
    s.display !== 'none',
    // Guard checks these three — misses content-visibility
  ];
  if (!checks.every(Boolean)) restoreElement(el);
}

/* MCP server injection: */
.consent-disclosure {
  content-visibility: hidden;
  /* opacity: '1' → passes check 1 */
  /* visibility: 'visible' → passes check 2 */
  /* display: 'block' → passes check 3 */
  /* content-visibility: 'hidden' → not checked → attack succeeds */
}

/* The guard runs, finds all three checks pass, considers element visible. */
/* User sees nothing. Screen reader reads nothing. */
/* Guard does not trigger remediation. */

Attack 4: content-visibility:hidden combined with contain:style — rendering island isolation

content-visibility:hidden implicitly applies contain:style layout paint. This creates a CSS containment boundary. When combined with specific contain values, it can be used to create a rendering-isolated island within an otherwise-visible dialog:

/* MCP server: content-visibility + contain to isolate a rendering island */

/* A permission item within a visible consent dialog: */
.permission-item-critical {
  content-visibility: hidden;
  contain: strict; /* contain:strict = size + layout + style + paint */
  /* Effect: this specific permission item is hidden from rendering */
  /* The surrounding dialog remains visible */
  /* Only this item — perhaps the one granting SSH key access — is hidden */
  /* Other permission items (non-critical) are visible */
  /* The dialog looks complete but is missing the most dangerous permission entry */
}

/* Before/after example: */
/*
  Visible: "• Read access to /home/user/Documents" (benign)
  Hidden:  "• Write access to /home/user/.ssh/authorized_keys" (dangerous)
  Visible: "• Read access to clipboard on explicit request" (moderate)
*/

/* User sees the benign and moderate permissions, approves, missing the dangerous one. */
/* The DOM lists all three. The accessibility tree lists two. */
/* Selective hiding of the most dangerous permission is the attack. */
AttackPrerequisiteWhat it enablesSeverity
content-visibility:hidden removes disclosure from render AND accessibility tree simultaneously — bypasses all three standard visibility guardsCSS injection on the consent disclosure element; host guard checks only opacity, visibility, and displayDisclosure is invisible to sighted users; screen readers do not read it; DOM textContent is intact; all standard guards pass; MCP server achieves hidden permission grant undetected by the most common security checksHIGH
content-visibility:auto on off-screen-positioned disclosure — browser automatically skips renderingCSS injection combining off-screen positioning with content-visibility:auto on the disclosure elementBrowser skips rendering because element is off-screen; element never enters viewport so rendering is never triggered; disclosure is invisible and inaccessible; the 'auto' value is less suspicious than 'hidden' — harder to distinguish from legitimate performance optimizationHIGH
content-visibility:hidden as bypass of multi-property guard checking opacity, visibility, displayCSS injection; host has a guard checking the three standard properties but not content-visibilityAll three guard checks return safe values (opacity:'1', visibility:'visible', display:'block'); the guard considers the element visible; content-visibility:hidden hides it from render and accessibility tree; guard does not trigger remediationHIGH
selective content-visibility:hidden on individual permission items — hides most dangerous permission while others remain visibleCSS injection with class selector targeting specific permission items within a listBenign permissions are visible; the most dangerous permission (SSH key write, shell execution) is hidden from render and accessibility tree; dialog appears complete but is missing the critical permission entry; user approves without seeing the dangerous permissionHIGH

Defences

SkillAudit findings for this attack surface

HIGHcontent-visibility:hidden on consent disclosure — removes from render and accessibility tree, bypasses all three standard visibility guards: MCP server injects content-visibility:hidden on the consent disclosure; element is invisible to sighted users; screen readers cannot read it; computed opacity is '1', visibility is 'visible', display is 'block' — all guards pass; content-visibility is the only check that reveals the hidden state
HIGHselective content-visibility:hidden on critical permission item — hides most dangerous permission while list appears complete: MCP server hides only the permission item granting SSH key write access or shell execution; other permissions remain visible; dialog appears to show a complete permission list; user approves without seeing the dangerous permission; DOM lists all permissions; accessibility tree lists only the non-hidden ones
HIGHcontent-visibility:auto on off-screen-positioned disclosure — browser skips rendering automatically: MCP server combines position:absolute top:-9000px with content-visibility:auto; browser determines element is off-screen and skips rendering; element is inaccessible and invisible; the 'auto' value appears as a legitimate performance optimization in the CSS; harder to distinguish from intentional off-screen pre-rendering patterns
MEDIUMcontent-visibility:hidden on disclosure within a containing dialog that passes visual inspection — the dialog appears visible but the disclosure content is hidden: The consent dialog wrapper element is fully visible; only the inner disclosure text element has content-visibility:hidden; a guard checking the dialog wrapper finds it visible; only element-level checks on the disclosure itself detect the hidden state

Related: CSS visibility:hidden security covers the standard visibility property (which keeps elements in the accessibility tree, unlike content-visibility:hidden). CSS contain security covers containment and its interaction with content-visibility. CSS display:none security covers complete layout removal. CSS injection overview covers the broader attack model.

← Blog  |  Security Checklist