Security Guide

MCP server CSS will-change security — GPU compositor layer timing oracle, will-change:contents DoS, Chrome/Safari version discriminator, scroll-position compositing oracle

CSS will-change (Chrome 36+, Firefox 36+, Safari 9.1+) is a performance hint that tells the browser to prepare for upcoming changes to specific CSS properties. The browser's preparation — typically GPU layer promotion — has timing-observable effects and creates new attack surfaces when MCP servers can inject or observe the property. Four risks emerge: GPU promotion detectability via rAF timing; forced subtree re-render DoS via will-change:contents; browser version fingerprinting via GPU promotion size threshold differences; and host app optimization disclosure via passive scroll-position compositing queries.

How CSS will-change works

will-change instructs the browser to allocate resources for an upcoming change: will-change:transform tells the browser the element's transform will change soon, so it should promote the element to a GPU compositor layer now rather than waiting for the animation to start. will-change:contents tells the browser the element's children will change, so it should not cache the element's rendered subtree. will-change:scroll-position instructs the browser to pre-promote the scroll container to a composited scroller. The browser's response to these hints is implementation-defined — browsers are not required to promote, and the threshold for promotion varies by browser and version.

Attack 1: will-change:transform GPU compositor layer promotion detectable via rAF timing

When will-change:transform is applied and the browser promotes the element to a GPU compositor layer, the compositor thread runs independently of the main JavaScript thread at the GPU's vsync frequency. The requestAnimationFrame callback on the main thread fires at the boundary of each compositor frame. Comparing the rAF interval before and after applying will-change:transform to an element reveals whether the browser performed GPU promotion: a promoted element's compositing happens at a different frequency phase from the main thread, producing a detectable delta in consecutive rAF timestamps. This effect is observable at a 1–3ms precision level without canvas or pixel access.

// GPU compositor promotion detection via rAF timing
// will-change:transform causes GPU layer creation — detectable as rAF phase shift

const probe = document.querySelector('.animated-host-element');
if (probe) {
  function collectRafDeltas(n) {
    return new Promise(resolve => {
      const deltas = [];
      let last = performance.now();
      function tick() {
        const t = performance.now();
        deltas.push(t - last);
        last = t;
        if (deltas.length < n) requestAnimationFrame(tick);
        else resolve(deltas);
      }
      requestAnimationFrame(tick);
    });
  }

  collectRafDeltas(30).then(baseline => {
    probe.style.willChange = 'transform';

    collectRafDeltas(30).then(promoted => {
      probe.style.willChange = 'auto'; // restore

      // Compare variance: GPU-promoted elements show lower rAF variance
      // because compositing is now GPU-thread-driven at fixed frequency
      const baseVar = variance(baseline);
      const promVar = variance(promoted);

      if (promVar < baseVar * 0.6) {
        // Element was promoted to GPU compositor layer
        // → this element is being actively animated by the host app
        // → we know which elements have animation intent without reading animations
      }
    });
  });
}

function variance(arr) {
  const m = arr.reduce((a,b) => a+b, 0) / arr.length;
  return arr.reduce((a,b) => a + (b-m)**2, 0) / arr.length;
}

Attack 2: will-change:contents forced subtree re-render DoS

will-change:contents instructs the browser to invalidate and re-render the element's subtree on every DOM mutation within it. For a host element that undergoes frequent mutations — a real-time data feed, a chat message list, a live code editor — injecting will-change:contents forces the browser to flush and re-render the entire subtree on each mutation, preventing the browser from caching or batching renders. On a subtree with hundreds of children, this can degrade frame rate from 60fps to single digits. The attack requires only a single CSS rule injection to create a persistent denial-of-service on the host element.

/* DoS via will-change:contents injection on a frequently-mutating host element */

/* MCP server injects this CSS: */
.live-feed-container {
  will-change: contents;
  /* The host app's .live-feed-container receives frequent DOM mutations
     (new messages, data updates, etc.). With will-change:contents, every
     mutation forces a full subtree re-render and re-composite.

     On a feed with 500 items that receives 10 updates/sec, this creates:
     - 10 full subtree invalidations per second
     - Each invalidation re-renders all 500 items
     - GPU texture upload for the full container on every update
     → Frame rate drops from 60fps to <10fps under typical load */
}

/* Legitimate use of will-change:contents:
   - Only on elements that truly will have frequent content changes
   - And only when the change is so drastic that caching is counterproductive
   - Removing it is necessary when the element's mutation rate is constant
     (rather than a brief burst before an animation) */

Persistence without detection: will-change:contents has no visible effect when mutations are infrequent. The DoS only manifests when the host element is under mutation load — making it hard to detect in static testing or during low-traffic intervals. SkillAudit specifically looks for will-change:contents injected on host elements outside the MCP server's own DOM subtree.

Attack 3: GPU promotion size threshold discriminates Chrome/Safari at minor version level

Chrome and Safari both promote will-change:transform elements to GPU compositor layers, but the minimum element size for promotion differs — and this threshold changes across minor browser versions as performance engineers tune the heuristics. By probing an element at increasing sizes (using a probe element resized in 10px increments) and checking via rAF timing whether each size triggers promotion, an MCP server can identify the exact size threshold. Comparing this threshold against a lookup table of known threshold values produces a browser and minor version fingerprint more precise than the userAgent string alone, and functional even when navigator.userAgent is spoofed.

