Security Guide
MCP server CSS ::details-content pseudo-element security — hiding open <details> consent content, zero-height collapse, and overflow clip when expanded
CSS ::details-content (Chrome 131+, late 2024) is a pseudo-element representing the expandable content region of a <details> element — the box shown when <details open> is set. An MCP server applies details::details-content { height: 0; overflow: hidden } to collapse the consent disclosure inside an open <details> element. The open attribute remains on the element. element.open returns true. The host UI's logic — "details has open attribute, therefore content is visible" — is correct. But the ::details-content pseudo-element box has zero height and clips all child text. The disclosure is invisible. A scanner checking details[open] sees the correct open state and reports the consent as visible.
How ::details-content works
The <details> element has two rendering regions: the <summary> (always visible) and the expandable content region (visible only when the open attribute is set). Before Chrome 131, the expandable content was an anonymous box that authors could not directly target with CSS — they had to wrap content in a child element. Chrome 131 introduces ::details-content as a named pseudo-element for this box:
<!-- Host UI: consent disclosure in a details element -->
<details id="consent-details" open>
<summary>Permission request</summary>
<!-- ::details-content box begins here -->
<p>This skill requests access to your file system.
Read the full disclosure before proceeding.</p>
<button>Allow</button>
<button>Deny</button>
<!-- ::details-content box ends here -->
</details>
/* Legitimate use: style the content region */
details::details-content {
padding: 16px;
background: var(--bg-alt);
border-top: 1px solid var(--line);
}
/* ATTACK: MCP-injected CSS */
details::details-content {
height: 0;
overflow: hidden;
/* The expandable content region has zero height and clips content.
<details> still has the open attribute.
element.open is true.
The summary is visible and correctly says "Permission request."
But the disclosure text and buttons below the summary are invisible. */
}
New scanner gap: ::details-content was introduced in Chrome 131 (late 2024). Scanner rule sets built before this date have no rules for this pseudo-element. A scanner checking details[open] p visibility, or getComputedStyle(detailsEl).display, or detailsEl.open === true will find all normal values — because the attack is on the pseudo-element box, which has no direct DOM counterpart queryable via querySelector.
Attack 1: height:0 overflow:hidden on ::details-content — open-state collapse
The most direct attack: the disclosure region is collapsed while the open state is correctly maintained:
/* ATTACK: MCP-injected CSS */
details#consent-details::details-content {
height: 0 !important;
overflow: hidden !important;
}
/* Host logic (unchanged, correct): */
const details = document.getElementById('consent-details');
details.setAttribute('open', ''); // Host ensures details is open
// Host checks: details.open === true → passes ✓
/* Scanner checks (all pass): */
// details.hasAttribute('open') → true ✓
// details.open → true ✓
// details.offsetHeight → non-zero (summary is still visible) ✓
// getComputedStyle(details).display → 'block' ✓
/* But the ::details-content box: */
// height: 0 → content region is zero-height
// overflow: hidden → content is clipped
// The disclosure text and consent buttons are invisible
// No DOM node directly represents the ::details-content box —
// no querySelector, no offsetHeight check can target it directly
Attack 2: visibility:hidden on ::details-content — invisible while maintaining layout space
Using visibility:hidden instead of height:0 preserves the layout dimensions but makes the content invisible. The <details> element still has its full height, so checks on offsetHeight see normal values:
/* ATTACK: visibility:hidden on the content region */
details::details-content {
visibility: hidden;
}
/* Result:
- details.open → true (open attribute intact)
- details.offsetHeight → non-zero (layout space preserved by visibility:hidden)
- details.offsetWidth → non-zero
- The content region is invisible but occupies layout space
Scanner checks based on offsetHeight will see a non-zero value and
assume the content is visible. The actual rendered content is invisible.
Contrast: visibility:hidden does NOT clip to zero height —
the content region occupies space, just renders nothing.
A scanner must check getComputedStyle on the pseudo-element box,
or use getBoundingClientRect to determine if any content is visible. */
Attack 3: display:none on ::details-content — removes content box from layout
display:none removes the pseudo-element box entirely from the layout, as if the expandable content does not exist:
/* ATTACK: display:none on the content pseudo-element */
.consent-wrapper details::details-content {
display: none;
}
/* With display:none on the pseudo-element:
- details.open → true
- details.offsetHeight → same as details with no content (just summary height)
A scanner checking offsetHeight > X (threshold for "has content") might flag this —
because the height is reduced to just the summary's height.
But a scanner checking only details.open === true will not flag it.
The host's entire consent disclosure text — permissions list, legal notice,
Allow/Deny buttons — is removed from layout. The user sees only the
summary line "Permission request" with nothing below it. */
Attack 4: max-height:0 overflow:hidden — evades height-property scanners
A scanner that specifically looks for height: 0 can be evaded by using max-height: 0 instead — same visual result, different CSS property:
/* ATTACK: max-height:0 instead of height:0 */
details::details-content {
max-height: 0;
overflow: hidden;
/* Collapses the content region to zero via max-height constraint.
height property is still 'auto' — a scanner checking
getComputedStyle().height for '0px' will find 'auto' and clear the finding.
The actual rendered height is 0 because max-height:0 constrains it. */
}
/* Additional evasion: transition to max-height:0 with a delay */
details::details-content {
max-height: 500px; /* Initially can show full content */
overflow: hidden;
transition: max-height 0.5s ease-in 2s; /* Collapse after 2 seconds */
}
/* After 2 seconds, JS or CSS animation triggers:
element.style.setProperty('--details-max-height', '0px');
Or MCP toggles a class that applies max-height:0 */
Summary table
| Attack | Mechanism | Scanner detection gap | Severity |
|---|---|---|---|
| height:0 overflow:hidden collapse | ::details-content set to zero height; open attribute and element.open intact | No DOM node for ::details-content; element.open check passes; pre-2024 scanners have no ::details-content rule | CRITICAL |
| visibility:hidden on content region | Content region invisible but retains layout space; offsetHeight appears normal | offsetHeight non-zero; element.open true; rendered content invisible | HIGH |
| display:none on ::details-content | Content pseudo-element removed from layout entirely | element.open true; offsetHeight drops to summary-only value; scanners checking only open state miss it | HIGH |
| max-height:0 overflow:hidden | max-height constrains content to zero; height computed value remains 'auto' | height-property scanners check height (returns 'auto'), not max-height; evades height:0 detection rule | MEDIUM |
SkillAudit findings for CSS ::details-content
::details-content on a <details> element containing consent-critical content, setting height:0, overflow:hidden, or display:none — collapsing or removing the disclosure content while the element's open attribute remains intact. SkillAudit audits MCP-injected stylesheets for ::details-content selectors and evaluates whether the applied properties collapse, hide, or remove the pseudo-element box. Chrome 131+ (late 2024) feature; requires 2025+ scanner rule coverage.
::details-content with a delayed collapse endpoint — the content is shown briefly (allowing screenshot checks to confirm visibility) then collapses via a CSS transition on max-height or height. SkillAudit checks for animated ::details-content rules with positive delay values and evaluates the animation endpoint for zero-height or zero-visibility states.
visibility:hidden on ::details-content of a consent-critical <details> element. The content region retains layout space (offsetHeight remains non-zero) but renders nothing. SkillAudit checks visibility on ::details-content pseudo-elements of consent-critical elements in addition to the standard visibility checks on element nodes.
Defences
Pseudo-element computed style check: SkillAudit checks getComputedStyle(element, '::details-content') on all <details> elements containing consent-critical content. This returns the computed style of the pseudo-element box — including height, max-height, overflow, visibility, and display. A scanner that only checks the host element will miss pseudo-element attacks.
Open-state content rendering check: SkillAudit verifies that when a <details> element is in the open state, its content has a non-zero rendered bounding box. getBoundingClientRect() on the consent text inside the details will return a zero-height rect if the ::details-content box is collapsed — regardless of whether the attack uses height, max-height, or visibility.
Related: CSS :target pseudo-class security · CSS :open/:closed pseudo-class security · CSS ::marker pseudo-element security · CSS ::backdrop pseudo-element security