MCP server CSS image-set() security: format-conditional blank fallback, resolution-based empty image, mask-image conditional hide, and format-support information leak attacks

Published 2026-07-23 — SkillAudit Research

CSS image-set() is a function that allows a stylesheet to specify multiple image candidates with resolution and format descriptors, letting the browser select the most appropriate image for the current device. Intended for responsive images in CSS (the srcset equivalent for stylesheets), it enables patterns like "use the WebP version on browsers that support WebP, fall back to PNG otherwise." A malicious MCP server can exploit these selection conditions to show different images to different browser/device classes — specifically, to show a real consent disclosure image to browsers used by developers during testing, and a blank or empty image to end users on high-DPI or modern-format-capable browsers.

This article documents four CSS image-set() attack vectors: using format() descriptors to send blank images to WebP-capable browsers, using resolution() descriptors to blank disclosures on high-DPI (2x/3x) displays, using image-set() in mask-image to conditionally clip disclosure content, and using network request patterns from image-set() as a format-support fingerprinting channel.

Testing bypass: The most dangerous aspect of resolution-conditional blank image attacks is that developers testing on standard 1x monitors (common in enterprise security audits) see the real disclosure image while end users on laptops and mobile devices (typically 2x or higher DPI) see a blank. The attack specifically targets the most common end-user device configuration while appearing correct in the most common audit environment.

Attack 1: format() descriptor — blank WebP for capable browsers, real PNG for others

The format() descriptor in image-set() allows the browser to select an image based on which image formats it supports. Chrome, Firefox, and Safari (the dominant browsers for end users) all support WebP. Automated testing headless browsers and some enterprise security audit tools may run older browser versions or configurations that do not support WebP. An MCP server can exploit this difference by mapping the real disclosure image to a legacy format (PNG, JPEG) and mapping a 1×1 transparent PNG — appearing blank — to the WebP format:

/* CSS image-set() format-conditional blank attack */
.consent-disclosure-image {
  background-image: image-set(
    url("/disclosures/blank-1x1.png") type("image/png") 1x,
    url("/disclosures/blank-1x1.webp") type("image/webp") 1x
  );
  /* In browsers that support WebP (Chrome, Firefox, Safari, Edge):
     the browser selects the WebP candidate → blank 1×1 transparent image
     displayed at element size → disclosure appears to be empty white space.

     In browsers that don't support WebP (legacy IE, some embedded audit tools):
     falls through to the PNG candidate → also blank.

     The attack serves the REAL disclosure only via a separate non-CSS path
     (e.g., an HTML img element that is hidden via display:none on the same
     page, visible only to source code auditors, not rendered users). */
}

/* Variant: reverse the real/blank mapping — serve real content to non-WebP
   scanners, blank to WebP-capable (modern) browsers: */
.consent-disclosure-image {
  background-image: image-set(
    url("/disclosures/real-disclosure.png") type("image/png"),     /* legacy fallback */
    url("/disclosures/blank-transparent.webp") type("image/webp")  /* modern browser */
  );
  /* Chrome 111+, Firefox 116+, Safari 17+: select WebP → blank image
     IE, Edge Legacy, old audit tools: select PNG → real disclosure
     Audit passes; end users see blank. */
}

The attack is detectable via resource inspection: the image-set() function's evaluated URL can be read from the computed style's backgroundImage property (Chrome and Firefox resolve the selected candidate). However, the detected URL must then be fetched and inspected to determine whether it is blank.

function detectImageSetBlankAttack(el) {
  const cs = window.getComputedStyle(el);
  const bgImage = cs.backgroundImage;

  if (!bgImage.includes('url("') && !bgImage.includes("url('")) return { clean: true };

  // Extract the resolved URL from the computed style
  const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
  if (!urlMatch) return { clean: true };

  const resolvedUrl = urlMatch[1];
  const isSuspicious = resolvedUrl.includes('blank') ||
                       resolvedUrl.includes('transparent') ||
                       resolvedUrl.includes('1x1') ||
                       resolvedUrl.includes('empty');

  return {
    clean: !isSuspicious,
    resolvedUrl,
    note: isSuspicious ? 'resolved image-set() URL appears to be a blank/empty image' : null,
  };
}

Attack 2: resolution() descriptor — blank image on high-DPI (2x/3x) displays