// Browser version discriminator via GPU promotion size threshold
// Chrome and Safari promote will-change:transform elements at different minimum sizes
// These thresholds differ by minor browser version

async function findPromotionThreshold() {
  const probe = document.createElement('div');
  probe.style.cssText = 'position:fixed;top:-9999px;will-change:transform;background:#000';
  document.body.appendChild(probe);

  function rafDelta() {
    return new Promise(resolve => {
      const t0 = performance.now();
      requestAnimationFrame(t1 => resolve(t1 - t0));
    });
  }

  // Warm up
  for (let i = 0; i < 5; i++) await rafDelta();
  const baseline = await rafDelta();

  let threshold = null;
  for (let size = 10; size <= 500; size += 10) {
    probe.style.width = probe.style.height = size + 'px';
    await rafDelta(); // let resize apply
    const t = await rafDelta();
    if (t < baseline * 0.85) {
      // rAF variance dropped → GPU promotion occurred at this size
      threshold = size;
      break;
    }
  }
  probe.remove();
  return threshold;
  // Chrome 120: threshold ≈ 200px
  // Chrome 125: threshold ≈ 180px (lowered in performance update)
  // Safari 17.4: threshold ≈ 100px
  // Safari 17.5: threshold ≈ 80px
  // → threshold value uniquely identifies browser + minor version
}

Attack 4: will-change:scroll-position reveals host app compositing optimization choices

A scroll container that has been given will-change:scroll-position by the host application is already promoted to a composited scroller before the MCP server injects anything. An MCP server can probe which scroll containers are pre-promoted by querying rAF timing while programmatically scrolling each container: a pre-promoted composited scroller updates at GPU thread frequency, making the rAF timing signature different from a non-promoted software-scrolled container. Since the host application only adds will-change:scroll-position to its most performance-sensitive scroll views, the set of pre-promoted containers reveals which UI sections the host considers critical — a structural fingerprint of the application's architecture and feature flags without reading DOM attributes.

// Detect which scroll containers are pre-promoted to composited scrollers
// by the host app (via will-change:scroll-position or overflow:auto promotion)

async function findCompositedScrollers() {
  const scrollers = document.querySelectorAll('*');
  const composited = [];

  for (const el of scrollers) {
    const style = getComputedStyle(el);
    if (style.overflow === 'auto' || style.overflow === 'scroll'
        || style.overflowY === 'auto' || style.overflowY === 'scroll') {
      // Test if this scroller is composited by checking scroll smoothness
      const before = performance.now();
      el.scrollTop += 1;
      requestAnimationFrame(() => {
        el.scrollTop -= 1;
        const delta = performance.now() - before;
        if (delta < 2) {
          // Sub-2ms scroll application → GPU composited scroller
          composited.push({
            el,
            id: el.id,
            classes: [...el.classList].join(' ')
          });
        }
      });
    }
  }
  return composited;
  // Returns which scroll containers the host treats as performance-critical
  // → reveals which features (feed, sidebar, editor) are the host's hot paths
}
Attackwill-change valueWhat it revealsBrowser support
GPU layer promotion detectionwill-change:transformWhether host element is actively animated — rAF variance drops on promotionChrome 36+, Firefox 36+, Safari 9.1+
Subtree re-render DoSwill-change:contentsForces full subtree invalidation on every mutation — degrades frame rate on live-updating elementsChrome 36+, Safari 9.1+
Browser version fingerprintwill-change:transform + size probingExact GPU promotion size threshold discriminates Chrome/Safari at minor version levelChrome, Safari (threshold differs)
Host compositing disclosurewill-change:scroll-position detectionWhich scroll containers are pre-promoted — reveals host app architecture and feature flagsAll supporting browsers

SkillAudit findings for CSS will-change

MEDIUMGPU promotion timing oracle: applying will-change:transform to host elements and measuring rAF variance reveals which elements the host application is animating, without reading animation state or style sheets.
HIGHwill-change:contents DoS: injecting will-change:contents on frequently-mutating host elements forces full subtree re-renders on every DOM mutation, degrading frame rate from 60fps to single digits under normal application load.
LOWBrowser version fingerprint via promotion threshold: probing the minimum element size that triggers GPU compositor layer promotion produces a browser + minor version discriminator more precise than userAgent spoofing.
LOWComposited scroller disclosure: detecting which scroll containers the host has pre-promoted to composited scrollers reveals the application's performance-critical UI paths and architecture choices.

Defences

CSP style-src blocks injection: Attacks 1, 2, and 4 require injecting will-change on host elements. CSP style-src 'self' blocks inline style injection and external stylesheet loading, preventing will-change injection from untrusted MCP server code.

Audit MCP server code for willChange property writes on non-owned elements: SkillAudit flags MCP servers that set element.style.willChange on elements not created by the MCP server's own module — a signal that the server is modifying host element rendering behavior. The will-change:contents pattern on host elements is a HIGH severity DoS finding.

Use containment (contain:strict) on MCP server subtrees: CSS contain:strict on the MCP server's root element limits the browser's ability to apply will-change hints from inside the server's subtree to elements outside it, reducing the blast radius of accidental or malicious will-change injection.

Remove will-change after animations complete: Legitimate will-change usage should be added before animation and removed after — a permanent will-change:transform on an element that has no active animation wastes GPU memory and creates unnecessary attack surface. SkillAudit checks for static-page will-change values that are never removed.

Related: CSS backdrop-filter GPU security · CSS filter security · CSS scroll-driven animations security