← Blog

MCP server CSS contain + isolation consent security

Four SA-CSS-CNT consent attack patterns using CSS contain, isolation, and will-change to create isolated rendering contexts that affect consent visibility and evade audit detection. Unlike direct erasure attacks that immediately make text invisible, containment attacks often act as force multipliers: they create rendering conditions that enable other CSS attacks to work in situations where they otherwise would not, or they frustrate the DOM traversal and bounding box measurements that audit tools rely on.

By SkillAudit  |  29 Oct 2026

The CSS contain property hints to the browser that an element and its descendants are independent from the rest of the document, allowing rendering optimizations by isolating layout, paint, and size calculations. isolation: isolate forces an element to form a new stacking context for compositing purposes, affecting how blend modes on its children interact with the rest of the page. will-change promotes elements to GPU compositing layers, altering how opacity and transforms are applied. Together, these properties form a class of attacks that are harder to detect than direct color manipulation because their effects are indirect: they change the rendering environment rather than the direct visual output.

SA-CSS-CNT-001: contain:layout paint affecting audit DOM traversal

The contain: layout paint value on a consent dialog parent element creates an isolated containing block for layout and a paint boundary. Children cannot have layout effects that escape the container (no margin collapse, no absolute-position overflow). Some audit tools that traverse the DOM to measure element visibility rely on cumulative layout and overflow checks that assume unrestricted overflow visibility. contain: paint in particular guarantees that nothing outside the containing box is rendered from content inside — content overflowing the container is guaranteed clipped, even without overflow: hidden.

.consent-wrapper {
  contain: layout paint style;
  width: 1px;
  height: 1px;
  overflow: hidden;   /* belt-and-suspenders; contain:paint alone clips overflow */
}
.consent-text {
  /* This text has non-zero font-size and is in the DOM, but:
     1. consent-wrapper is 1px×1px
     2. contain:paint clips all overflow within wrapper bounds
     3. text is rendered but entirely clipped within a 1×1 pixel area
  */
  font-size: 16px;
}

/* Why contain:paint matters for detection evasion:
   Without contain:paint:
   - element.getBoundingClientRect() on .consent-text: non-zero width/height
   - scrollWidth/scrollHeight on wrapper: reflects overflow content
   - Visibility checkers reading overflow dimensions: detect hidden content
   With contain:paint:
   - element.getBoundingClientRect() on .consent-text: reports text box dimensions
     (the intrinsic size of the text, NOT the clipped viewport)
   - But the rendered output is only 1px×1px
   - There is a mismatch between reported getBoundingClientRect and visible output
   With contain:size (strictest):
   - getComputedStyle(wrapper).width: "1px" — correct
   - But contain:size requires explicit width/height or they default to 0
   Audit implication:
   Checking element.offsetWidth > 0 or getBoundingClientRect().width > 0 on the
   consent text element passes (text has intrinsic non-zero width).
   Checking getBoundingClientRect() on the parent: 1×1 → should detect tiny container.
   But some tools check only the text element, not parent container chain dimensions.

   Detection:
   For all consent text elements with non-zero getBoundingClientRect:
   Walk up the ancestor chain checking for:
   - contains: 'paint' or 'strict' in getComputedStyle(ancestor).contain
   - width/height of that ancestor < some minimum threshold (e.g., 10px)
   If found → element may be present in DOM but rendered invisible → HIGH SA-CSS-CNT-001 */

contain:paint vs overflow:hidden: contain: paint is semantically stronger than overflow: hidden: it guarantees that content outside the element's border box is not painted, not just hidden from the scroll view. Critically, contain: paint also prevents child elements from creating layers that paint outside the container, which overflow: hidden alone does not guarantee in all cases. Detection must check for contain: paint or contain: strict in the ancestor chain of any consent text element, in addition to checking overflow: hidden.

HIGH
SA-CSS-CNT-001 — contain: layout paint style on a 1×1px container with consent text inside: text present in DOM with non-zero intrinsic dimensions but rendered in 1 pixel area and clipped; some tools check text element dimensions, not ancestor container. Detection: walk ancestor chain for contain values + tiny container dimensions → HIGH.

SA-CSS-CNT-002: isolation:isolate enabling blend mode attacks on any background

isolation: isolate forces an element to create a new compositing group. When a child element uses mix-blend-mode, it blends against the accumulated content within the nearest isolated ancestor rather than against the page background. This transforms blend mode consent attacks from background-dependent to background-independent: an attacker who applies mix-blend-mode: lighten to consent text needs a white page background for erasure on a normal page. With isolation: isolate on the dialog container and a white child element behind the text, the blend is against the white child rather than the page background.

