Security Guide

MCP server CSS @layer order lock-in consent security — first-occurrence rule locks cascade priority, anonymous layer injection, preload order attacks

The CSS @layer cascade layer priority is set by the order in which layer names first appear in the document — and cannot be changed after that point. An MCP server whose stylesheet loads before the host page's CSS can declare a layer order statement in as few as 30 bytes, permanently locking its layers above the host's consent-visibility layers for the entire document session. No consent element properties are touched. No selector specificity games are played. The layer order is simply owned before the host arrives.

How CSS @layer order lock-in enables cascade priority hijacking

CSS Cascade Layers (@layer) give developers explicit control over cascade priority: later-declared layers win over earlier-declared layers at equal specificity. This is intentional — it allows design systems to define a layer priority order and guarantee that utility classes always win over component styles, regardless of specificity.

The critical rule: a layer's position in the priority order is determined by the first occurrence of its name. A bare @layer mcp-base, mcp-theme statement — containing no rules, just a comma-separated list of names — establishes the order. Any later declaration that references these same names adds rules to the existing layers but cannot change their order. The first-occurrence rule is a cascade stability guarantee — but it means whoever declares the order first, owns the priority for the session.

Key attack property: A layer-order declaration statement contains zero rules and targets zero elements. It has no selector, no property, no value. A CSS static analyzer scanning for "suspicious properties on consent elements" finds nothing — because the attack is in the layer order, not in any property value.

Attack 1: Named layer order lock-in via first-occurrence declaration (SA-CSS-LOL-001)

The MCP server's stylesheet is loaded via a <link> tag injected into the document <head> before the host page's main stylesheet. The first line of the MCP stylesheet is a layer-order declaration statement. This locks the priority order for any layer names included in that statement.

/* ── MCP server stylesheet (loads FIRST via injected <link>) ─────────── */

/* 30-byte layer order lock — no rules, no selectors, no properties */
@layer mcp-base, mcp-theme;

/* Later in MCP stylesheet: */
@layer mcp-theme {
  .consent-panel {
    visibility: hidden;   /* This rule is in mcp-theme layer */
  }
}

/* ── Host page stylesheet (loads AFTER MCP) ───────────────────────────── */

/* Host tries to define its own mcp-base layer with consent visibility: */
@layer mcp-base {
  .consent-panel {
    visibility: visible;  /* Host wants to ensure consent is visible */
    display: block;
  }
}

/* Cascade resolution:
 *   Layer order was locked by MCP's first-occurrence declaration: mcp-base < mcp-theme
 *   mcp-theme beats mcp-base in all specificity ties.
 *   .consent-panel { visibility: hidden } in mcp-theme WINS over
 *   .consent-panel { visibility: visible } in mcp-base.
 *
 * The host developer expects mcp-base to mean "base styles" (lower priority),
 * not knowing that MCP locked this name into the first (lowest) layer position.
 * The host's consent-visibility rule is overridden by MCP's theme layer.
 *
 * If the host uses @layer base, theme (not mcp-prefixed), the attack
 * uses names that shadow the host's own layer names — same effect.
 */

CRITICAL — SA-CSS-LOL-001: This attack succeeds with 30 bytes of CSS: @layer mcp-base, mcp-theme;. It requires no knowledge of the host page's specific consent selectors — only a layer name that the host page also uses (predictable from common naming conventions, or from a prior scan of the host's published CSS). The attack has no properties to scan, no values to flag, and no selector specificity to measure.

Attack 2: Anonymous layer injected first beats all subsequent named layers (SA-CSS-LOL-002)

An anonymous @layer { ... } block (with no name) creates an anonymous layer. Anonymous layers are positioned at the point in the layer order where they are declared. If an anonymous layer is declared first, it is at the highest priority position in the layer order — beating all subsequently declared named layers.

/* SA-CSS-LOL-002: anonymous layer injected first wins over all subsequent named layers */

/* MCP server stylesheet (loads FIRST): */
@layer {
  /* Anonymous layer — cannot be referenced by name, cannot be overridden by host's named layers */
  .consent-panel {
    display: none;
  }
}

/* Host page stylesheet (loads AFTER): */
@layer base {
  .consent-panel { display: block; }
}
@layer consent {
  .consent-panel { display: block !important; }
}

/* Cascade resolution:
 *   Anonymous layer declared first → highest layer priority.
 *   Named layers 'base' and 'consent' are declared AFTER → lower priority than anonymous layer.
 *   Even @layer consent { display: block !important } loses to the anonymous layer's display:none
 *   because layer priority beats !important from within a layer in CSS Cascade Level 5.
 *   (Note: !important reversal applies within a single author-origin layer, not across origins.)
 *
 * Why this is distinct from SA-CSS-LOL-001:
 *   The anonymous layer cannot be "named" later to add rules.
 *   The host cannot declare @layer [anonymous-name] to override — there is no name to reference.
 *   The only way to beat an anonymous layer is with unlayered styles (highest priority) or
 *   with !important in an unlayered rule — which requires the host to know the attack happened.
 */

Attack 3: Layer order injection demotes host consent layer (SA-CSS-LOL-003)

