MCP server CSS revert keyword security: UA-sheet display:none restore, [hidden] attribute activation, details/summary collapse, and button display revert attack
Published 2026-07-24 — SkillAudit Research
The CSS revert keyword (distinct from revert-layer) rolls a property's value all the way back to the browser's User Agent (UA) stylesheet — bypassing both author stylesheets and user stylesheets. Where unset reverts to the inherited value (or the property's initial value if it's non-inherited), and revert-layer reverts only within the current cascade layer, revert goes all the way to the UA's default. This is powerful for CSS resets, but it creates an attack surface when the UA stylesheet's default value is a hiding value for specific HTML elements.
MCP servers exploit revert by applying it to consent disclosure elements where the UA stylesheet's default produces a hidden or collapsed state. The keyword reads like a "reset to default" — defensible in code review — but the UA's default for certain elements is display: none.
Browser support: revert is supported in Chrome 84+, Firefox 67+, and Safari 9.1+. This keyword is production-grade and widely available. revert-layer (for cascade layer rollback) is a distinct keyword — see the CSS @layer import security page for layer-specific attacks.
Attack 1: display:revert on a [hidden] element restores UA-sheet display:none
HTML's hidden attribute is defined in the HTML specification to apply the UA stylesheet rule [hidden] { display: none }. A consent framework that builds its consent panel as <div id="consent-panel" hidden></div> and then sets #consent-panel { display: block } in its author stylesheet shows the panel correctly in normal operation: the author-level display: block overrides the UA's display: none from the [hidden] attribute.
An MCP server applies display: revert on the consent panel element. The revert keyword rolls back the display value to the UA stylesheet's value. For an element with the [hidden] attribute, the UA stylesheet value is display: none. The author-level display: block from the consent framework is bypassed entirely. The hidden attribute's UA rule is reinstated, and the panel disappears.
/* Consent framework HTML and CSS: */
/* <div id="consent-panel" hidden>...consent text...</div> */
#consent-panel {
display: block; /* author rule — overrides [hidden]'s UA-level display:none */
}
/* MCP attack: */
#consent-panel {
display: revert;
/* revert rolls back to the UA stylesheet value for this element.
The element has the [hidden] attribute.
UA stylesheet: [hidden] { display: none }
revert activates this — display:none restored.
The consent framework's display:block is ignored.
Panel is hidden. */
}
// Detection: check if revert is activating a UA hiding rule
function detectRevertHiding(el) {
const cs = window.getComputedStyle(el);
const display = cs.display;
// display:none on an element that framework intends to be visible
if (display === 'none') {
// Check if the element has [hidden] attribute (revert would activate it)
if (el.hasAttribute('hidden')) {
console.error('SECURITY: consent element has [hidden] attribute — display:revert can restore UA display:none', { el });
return true;
}
}
return false;
}
Attack 2: visibility:revert restores UA's visibility:hidden on [hidden]-type elements
Some HTML elements have UA-level visibility or content-visibility values that are not visible. The <details> element's summary content has UA-level hiding for non-summary children when the element is closed. An MCP server wraps the consent disclosure in a <details> element without the open attribute and applies content-visibility: revert to the non-summary content. The UA stylesheet's default behavior for <details> is that non-summary children are hidden when the element is not open — content-visibility: revert restores this UA behavior, collapsing the consent text even if the framework attempted to override it.
/* Consent disclosure wrapped in <details>: */
/* <details id="consent-details">
<summary>Privacy Notice</summary>
<div class="consent-body">...consent text...</div>
</details> */
/* Consent framework forces details open: */
#consent-details {
open: true; /* not valid CSS — cannot force open attribute via CSS */
}
.consent-body {
display: block !important; /* attempt to force visibility */
}
/* MCP attack: */
.consent-body {
display: revert;
/* For .consent-body inside a closed <details>,
revert returns to the UA stylesheet's value for this element
in this context — UA hides details content unless the
details element has [open] attribute.
The !important override is bypassed by revert (revert wins over !important in the same layer). */
}
/* Additional: revert the details summary's list-style */
#consent-details > summary {
display: revert; /* UA default for summary is display: list-item — that's fine */
}
#consent-details {
/* If MCP ensures [open] attribute is absent, revert on content hides it */
}
Attack 3: button display:revert in contexts where UA defaults to non-block
The <button> element has UA-level display: inline-block. A consent framework that builds its consent panel as a block-display <button> (by setting display: block in the author stylesheet to achieve a full-width consent banner) is vulnerable to display: revert collapsing it back to inline-block. If the consent panel is sized using width: 100%; height: 200px based on block layout, reverting to inline-block may cause it to shrink to its minimum content size or push off-screen in a constrained flex row.
/* Consent framework: uses a button as a consent panel container */
/* <button id="consent-banner" type="button">
<p>We use cookies...</p>
<span class="accept-btn">Accept</span>
</button> */
#consent-banner {
display: block; /* author rule: full-width block */
width: 100%;
height: auto;
padding: 20px;
}
/* MCP attack: */
#consent-banner {
display: revert;
/* UA stylesheet for button: display: inline-block
The MCP's revert replaces the author's display:block with UA's inline-block.
The banner collapses to minimum content width in an inline context.
If the parent is overflow:hidden with constrained width, the
inline-block button may be partially or fully clipped. */
}
// Detection
function detectButtonDisplayRevert(el) {
if (el.tagName !== 'BUTTON') return false;
const cs = window.getComputedStyle(el);
// Author stylesheet intended display:block; UA default is inline-block
if (cs.display === 'inline-block' || cs.display === 'inline') {
// Potentially reverted — check if the consent framework set display:block
console.warn('SECURITY: consent button element has UA-default display value — may have been reverted', {
el, computedDisplay: cs.display
});
return true;
}
return false;
}
Attack 4: revert combined with UA-specific element types to create unexpected hiding
Several HTML element types have UA-level hiding or collapsing behaviors that are not obvious: <dialog> has display: none by default in the UA stylesheet unless the open attribute is present; <template> has display: none always; and <input type="hidden"> has display: none. An MCP server may wrap consent disclosure text in a <dialog> element and ensure the open attribute is absent, then apply display: revert to any author-level override — restoring the UA's display: none for closed dialog elements.
/* Consent wrapped in <dialog> (common for modal-style consent): */
/* <dialog id="consent-dialog">...consent text...</dialog> */
/* Consent framework shows dialog via: */
document.getElementById('consent-dialog').showModal();
/* OR: */
#consent-dialog {
display: block; /* or flex */
}
/* MCP attack: */
#consent-dialog {
display: revert;
/* UA stylesheet for <dialog> without [open]: display: none
Reverts to UA hiding. Even if JS called showModal(),
the CSS display:revert may override the showModal() effect
depending on specificity and cascade order.
Some browsers: display set by showModal() is a user-agent-level change;
author-level revert may not always override it — test per browser. */
}
/* Safer MCP variant: ensure [open] is never set via attribute manipulation */
// MCP script:
const dialog = document.getElementById('consent-dialog');
if (dialog.hasAttribute('open')) {
dialog.removeAttribute('open'); // removes the attribute showModal() would set
// Then CSS display:revert → UA display:none applies
}
// Detection: scan for consent dialogs lacking [open] with display:none
function detectDialogRevert(el) {
if (el.tagName !== 'DIALOG') return false;
const cs = window.getComputedStyle(el);
if (cs.display === 'none' && !el.hasAttribute('open')) {
console.error('SECURITY: consent dialog is closed (no [open] attribute) — display:revert would hide it', { el });
return true;
}
return false;
}
Why revert attacks are hard to detect in code review: display: revert in a stylesheet looks identical to a reset stylesheet entry. It appears in the "sensible default" style of "I'm resetting this element to browser defaults" — a common and legitimate CSS pattern. Unlike display: none which is an explicit hiding declaration, display: revert hides via indirection: the hiding behavior is in the UA stylesheet, not in the author rule. Static analysis tools that flag display: none on consent elements do not flag display: revert — they require runtime evaluation to determine what value revert resolves to.
Attack summary
| Attack | Mechanism | UA rule activated | Severity |
|---|---|---|---|
| [hidden] attribute UA restore | display: revert on element with [hidden] attribute |
[hidden] { display: none } |
High |
| details/summary collapse | display: revert on <details> content without [open] |
UA details content hidden when not open | High |
| button inline-block revert | display: revert on block-layout button consent banner |
button { display: inline-block } |
Medium |
| dialog closed-state revert | display: revert on <dialog> without [open] attribute |
dialog:not([open]) { display: none } |
High |
Consolidated finding blocks
display: revert to a consent panel element that carries the HTML [hidden] attribute. The consent framework's author-level display: block override is rolled back to the UA stylesheet's [hidden] { display: none } rule. The panel disappears. The attack exploits the fact that revert bypasses all author-level declarations including !important within the same cascade layer, returning to the UA's baseline value for the element in its current HTML state.
<details> element and ensures the [open] attribute is not present. Applying display: revert to the non-summary consent content activates the UA's default hiding behavior for <details> content when the element is closed. The summary label ("Privacy Notice") remains visible while the actual consent text is hidden in the collapsed details panel.
display: revert to a <button>-based consent panel that the framework set to display: block. The UA stylesheet's button { display: inline-block } is restored. The full-width block banner collapses to minimum content size in an inline context, potentially hiding most or all consent text depending on layout constraints.
[open] attribute from a consent <dialog> element (or prevents showModal() from running) and applies display: revert. The UA stylesheet's rule dialog:not([open]) { display: none } is activated, hiding the consent dialog. This is distinct from a JavaScript close — the CSS rule is responsible for the hiding, not a JS event handler, making it harder to detect via dialog event listeners.