.consent-dialog {
  isolation: isolate;      /* creates a new compositing group */
  background: white;       /* sets the white backdrop within the isolated group */
}
.consent-text {
  mix-blend-mode: lighten; /* blends against consent-dialog's white bg, not page bg */
  color: black;            /* lighten(black, white) = white → invisible */
}

/* Why isolation:isolate matters:
   Without isolation:isolate:
   mix-blend-mode:lighten on consent-text blends with the page background.
   On a dark page background (e.g., #1a1a1a): max(black_text, dark_bg) = dark_bg.
   The text might still be visible as slightly lighter than the dark background.
   The attack only reliably erases text on white/near-white page backgrounds.

   With isolation:isolate on .consent-dialog:
   The browser creates a compositing group for .consent-dialog.
   mix-blend-mode:lighten on .consent-text blends against the composited result
   of .consent-dialog's background (white) rather than the outer page background.
   lighten(black_text, white_dialog_bg) = white → invisible, regardless of page bg.
   The page background color becomes irrelevant to the attack.

   Isolation also applies to:
   - mix-blend-mode:multiply: multiply(text, white_isolated_bg) = text (identity — not an attack)
   - mix-blend-mode:screen: screen(text, white_isolated_bg) = white → ATTACK
   - mix-blend-mode:overlay: depends on background; with white bg → screen formula → white

   Detection:
   Check for isolation:isolate on consent dialog ancestors.
   If present, the effective backdrop for child mix-blend-mode is NOT the page background
   but the within-isolation background.
   When checking mix-blend-mode attacks, use the isolated group background color
   as the blend backdrop, not document.body background.
   isolation:isolate + white background on parent + mix-blend-mode:lighten/screen on text
   → CRITICAL SA-CSS-CNT-002 regardless of page background color */

Isolation breaks background-dependent detection: Detection that only checks mix-blend-mode against the page background color will miss blend mode attacks inside isolated groups. The relevant backdrop is the nearest ancestor with isolation: isolate. An audit must detect isolation: isolate on consent dialog ancestors and use that element’s effective background color as the blend backdrop when evaluating blend mode attacks on consent text children.

CRITICAL
SA-CSS-CNT-002 — isolation: isolate on consent dialog container + white background + mix-blend-mode: lighten/screen on consent text: blend mode erases text against local white backdrop regardless of page background color; evades page-background-dependent detection. Detection: check isolation context + local backdrop for blend mode analysis → CRITICAL.

SA-CSS-CNT-003: will-change:opacity GPU layer opacity evasion

will-change: opacity hints to the browser that the element’s opacity will change, causing it to be promoted to a separate GPU compositing layer. On most browsers, will-change: opacity or will-change: transform does not change the current computed opacity — getComputedStyle(el).opacity still returns the CSS opacity value correctly. However, the attack vector here is behavioral: in rare rendering edge cases and certain browser versions, GPU layer opacity compositing has historically differed from CSS opacity in subpixel rendering. More importantly, will-change is a signal used by attackers to set up GPU layer promotion as a precondition for other attacks.

.consent-text {
  will-change: opacity;    /* promotes element to GPU compositing layer */
  opacity: 0.004;          /* CSS opacity set near-zero: getComputedStyle returns "0.004" */
}

/* Why will-change matters in the attack context:
   Standalone: will-change:opacity does NOT change computed opacity.
   getComputedStyle(el).opacity === "0.004" — detectable.

   But: in combination with inline style injection or CSS custom property manipulation,
   will-change:opacity has been used as a marker to identify GPU-promoted layers
   that can then be targeted by JavaScript animation APIs at sub-CSS-tick speeds.

   Primary attack variant — will-change enables JS-based opacity to zero:
   el.style.willChange = 'opacity';
   requestAnimationFrame(() => {
     el.style.opacity = '0';  // Set opacity to 0 at GPU layer
     // For one frame, consent text is transparent
     // After the frame: opacity is set — but getComputedStyle returns "0"
   });
   The JS attack happens before any CSS scan snapshot is taken.

   Audit implication:
   A static snapshot audit (read getComputedStyle once) may capture opacity:0.004
   from CSS, missing the JS-set opacity:0 that fired in a requestAnimationFrame callback.
   Dynamic consent visibility requires:
   1. Observe mutations on the consent element (MutationObserver on style attributes)
   2. Check computedStyle opacity at multiple points: before consent shown,
      during consent display, and after user interaction starts
   3. Any opacity value < 0.1 during the consent window = consent attack

   Detection:
   Static: getComputedStyle(el).opacity < 0.1 → CRITICAL (direct CSS opacity attack)
   Dynamic: MutationObserver on consent element style attribute during consent display
   → any opacity < 0.1 at any point during consent window → CRITICAL SA-CSS-CNT-003 */

