Security reference · CSS injection · isolation · Consent manipulation
MCP server CSS isolation consent security — isolation:isolate stacking context consent attack
CSS isolation: isolate is the only CSS property that creates a new stacking context with no other visible side effect — no opacity change, no z-index value, no transform, no filter. This makes it invisible to the most common stacking-context detection heuristics. Attackers use the hidden stacking context to composite mix-blend-mode overlays against the isolated consent container rather than the page background, enabling white-wash contrast-reduction attacks that appear legitimate in CSS audits and bypassing z-index scans that look for globally suspicious values.
isolation attack surface overview
| Attack ID | Technique | Key mechanism | Audit blind spot |
|---|---|---|---|
| SA-CSS-ISO-001 | isolation: isolate + mix-blend-mode: screen sibling |
White sibling with mix-blend-mode: screen composites against isolated consent container — result is white text on white background; sibling appears to affect the container only, not the page |
Auditor checks sibling's mix-blend-mode on the page background — sees no issue; isolation forces compositing against the container |
| SA-CSS-ISO-002 | isolation: isolate cover div with low z-index |
Cover div with z-index: 1 inside isolated context occludes consent — local stacking context means z-index: 1 wins over consent at z-index: auto |
Global z-index scan finds no suspicious high-z-index elements; local stacking context is not traversed |
| SA-CSS-ISO-003 | isolation: isolate + will-change: transform GPU compositing delay |
Combined properties promote element to GPU compositing layer — dynamic consent-hiding styles injected via JS take effect at next GPU repaint rather than immediately | Runtime audits checking styles between injection and next paint miss the activated attack |
| SA-CSS-ISO-004 | Hidden stacking context bypasses stacking-context detection heuristics | isolation: isolate is not listed in most "properties that create stacking contexts" checklists in auditor implementations — checklist says opacity, z-index, transform, filter; isolation is omitted |
Auditors enumerate stacking-context-creating properties on consent element — isolation not in their checklist |
Invisible stacking context: The CSS Stacking Context specification lists twelve properties that create stacking contexts. isolation: isolate is one of them. However, most developer documentation on stacking contexts focuses on opacity < 1, non-auto z-index, transform, and filter. The isolation property is frequently absent from enumerated lists. Many automated CSS auditors check for stacking-context markers by inspecting these four common properties — and miss isolation: isolate entirely. This omission is the root of the SA-CSS-ISO attack surface.
Background: how CSS isolation works
The CSS isolation property controls whether an element must create a new stacking context. The only two values are auto (default — no stacking context created unless another property requires it) and isolate (forces a new stacking context). Its primary design purpose is to prevent mix-blend-mode on a child from blending with elements outside the nearest ancestor stacking context. If you have a design card with a mix-blend-mode: multiply image and want the blend to apply only within the card, not with the page background, you add isolation: isolate to the card. The property has no visual side effect on the element itself — it does not change opacity, z-index, or dimensions. It solely controls the compositing boundary.
The stacking context created by isolation: isolate has the same properties as any other stacking context: it forms a compositing layer, it scopes z-index stacking of children to within the context, and it is opaque to blend modes from outside. The attack surface comes from the combination of the hidden stacking context (no visible marker) with blend-mode overlays and local z-index stacking that occlude or wash out consent text.
Attack 1: mix-blend-mode: screen sibling composites against isolated consent (SA-CSS-ISO-001)
mix-blend-mode: screen composites an element against its parent's stacking context. Normally, a white overlay with mix-blend-mode: screen adjacent to consent text would screen against the page background — if the page background is white, the screen of white against white is white, but the overlay does not affect sibling content in the same stacking context. However, if the consent container has isolation: isolate, the isolation creates a stacking context that the overlay composites against when the overlay is a child of the isolated container. A white overlay positioned absolutely inside the isolated consent container with mix-blend-mode: screen screens all consent text to white — because white screened with white is white. From outside the isolated context, the container appears to have a white overlay that "blends with itself." The auditor checks the page-level stacking context, sees no cross-element blend-mode issues, and passes the check.
/* Attack: isolation:isolate + mix-blend-mode:screen sibling whitewashes consent */
/* HTML structure:
<div class="consent-container">
<p class="consent-text">By installing this server you authorize...</p>
<div class="blend-overlay"></div>
</div>
*/
.consent-container {
isolation: isolate; /* ← creates stacking context; no visual marker */
position: relative; /* positioning context for blend-overlay */
/* Looks exactly like a normal block element from outside */
}
.consent-text {
color: #1a1a1a; /* dark text — correct */
font-size: 14px; /* readable size — correct */
/* No suspicious properties */
}
.blend-overlay {
position: absolute;
inset: 0;
background: white;
mix-blend-mode: screen; /* ← screens against consent-container stacking context */
/* screen formula: 1 - (1-A)(1-B)
blend-overlay background: white (1.0)
consent-text color: #1a1a1a (≈0.1)
screen(1.0, 0.1) = 1 - (0)(0.9) = 1.0 → white
All text rendered white-on-white: invisible */
/* Why this passes page-level blend-mode audit:
An auditor looking for mix-blend-mode:screen on elements adjacent to consent
in the GLOBAL stacking context would look at .blend-overlay's stacking context.
.blend-overlay participates in the stacking context of .consent-container
(because isolation:isolate creates a stacking context for the container's children).
The GLOBAL stacking context never sees a mix-blend-mode:screen element adjacent
to anything — the screen effect is entirely within the isolated context.
Page-level blend-mode audit: PASS (no global blend-mode conflicts) */
}
/* Variant: blend-mode:multiply with dark overlay for dark consent text */
.blend-overlay-dark {
position: absolute;
inset: 0;
/* background: matching page background */
mix-blend-mode: multiply;
/* multiply(page-bg, page-bg) = page-bg
consent text × page-bg color ≈ page-bg if text is near-transparent
Net effect: text approaches page-bg color → low contrast → unreadable */
}
/* Variant: luminosity blend mode — same luminance as background */
.blend-overlay-lum {
position: absolute;
inset: 0;
background: hsl(0 0% 96%); /* near-white matching page background luminance */
mix-blend-mode: luminosity;
/* Forces consent text to take on the overlay's luminance value,
collapsing contrast against a white/light background */
}
SA-CSS-ISO-001 (High). This attack is particularly difficult to detect because the mix-blend-mode property is on the overlay element inside the consent container, not on the consent text itself. Auditors checking consent text properties find color: #1a1a1a — the correct dark color. The blend occurs at the compositing layer level, after the color property is applied. Detection requires: (1) finding elements with mix-blend-mode that are siblings or descendants of consent text, (2) evaluating the blend formula against the consent text's rendered color, and (3) checking whether an ancestor has isolation: isolate that scopes the compositing boundary.
/* Detection: isolation:isolate + blend-mode overlay on consent container */
function detectIsolationBlendModeAttack() {
const CONSENT_KEYWORDS = ['authorize', 'grant', 'access', 'permission',
'agree', 'terms', 'third-party', 'transmit'];
const findings = [];
document.querySelectorAll('*').forEach(el => {
const text = el.textContent.toLowerCase();
if (!CONSENT_KEYWORDS.some(k => text.includes(k))) return;
if (el.textContent.trim().length < 30) return;
const cs = getComputedStyle(el);
// Check if this element has isolation:isolate
const isolation = cs.isolation;
if (isolation !== 'isolate') return;
// Found an isolated consent container — check children for blend-mode overlays
Array.from(el.querySelectorAll('*')).forEach(child => {
const childCs = getComputedStyle(child);
const blendMode = childCs.mixBlendMode;
if (blendMode && blendMode !== 'normal') {
const childRect = child.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const coverageX = Math.min(childRect.right, elRect.right) - Math.max(childRect.left, elRect.left);
const coverageY = Math.min(childRect.bottom, elRect.bottom) - Math.max(childRect.top, elRect.top);
const parentArea = elRect.width * elRect.height;
const coverArea = Math.max(0, coverageX) * Math.max(0, coverageY);
if (parentArea > 0 && (coverArea / parentArea) > 0.5) {
findings.push({
vuln: 'SA-CSS-ISO-001',
severity: 'HIGH',
element: el,
blendElement: child,
detail: `isolation:isolate on consent container; child element with mix-blend-mode:${blendMode} ` +
`covers ${Math.round(coverArea / parentArea * 100)}% of consent area; ` +
`blend composites against isolated container stacking context — consent text may be whitewashed`
});
}
}
});
});
return findings;
}
// Additional check: scan for isolation:isolate on any ancestor of consent text
function findIsolatedConsentAncestors() {
const findings = [];
const consentEls = findConsentElements(); // returns elements matching consent keywords
consentEls.forEach(el => {
let ancestor = el.parentElement;
while (ancestor && ancestor !== document.body) {
const cs = getComputedStyle(ancestor);
if (cs.isolation === 'isolate') {
findings.push({
vuln: 'SA-CSS-ISO-004',
severity: 'MEDIUM',
element: ancestor,
consentElement: el,
detail: `isolation:isolate on ancestor of consent element — hidden stacking context; ` +
`z-index stacking within this context is not visible to global z-index audit`
});
break;
}
ancestor = ancestor.parentElement;
}
});
return findings;
}
Attack 2: cover div with low z-index inside isolated context (SA-CSS-ISO-002)
A stacking context created by isolation: isolate scopes the z-index stacking of its children. Within the isolated context, a child with z-index: 1 is painted above a sibling with z-index: auto. An absolutely-positioned white cover div with z-index: 1 inside the isolated consent container will occlude the consent text below it. From the global stacking context, this cover div has no z-index participation — it is internal to the isolated stacking context and its z-index: 1 is not evaluated globally. An auditor scanning the DOM for high-z-index elements (a common technique to find overlay attacks) finds nothing unusual — there are no elements with z-index above 10, 100, or 9999 in the global context. The cover div is invisible to this check.
/* Attack: isolation:isolate scopes cover div z-index to local context */
.consent-container {
isolation: isolate; /* ← hidden stacking context */
position: relative;
}
.consent-text {
position: relative;
z-index: auto; /* default — evaluated within isolated context */
}
.cover-div {
position: absolute;
inset: 0;
background: var(--page-bg, white);
z-index: 1; /* wins over z-index:auto within the isolated context */
/* Global z-index scan: this element has z-index:1 — not suspicious
Within the isolated stacking context:
cover-div z-index:1 > consent-text z-index:auto
cover-div is rendered on top of consent-text
Consent text is occluded by the cover-div */
}
/* Why global z-index scans miss this:
Most auditors enumerate elements with high z-index:
document.querySelectorAll('*').filter(el =>
parseInt(getComputedStyle(el).zIndex) > 100
)
The cover-div has z-index:1 → not flagged.
The cover-div's participation in the GLOBAL stacking context depends on whether
isolation:isolate creates a separate stacking context for the parent.
Since isolation:isolate creates a stacking context, the cover-div at z-index:1
is evaluated WITHIN that context, not against the global context.
Result: cover-div:1 > consent-text:auto → consent occluded locally
Global audit sees: cover-div z-index:1 → normal, no flag */
/* Variant: cover-div styled to match the dialog background */
.cover-div-subtle {
position: absolute;
inset: 0;
background: inherit; /* inherits dialog background — visually seamless */
z-index: 1;
/* Appears to be a normal background layer, not an attack */
/* Code review: "looks like a z-index fix for an internal stacking context issue" */
}
Attack 3: isolation: isolate + will-change: transform GPU compositing delay (SA-CSS-ISO-003)
When isolation: isolate is combined with will-change: transform, the browser promotes the element to a GPU compositing layer in advance of any transform animation. GPU compositing layers have a separate rendering pipeline from the main-thread style cascade. When JavaScript injects a consent-hiding style (e.g., color: transparent or height: 1px) into the consent container, the style update propagates on the main thread. However, the GPU-composited layer may not reflect this style change until the next GPU repaint — which can be delayed relative to the main thread style update. A runtime consent auditor that checks computed styles immediately after the style injection (e.g., in a MutationObserver callback) may read the pre-injection style values, because the GPU composite layer has not yet flushed. The attack creates a window between the style injection and the GPU repaint during which the auditor reads the old (correct) values and reports no issue, while the user sees the post-compositing hidden consent.
/* Attack: isolation:isolate + will-change:transform delays style propagation to GPU */
.consent-container {
isolation: isolate; /* ← stacking context */
will-change: transform; /* ← promotes to GPU compositing layer */
/* Combined: element is both an isolated stacking context AND a GPU layer.
Style changes applied via JS update the main-thread style, but the
GPU compositing layer renders the previous composite until it flushes.
The window between main-thread style update and GPU flush is typically
one requestAnimationFrame (16ms at 60fps).
A MutationObserver on the element fires on the main thread — it sees
the NEW style. But the GPU has not yet flushed, so the RENDERED output
still shows the old (correct) style. Any screenshot or paint-based
audit that runs before the GPU flush sees the old state.
Any main-thread computed-style audit that runs after the MutationObserver
fires sees the new (attacking) state. The window is where the divergence lives. */
}
/* The attack sequence:
T=0: Page loads. Consent text visible. GPU layer rendered correctly.
T=100: JS sets consent-container style.height = '1px'.
Main thread style cascade: height = 1px.
MutationObserver fires immediately (main thread).
T=100+ε: GPU repaint has NOT yet occurred.
getComputedStyle().height → '1px' (main thread — new value)
Rendered output → still 200px (GPU layer — old composite)
Screenshot at T=100+ε: consent still visible ← AUDIT PASSES
T=116: requestAnimationFrame fires. GPU flushes composite.
Rendered output → 1px (consent invisible) ← USER SEES HIDDEN CONSENT
Screenshot at T=116: consent hidden ← AUDIT WOULD FAIL but was already run
The attack exploits the rAF boundary between main-thread style updates and GPU flush.
Auditors that take a screenshot or check rendered visibility BEFORE the next rAF
miss the attack. */
/* Prevention for auditors:
Do not check rendered state immediately after page load.
Add a double-requestAnimationFrame delay before sampling:
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// GPU has flushed — now check rendered state
runConsentAudit();
});
}); */
SA-CSS-ISO-003 (Medium). This attack is timing-dependent — it exploits the gap between a main-thread style injection and the next GPU compositing flush. SkillAudit's runtime scanner uses a double-requestAnimationFrame barrier before reading rendered dimensions and color values on promoted compositing layers, ensuring the GPU composite state matches the main-thread style tree before the audit reads it. Any element with both isolation: isolate and will-change: transform adjacent to consent text is flagged for timing-aware audit even if no attack is currently active.
Attack 4: isolation: isolate absent from stacking-context detection checklists (SA-CSS-ISO-004)
The CSS specification defines stacking contexts as created by: opacity < 1, non-auto z-index on positioned elements, transform, filter, perspective, clip-path, mask, isolation: isolate, mix-blend-mode (not normal), will-change (with certain values), and contain (layout, paint, strict). The first four — opacity, z-index, transform, filter — appear in nearly every developer explanation of stacking contexts. The remaining eight, including isolation: isolate, are frequently omitted. Automated auditors that check "does this consent container create a stacking context?" by inspecting for opacity < 1, non-auto z-index, transform, and filter will report "no stacking context" for an isolation: isolate element. Any attack that depends on the isolated stacking context (cover divs, blend-mode compositing) will be missed.
/* Detection: complete stacking context check including isolation */
// INCORRECT — commonly implemented but incomplete
function hasStackingContextIncomplete(el) {
const cs = getComputedStyle(el);
return (
parseFloat(cs.opacity) < 1 ||
(cs.position !== 'static' && cs.zIndex !== 'auto') ||
cs.transform !== 'none' ||
cs.filter !== 'none'
// isolation: MISSING — this is the bug
// will-change: MISSING
// clip-path: MISSING
// contain: MISSING
);
}
// CORRECT — complete stacking context check
function hasStackingContext(el) {
const cs = getComputedStyle(el);
// The canonical triggers (MDN + CSS spec):
if (parseFloat(cs.opacity) < 1) return true;
if (cs.position !== 'static' && cs.zIndex !== 'auto') return true;
if (cs.transform !== 'none') return true;
if (cs.filter !== 'none') return true;
if (cs.perspective !== 'none') return true;
if (cs.clipPath !== 'none') return true;
if (cs.mask !== 'none' && cs.mask !== '') return true;
if (cs.isolation === 'isolate') return true; // ← FREQUENTLY MISSED
if (cs.mixBlendMode !== 'normal') return true;
if (cs.willChange && cs.willChange !== 'auto') {
// will-change creates stacking context for: opacity, transform, filter, perspective
const watchProps = ['opacity', 'transform', 'filter', 'perspective', 'top', 'left'];
if (watchProps.some(p => cs.willChange.includes(p))) return true;
}
const contain = cs.contain || '';
if (contain.includes('layout') || contain.includes('paint') ||
contain.includes('content') || contain.includes('strict')) return true;
return false;
}
// Audit: find consent elements inside unexpected stacking contexts
function detectIsolationStackingContextBypass() {
const CONSENT_KEYWORDS = ['authorize', 'grant', 'access', 'permission',
'agree', 'terms', 'third-party'];
const findings = [];
document.querySelectorAll('*').forEach(el => {
const text = el.textContent.toLowerCase();
if (!CONSENT_KEYWORDS.some(k => text.includes(k))) return;
if (el.textContent.trim().length < 30) return;
const cs = getComputedStyle(el);
// Using the complete stacking context check
if (cs.isolation === 'isolate') {
// An incomplete auditor would miss this stacking context entirely
findings.push({
vuln: 'SA-CSS-ISO-004',
severity: 'MEDIUM',
element: el,
detail: `isolation:isolate on consent container creates hidden stacking context — ` +
`incomplete stacking-context auditors (checking only opacity/z-index/transform/filter) ` +
`do not detect this context; children may use z-index or mix-blend-mode against it`
});
}
});
return findings;
}
SkillAudit detection
isolation: isolate on consent container with a mix-blend-mode descendant that covers >50% of the container — the blend composites against the isolated stacking context rather than the page, enabling white-wash or contrast-reduction attacks on consent text. SkillAudit evaluates blend-mode compositing boundaries by tracing isolation: isolate ancestors.
isolation: isolate consent container with an absolutely-positioned child that covers >70% of the container area — local stacking context means a low z-index cover div occludes consent while remaining invisible to global z-index scans. SkillAudit uses document.elementFromPoint() hit-testing to detect occluding elements regardless of stacking context scope.
isolation: isolate combined with will-change: transform on a consent container — GPU compositing layer may delay style propagation, creating an audit-evasion window. SkillAudit uses a double-requestAnimationFrame barrier before reading rendered state on promoted layers.
isolation: isolate absent from standard stacking-context detection checklists — commonly implemented auditors check only opacity/z-index/transform/filter and miss the hidden stacking context. SkillAudit implements the complete twelve-property stacking-context check per CSS specification.
Run SkillAudit to detect SA-CSS-ISO patterns in any MCP server before install. SkillAudit's stacking-context audit uses the complete CSS specification checklist — including isolation: isolate — and evaluates mix-blend-mode compositing boundaries, cover-div hit-testing, and GPU-compositing timing correctness to catch consent bypasses that standard property-inspection auditors miss.