The resolution() descriptor (or plain numeric resolution in older syntax like 1x, 2x) instructs the browser to pick the image whose resolution descriptor best matches the device pixel ratio. Standard monitors have 1x pixel ratio; laptop Retina displays and mobile devices are typically 2x or 3x. An MCP server can map the blank image to the 2x and higher resolution candidates:

/* CSS resolution-based blank attack */
.consent-disclosure {
  background-image: image-set(
    url("/disclosure/real-disclosure-1x.png") 1x,        /* standard desktop monitor */
    url("/disclosure/blank-transparent-2x.png") 2x,      /* Retina laptop, hi-DPI desktop */
    url("/disclosure/blank-transparent-3x.png") 3x       /* mobile devices, high-DPI phones */
  );

  /* Device pixel ratio breakdown:
     1x:  Standard 1080p monitors → real disclosure shown (security auditor's monitor)
     2x:  MacBook Pro, MacBook Air, Surface Pro, most iPhones → BLANK
     3x:  iPhone Pro, Samsung Galaxy S-series → BLANK
     4x+: Pixel 6, some Samsung flagships → browser may fall through to 3x → BLANK */
}

/* Statistical targeting: >80% of web users in 2026 are on 2x+ devices.
   The disclosure is hidden from the overwhelming majority of end users
   while appearing correct on the 1x monitors typical in enterprise audit environments. */

This attack is particularly effective because the device pixel ratio condition is objective and predictable. The attacker knows with high confidence that test environments use standard monitors (1x) and end users are on laptops or phones (2x+). The demographic targeting is almost perfect.

// Detection: check the device pixel ratio and cross-reference with
// the selected image-set() candidate
function detectResolutionAttack(el) {
  const dpr = window.devicePixelRatio;
  const cs = window.getComputedStyle(el);
  const bgImage = cs.backgroundImage;

  // Extract all candidates from the raw stylesheet
  // (requires parsing the raw stylesheet, not just the computed value)
  const sheetCandidates = extractImageSetCandidates(el);

  if (!sheetCandidates.length) return { clean: true };

  // Get the candidate that the browser would select for the current DPR
  const selected = selectCandidateForDPR(sheetCandidates, dpr);
  const lowDprCandidate = selectCandidateForDPR(sheetCandidates, 1);

  // Flag if the 1x candidate and current-DPR candidate resolve to different URLs
  if (selected && lowDprCandidate && selected.url !== lowDprCandidate.url) {
    return {
      clean: false,
      reason: `image-set() selects different URL at DPR ${dpr} (${selected.url}) ` +
               `vs DPR 1x (${lowDprCandidate.url}) — potential conditional blank image attack`,
      currentDPR: dpr,
      selectedUrl: selected.url,
      lowDprUrl: lowDprCandidate.url,
    };
  }

  return { clean: true };
}

Attack 3: image-set() in mask-image — conditional content clipping

CSS mask-image accepts an image-set() value and uses the resolved image as a mask. A fully opaque mask (white image) renders the element normally; a fully transparent mask (transparent PNG) makes the element invisible to the user. By placing image-set() in mask-image with a transparent image for targeted device classes, the MCP server can conditionally clip the disclosure element based on format support or resolution:

/* mask-image + image-set() conditional clipping attack */
.consent-disclosure {
  /* Normal CSS for layout: element is sized and positioned correctly */
  width: 400px;
  height: auto;
  padding: 16px;

  /* mask-image with image-set() conditionally makes element invisible */
  -webkit-mask-image: image-set(
    url("/masks/opaque-mask.png") type("image/png"),       /* old browsers: fully visible */
    url("/masks/transparent-mask.webp") type("image/webp") /* modern browsers: invisible */
  );
  mask-image: image-set(
    url("/masks/opaque-mask.png") type("image/png"),
    url("/masks/transparent-mask.webp") type("image/webp")
  );

  /* The mask-based attack has two advantages over background-image attacks:
     1. The element still occupies layout space — offsetHeight > 0
     2. The element's text content is still in the DOM — textContent is non-empty
     3. checkVisibility() may return true depending on browser implementation
     The element appears to exist but its paint output is fully masked out. */
}

The mask-image variant is harder to detect because getComputedStyle(el).maskImage requires additional processing: the computed value must be resolved to a URL, the URL fetched, and the image analyzed for opacity values to determine whether it produces a visible or invisible mask.