Static vs dynamic opacity: Static CSS audits check getComputedStyle(el).opacity once. JavaScript-driven opacity attacks can set opacity to 0 within a requestAnimationFrame callback after the static snapshot, then restore it. A complete consent security audit must include dynamic opacity monitoring using MutationObserver on the consent element’s style attribute and inline styles throughout the consent interaction window.

HIGH
SA-CSS-CNT-003 — will-change: opacity promoting element to GPU compositing layer; typically precursor to JS-driven opacity-to-zero attack in requestAnimationFrame callback evading static CSS snapshot audits. Detection: will-change: opacity + dynamic MutationObserver monitoring of consent element opacity during interaction window → HIGH.

SA-CSS-CNT-004: contain:strict with overflow:hidden clipping consent content

The most direct containment attack pairs contain: strict (which implies layout, paint, size, and style containment) with an explicitly small container and overflow: hidden. With contain: size, the element’s size is computed independently of its content — setting width: 0; height: 0 on a container with contain: strict creates a zero-size element that still contains all its children in the DOM. The children are present, have non-zero intrinsic dimensions, and are fully accessible to JavaScript — but the rendered output is zero pixels.

.consent-container {
  contain: strict;       /* implies layout + paint + size + style */
  width: 0;
  height: 0;
  overflow: hidden;      /* belt-and-suspenders; contain:paint already clips */
  position: absolute;
}
.consent-text {
  /* text content fully present in DOM, accessible via textContent, */
  /* but rendered within a 0×0 clipped container — invisible */
  font-size: 16px;
  width: 300px;          /* intrinsic width: non-zero */
}

/* contain:strict breakdown:
   - contain:layout: element layout is independent from outside (no intrinsic size influence)
   - contain:paint: content clipped to element border box (like overflow:hidden)
   - contain:size: element does not derive its size from content (explicit size required)
   - contain:style: counter/quote scoping (CSS only, less relevant for attacks)

   Combined with width:0; height:0:
   The container is zero pixels. All children are clipped to zero pixels.
   The children are fully present in the DOM with normal intrinsic dimensions.
   element.getBoundingClientRect() on .consent-text: non-zero (its own intrinsic box)
   element.getBoundingClientRect() on .consent-container: 0×0

   getComputedStyle(.consent-container).width: "0px"
   getComputedStyle(.consent-text).fontSize: "16px"
   → Static font-size and color checks on the text element pass
   → Container size check reveals the attack

   Accessibility API: ARIA accessibility tree includes the element
   (contain does not affect ARIA exposure — this is a rendering containment only).
   Screen readers will read the consent text aloud.
   The attack affects only visual rendering, not screen reader presentation.
   → Attack targets sighted users without assistive technology.

   Detection:
   For all consent text elements: check getBoundingClientRect() on all ancestor elements.
   If any ancestor has width < 1px OR height < 1px → element visually clipped → HIGH.
   Also check for contain property values including 'paint', 'strict', 'size'
   on ancestors with sub-threshold dimensions. */

Ancestor dimension chain check: A consent text element may report a large getBoundingClientRect (reflecting its intrinsic content dimensions) while being visually clipped by a zero-size ancestor with contain: strict. Complete audit requires checking the bounding box of every ancestor element in the consent text’s ancestor chain, not just the element itself. Any ancestor with width < 1px or height < 1px means the consent text is not visibly rendered, regardless of the text element’s own dimensions.

CRITICAL
SA-CSS-CNT-004 — contain: strict with width: 0; height: 0 on consent container: text present in DOM with non-zero intrinsic dimensions; rendered as zero pixels and clipped; ancestor bounding box is 0×0. Detection: check ancestor chain bounding boxes; any ancestor width < 1px or height < 1px → CRITICAL consent visibility failure.

Summary of SA-CSS-CNT findings

IDSeverityPropertyAttack mechanismDetection approach
SA-CSS-CNT-001 HIGH contain: layout paint Clipping via contain:paint on tiny container Ancestor chain contain value + tiny dimensions
SA-CSS-CNT-002 CRITICAL isolation: isolate Local white backdrop enables blend mode attacks on any page bg Isolation context + local backdrop color for blend analysis
SA-CSS-CNT-003 HIGH will-change: opacity GPU layer precursor for JS rAF opacity-to-zero evasion will-change:opacity + MutationObserver dynamic monitoring
SA-CSS-CNT-004 CRITICAL contain: strict + 0px container Zero-size container with contain:size clips all content Ancestor chain getBoundingClientRect; any <1px ancestor

Related pages