MCP server CSS will-change consent security
The CSS will-change property is a performance hint that informs the browser which properties an element is likely to animate, allowing it to pre-create compositing layers and stacking contexts. As a side effect, will-change:transform creates a new containing block — breaking position:fixed for any descendant consent element that expects to position relative to the viewport — and will-change:contents forces a new stacking context that alters z-index layering for consent overlays. Neither effect is visible in the CSS as an intentional layout rule; both appear as browser-optimization hints.
Attack findings
el.style.willChange='transform') breaks position:fixed at the moment the install interaction begins; static analysis misses runtime-applied will-changeBackground: will-change containing block and stacking context side-effects
The CSS specification (CSS Transforms Level 1) states that an element with a transform (or will-change:transform, which creates the same compositing context) establishes a new containing block for all absolutely- and fixed-positioned descendants. This is the same side-effect as transform:translateZ(0), a well-known trick for forcing GPU compositing. In normal use, this behavior is expected: the developer who applies the transform controls the positioning context. In the consent attack, an ancestor element outside the consent component applies will-change:transform, silently overriding the position:fixed intent of the consent element the component author wrote.
Audit gap: Static CSS analysis that checks position:fixed on a consent element and concludes "this element is viewport-fixed" is wrong when any ancestor has will-change:transform, transform, perspective, or filter. The effective containing block for position:fixed is not the viewport but the nearest transformed ancestor. Consent positioned this way scrolls off-screen with its containing block.
Attack 1 — will-change:transform breaks position:fixed (SA-CSS-WC-001)
A consent element authored as position:fixed; top:0; left:0; width:100%; height:100% (a modal overlay) expects to cover the entire viewport regardless of scroll position. When an ancestor has will-change:transform, the consent element's containing block becomes that ancestor. If the ancestor is scrolled, the consent element moves with it. If the ancestor has overflow:hidden, the consent element is clipped to the ancestor's bounds. The consent element is in the DOM, computed style shows position:fixed, but getBoundingClientRect() reveals it has scrolled out of the visible viewport area.
/* Attack: ancestor will-change:transform breaks position:fixed consent */
.page-wrapper {
will-change: transform; /* Performance hint — also creates new containing block */
overflow: hidden; /* Clips fixed-positioned descendants to wrapper bounds */
}
.consent-modal {
position: fixed; /* Intent: viewport-level modal */
top: 0; left: 0;
width: 100%; height: 100%;
/* Actual: positioned relative to .page-wrapper, not viewport */
/* If .page-wrapper scrolls or has overflow:hidden — consent clipped */
}
/* Detection */
function checkFixedContainingBlock(el) {
if (getComputedStyle(el).position !== 'fixed') return null;
// Walk ancestors for will-change:transform
let ancestor = el.parentElement;
while (ancestor) {
const wc = getComputedStyle(ancestor).willChange;
const tr = getComputedStyle(ancestor).transform;
if (wc.includes('transform') || (tr && tr !== 'none')) {
return { vuln: 'SA-CSS-WC-001',
detail: `position:fixed containing block is ${ancestor.tagName} not viewport` };
}
ancestor = ancestor.parentElement;
}
return null;
}
SA-CSS-WC-001 (High). Detection requires walking the DOM ancestor chain of any fixed-positioned consent element and checking each ancestor for will-change:transform, transform, perspective, or filter. Any of these properties creates a new containing block. SkillAudit checks the full ancestor chain for every fixed-positioned consent element.
Attack 2 — will-change:contents breaks stacking context for consent overlays (SA-CSS-WC-002)
The CSS specification states that will-change:contents may cause the browser to create a new stacking context. In practice, Chromium creates a new stacking context for elements with will-change:contents. This changes how z-index is evaluated for the element's descendants relative to elements outside it. An overlay that previously painted below the consent element (insufficient z-index) now paints over it when the consent element's ancestor establishes a new stacking context via will-change:contents. The stacking order changes invisibly — neither element's z-index value changes, only the stacking context they participate in.
/* Attack: will-change:contents creates stacking context — overlay now covers consent */
.consent-section {
will-change: contents; /* Creates new stacking context in Chromium */
position: relative;
z-index: 1;
}
.decorative-overlay {
position: absolute;
z-index: 0; /* Previously: painted below consent
After stacking context change: paints over it
because the stacking context changes comparison */
background: rgba(255,255,255,0.95);
inset: 0;
pointer-events: none;
}
Attack 3 — will-change:transform on scroll container traps consent off-screen (SA-CSS-WC-003)
When will-change:transform is applied to a scroll container that is the containing block for an absolutely-positioned consent element, the consent element's initial render position can be placed below the scroll container's initial scroll offset, making it unreachable without scrolling down. The key is that the initial scrollTop is set to a value that skips the consent section: the page loads, the scroll container renders at scrollTop=0 visually showing the install button, but the consent lives at a scroll position that the container never reaches in normal interaction flow.
/* Attack: will-change:transform + scrollTop pre-set bypasses consent scroll position */
.install-scroll-container {
will-change: transform;
overflow-y: scroll;
height: 200px; /* Fixed-height container */
}
/* JS: on DOMContentLoaded, set scrollTop past the consent section */
document.querySelector('.install-scroll-container').scrollTop = 9999;
/* Consent exists in DOM at scroll position 300px; container shows only bottom */
Attack 4 — JS-applied will-change at interaction time (SA-CSS-WC-004)
JavaScript can apply will-change to any element at runtime via el.style.willChange = 'transform'. When applied to a consent ancestor at the moment the user begins the install interaction — for example, in a mousedown or touchstart listener on the install button — the containing block change takes effect immediately. The consent element was correctly positioned before the interaction began; it breaks at the same moment the user commits to install. Static analysis of the stylesheet does not see this behavior; only analysis that executes the JS and monitors style mutations detects it.
/* Attack: JS applies will-change at install interaction time */
document.querySelector('#install-btn').addEventListener('mousedown', () => {
// Applied 50ms before click fires — consent repositions before install commit
document.querySelector('.page-wrapper').style.willChange = 'transform';
});
/* Detection: MutationObserver on consent-ancestor style attribute */
const observer = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.attributeName === 'style') {
const wc = m.target.style.willChange;
if (wc && wc.includes('transform')) {
flagFinding('SA-CSS-WC-004',
'will-change:transform applied at runtime — check fixed consent positioning');
}
}
}
});
observer.observe(document.body, { subtree: true, attributes: true, attributeFilter: ['style'] });
SkillAudit detection: SkillAudit checks all fixed-positioned consent elements for transformed or will-change ancestors, evaluates stacking context membership for consent elements in will-change:contents sections, and monitors runtime style mutations during the install interaction simulation. Run a free audit →
Detection summary
| Attack ID | will-change value + mechanism | Key detection signal |
|---|---|---|
| SA-CSS-WC-001 | will-change:transform → position:fixed containing block | Fixed consent ancestor has will-change:transform or transform ≠ none |
| SA-CSS-WC-002 | will-change:contents → stacking context override | Ancestor has will-change:contents + overlay paints over consent in computed layer |
| SA-CSS-WC-003 | will-change:transform on scroll container → scrollTop bypass | Consent rect outside scroll container visible bounds at load scrollTop |
| SA-CSS-WC-004 | JS will-change applied at mousedown → runtime containing block break | MutationObserver detects style.willChange mutation during interaction |