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. */
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| content-visibility:hidden removes disclosure from render AND accessibility tree simultaneously — bypasses all three standard visibility guards | CSS injection on the consent disclosure element; host guard checks only opacity, visibility, and display | Disclosure 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 checks | HIGH |
| content-visibility:auto on off-screen-positioned disclosure — browser automatically skips rendering | CSS injection combining off-screen positioning with content-visibility:auto on the disclosure element | Browser 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 optimization | HIGH |
| content-visibility:hidden as bypass of multi-property guard checking opacity, visibility, display | CSS injection; host has a guard checking the three standard properties but not content-visibility | All 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 remediation | HIGH |
| selective content-visibility:hidden on individual permission items — hides most dangerous permission while others remain visible | CSS injection with class selector targeting specific permission items within a list | Benign 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 permission | HIGH |
Defences
- Add
content-visibilityto every computed style guard.getComputedStyle(el).contentVisibilityreturns'hidden','auto', or'visible'. Any value other than'visible'on a security-critical element should be flagged. - Check
element.checkVisibility()(Chrome 105+, Firefox 124+). ThecheckVisibility()method returnsfalseif the element or any ancestor hascontent-visibility:hiddenor is off-screen withcontent-visibility:auto. This is the most comprehensive single-call check:if (!el.checkVisibility({ checkVisibilityCSS: true })) { warn(); }. - Monitor for
content-visibilitymutations. Observe style attribute mutations on all elements in the consent dialog subtree. Flag anycontent-visibilityvalue appearing in a style attribute or injected<style>block. - CSP
style-srcwith nonce. Prevents the injection vector entirely, which is the most robust defense against all CSS-based attacks includingcontent-visibility. - SkillAudit flags:
content-visibility:hiddenon any security-critical element;content-visibility:autocombined with off-screen positioning;element.checkVisibility()returning false on any disclosure element.
SkillAudit findings for this attack surface
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.