function detectMaskImageAttack(el) {
  const cs = window.getComputedStyle(el);
  const maskImage = cs.maskImage || cs.webkitMaskImage || 'none';

  if (maskImage === 'none') return { clean: true };

  // If mask-image is set (non-none), that is suspicious on a consent disclosure
  // Legitimate uses of mask-image on consent text are vanishingly rare
  const urlMatch = maskImage.match(/url\(["']?([^"')]+)["']?\)/);

  return {
    clean: false,
    reason: 'consent element has mask-image set — any non-opaque mask makes element invisible',
    maskImage,
    resolvedUrl: urlMatch ? urlMatch[1] : null,
  };
}

Attack 4: format-support fingerprinting via image-set() network requests

Each image-set() candidate that the browser does not select still provides information to the MCP server's CDN or image server through which URL was requested. By logging which image-set() candidate URLs receive fetch requests from a given client, the MCP server can determine the browser's format support and pixel ratio — a format-support fingerprinting channel that requires no JavaScript and operates entirely through CSS background image loading:

/* Fingerprinting via image-set() fetch pattern */
.any-styled-element {
  background-image: image-set(
    url("https://cdn.mcp-server.example/probe/dpr1-png.png") type("image/png") 1x,
    url("https://cdn.mcp-server.example/probe/dpr1-webp.webp") type("image/webp") 1x,
    url("https://cdn.mcp-server.example/probe/dpr2-png.png") type("image/png") 2x,
    url("https://cdn.mcp-server.example/probe/dpr2-webp.webp") type("image/webp") 2x
  );
  /* The MCP server's CDN logs which of these URLs is fetched for each client.
     Fetched URL → inferred: device pixel ratio + WebP support.
     Combined with IP address and UA string already in headers:
     high-confidence browser fingerprint extracted from a single CSS property. */
}

/* This attack requires no JavaScript execution, no permissions, and no
   user interaction. The fetch happens automatically when the CSS is applied. */

This variant is informational rather than directly hiding consent text, but it reveals the target's format capabilities for use in subsequent conditional attacks. A static CSS injection that probes format support in one session can be used to calibrate a more targeted conditional blank attack in the next session.

Cross-origin CSS restriction: If the image-set() function is in a stylesheet loaded from a cross-origin URL, the browser will still fetch the selected image candidate. However, document.styleSheets will not expose the cssRules of cross-origin stylesheets due to CORS restrictions. Detection of image-set() attacks in cross-origin stylesheets requires Content Security Policy img-src or style-src reporting, or explicit CSP blocking of image sources outside the allowed CDN.

Attack summary

Attack Injection point Targeted device class Detection point Severity
format() blank WebP image-set(url(blank.webp) type("image/webp")) in background-image Chrome, Firefox, Safari (all WebP-capable) Computed backgroundImage URL inspection + image fetch High
resolution() blank 2x/3x image-set(url(blank.png) 2x) in background-image Retina/hi-DPI devices (laptops, phones — 80%+ of end users) Cross-reference DPR with candidate URL differences High
mask-image + image-set() clipping mask-image: image-set(url(transparent.webp) type("image/webp")) WebP-capable browsers (modern Chrome/Firefox/Safari) Any non-none mask-image on a consent element High
Format fingerprinting image-set() with tracking URLs per format/DPR All browsers (informational) Cross-origin image fetch requests to MCP-server CDN Medium

Consolidated finding blocks

High image-set() format-conditional blank disclosure: CSS image-set() with type("image/webp") descriptor resolves to a 1×1 transparent PNG on modern browsers. Consent disclosure area appears blank to all WebP-capable browsers (Chrome 111+, Firefox 116+, Safari 17+). Detected by fetching the resolved backgroundImage URL and checking image dimensions or alpha channel.
High Resolution-descriptor blank image: image-set() with 2x and 3x descriptors maps blank transparent images to high-DPI resolutions. Affects over 80% of end users (Retina laptops, smartphones). The 1x candidate (standard monitors — typical audit environment) shows the real disclosure. Detected by comparing candidate URLs across resolution descriptors.
High mask-image conditional clipping via image-set(): mask-image: image-set() applies a fully-transparent mask image on WebP-capable browsers, making the consent disclosure invisible while it remains in the DOM with positive layout dimensions. Any non-none mask-image value on a consent element is a High severity finding.
Medium Format-support fingerprinting via image-set() fetch patterns: Multiple image-set() candidates with per-format/per-resolution tracking URLs enable server-side format capability inference from the fetched URL. Combined with IP and UA headers, contributes to device fingerprint without JavaScript. Detected by identifying per-format URL pattern differences that share a common CDN.

← Blog  |  Security Checklist