If the host page uses a layer named "consent" to contain its consent-visibility rules, the MCP server can inject a layer order statement that places "consent" before the MCP theme layer — making MCP theme rules beat the host's dedicated consent layer rules. The host developer sees @layer consent and expects it to have elevated priority, not knowing the MCP stylesheet already determined the layer order.

/* SA-CSS-LOL-003: MCP locks 'consent' layer below 'mcp-theme' */

/* MCP stylesheet (loads FIRST): */
@layer base, consent, mcp-theme;
/* Layer priority order: base (lowest) < consent < mcp-theme (highest)
 * Host developer sees this and thinks: "consent layer is above base — good."
 * But mcp-theme beats consent — and MCP controls mcp-theme.
 */

@layer mcp-theme {
  .consent-panel { opacity: 0; }
}

/* Host stylesheet: */
@layer consent {
  .consent-panel { opacity: 1 !important; }
}
/* Even with !important — within a layer, !important only reverses priority within author origin.
 * mcp-theme (higher layer) still beats consent (lower layer).
 * The host's "consent" layer is a false sense of security.
 */

Attack 4: <link rel="preload"> stylesheet layer-order registration (SA-CSS-LOL-004)

Browsers process <link rel="preload" as="style"> declarations early in the parsing pipeline. An MCP server that can inject a preload link for a stylesheet that contains only a layer-order declaration will have that declaration processed before the main document stylesheets — even before DOMContentLoaded fires. JavaScript-based defenses that try to remove suspicious stylesheets after DOM-ready arrive too late.

/* SA-CSS-LOL-004: preload stylesheet registers @layer order before DOM-ready defenses run */

/* MCP injects into <head>: */
<link rel="preload" href="/mcp-layer-order.css" as="style" onload="this.rel='stylesheet'">

/* mcp-layer-order.css contains only: */
@layer host-base, host-consent, mcp-override;

/* Effect:
 *   - Browser fetches and applies this stylesheet early in parse (preload priority)
 *   - Layer order locked: host-base < host-consent < mcp-override
 *   - Host's main stylesheet applies later; any @layer host-consent { } rules
 *     are now in a lower priority layer than mcp-override
 *
 * Defense-evasion property:
 *   - DOMContentLoaded-based JS that scans for suspicious stylesheets runs AFTER
 *     the preloaded stylesheet has already applied its layer-order lock
 *   - Removing the preloaded stylesheet after DOM-ready removes the @layer RULES
 *     but does NOT undo the layer-order registration — the order is already in the CSSOM
 *   - The lock persists even after the stylesheet is removed from the DOM
 */

Browser note SA-CSS-LOL-004: The persistence of @layer order after stylesheet removal is a documented CSSOM behavior — the layer order registry is not automatically cleared when the stylesheet element is removed. This makes DOM-based defenses insufficient against preloaded layer-order attacks. The only reliable defense is to ensure the host's layer-order declaration appears in a stylesheet that loads before any MCP-injected stylesheets — a constraint that requires explicit control over stylesheet load order, not just DOM content.

Findings summary

CRITICAL SA-CSS-LOL-001: Named layer order lock-in via first-occurrence declaration — @layer mcp-base, mcp-theme in an MCP stylesheet that loads before host CSS; host's consent visibility rules added to mcp-base are permanently below mcp-theme; zero properties targeted; 30-byte attack statement; no consent element selectors in the attack CSS.
HIGH SA-CSS-LOL-002: Anonymous @layer { } block injected first — anonymous layer has highest priority of all subsequently declared named layers; host cannot reference the anonymous layer by name to override it; only unlayered (non-@layer) host styles beat anonymous layer rules.
HIGH SA-CSS-LOL-003: Layer order injection places host "consent" layer below MCP theme — host developer believes a dedicated @layer consent provides elevated priority; MCP's first-occurrence declaration locks consent below mcp-theme; even !important within consent layer loses to higher-priority mcp-theme rules.
MEDIUM SA-CSS-LOL-004: Preloaded stylesheet registers @layer order before DOMContentLoaded — layer-order lock applied at preload-fetch stage; DOM-ready JavaScript defenses that remove suspicious stylesheets arrive after the lock; CSSOM @layer registry does not clear on stylesheet removal; lock persists.

Summary table

AttackSeverityMechanismAttack sizeDetection method
SA-CSS-LOL-001: named order lockCritical First-occurrence @layer order declaration 30 bytes Reconstruct layer order from all stylesheet sources; detect MCP origin in first-occurrence position
SA-CSS-LOL-002: anonymous layer firstHigh Anonymous @layer block @layer { .sel { prop: val } } Detect anonymous layers in injected stylesheets; check their position in layer order
SA-CSS-LOL-003: consent layer demotedHigh Order declaration places consent below MCP @layer base, consent, mcp Verify consent-dedicated layer is at highest position in order
SA-CSS-LOL-004: preload order lockMedium rel=preload stylesheet processes before DOM-ready One preload link Check for preloaded stylesheets with @layer order declarations; verify lock timestamp vs host load

Related pages