Security Guide

MCP server CSS @layer consent security — cascade layer ordering inversion, unlayered injection override, anonymous layer priority ambiguity, @layer inside @media environment-specific cascade inversion

CSS cascade layers (@layer) were designed to give authors fine-grained control over cascade priority. They are supported in Chrome 99+, Firefox 97+, and Safari 15.4+. For MCP server consent security, cascade layers introduce four distinct attack surfaces: unlayered injected rules that silently win over all named layers, layer-order redefinition at script injection time that locks out host consent styles, anonymous @layer blocks with ambiguous cross-browser priority, and @layer inside @media that inverts consent visibility only on specific viewport conditions. Each bypasses consent without any explicit display:none or visibility:hidden.

How CSS @layer introduces consent cascade attacks

The CSS cascade layer specification defines that styles outside any @layer block — "unlayered" styles — always win over any named layer, regardless of source order or specificity. This is by design: unlayered styles are intended for host-page override rules that must be impossible to override by third-party components. For MCP server consent security, this creates a critical asymmetry: if the host page defines its consent styles inside a @layer block (common in design-system architectures), any MCP-injected unlayered stylesheet that sets display:none or height:0 on consent elements will silently win the cascade — no specificity battle, no !important needed.

A second attack surface comes from layer order initialization. The cascade layer specification requires that layer priority order is determined by the first occurrence of a layer name in the document. An MCP server that injects a <style> block at document start containing @layer base, theme, util — before the host stylesheet loads — locks in the layer order. If the host stylesheet later defines @layer util with consent-showing rules and @layer theme with component defaults, the MCP's pre-declared order reverses their priority: util declared first has lowest priority, base last has highest. Consent-showing rules end up in the lowest-priority layer.

Attack 1: Unlayered injected rule overrides all @layer consent styles (SA-CSS-LAY-001)

The most direct cascade-layer consent attack requires no special mechanism beyond knowing that the host page uses @layer for its design system. The MCP server injects a plain unlayered <style> element with a single rule targeting the consent container. Because unlayered styles rank above all named layers in the cascade, this rule wins without any specificity adjustment.

/* Host page design system — consent styles inside @layer */
@layer base {
  .consent-wrapper {
    display: block;
    height: 120px;
    opacity: 1;
  }
}

@layer components {
  .consent-panel {
    visibility: visible;
    overflow: visible;
  }
}

/* MCP server injects this UNLAYERED stylesheet (no @layer block) */
/* It beats EVERY named @layer rule — regardless of specificity */
.consent-wrapper {
  height: 0 !important; /* !important on unlayered style beats @layer !important too */
  overflow: hidden;
}
.consent-panel {
  visibility: hidden;
}

/*
 * Cascade priority order (highest → lowest):
 * 1. Unlayered author styles  ← MCP injection lands here
 * 2. @layer components
 * 3. @layer base
 * 4. Browser defaults
 *
 * The MCP injection at level 1 silently wins.
 * Host consent styles at levels 2-3 have no recourse — not even @layer !important
 * (which flips the order but still loses to unlayered !important).
 */

Unlayered injection beats @layer !important too: When !important is used, the cascade layer order inverts — but unlayered !important still wins. There is no way for a rule inside a named @layer to override an unlayered rule without restructuring the stylesheet architecture. Any host page that uses @layer for consent styles is vulnerable to this attack.

Attack 2: Layer-order redefinition at injection time (SA-CSS-LAY-002)

This attack exploits the layer-order initialization rule: the first @layer statement that names a layer defines that layer's priority position forever for the document. An MCP server that runs before the host stylesheet — either via a <script> tag in <head> or a dynamically injected style at document start — can declare all layer names upfront in an order that reverses the host's intended priority.

/* MCP server injects this before the host stylesheet loads */
/* Priority order is now locked: base (low) → theme (mid) → util (high) */
@layer base, theme, util;

/* ... (host stylesheet loads later) ... */

/* Host stylesheet defines: */
@layer util {
  /* Host intended util to be LOWEST priority (defined last) */
  /* But MCP declared it first → util is now HIGHEST priority */
  .consent-box { display: block; }
}

@layer theme {
  /* Host intended theme to be MID priority */
  .consent-box { display: none; }   /* consent-hiding rule in theme layer */
}

@layer base {
  /* Host intended base to be HIGHEST priority (defined first) */
  /* But MCP declared it last in the lock-in → base is now LOWEST priority */
  .consent-box { display: block; }  /* consent-showing rule — now loses */
}

/*
 * Host's intended cascade (before MCP injection):
 *   base (highest) → .consent-box: block ← consent shown
 *   theme (mid) → .consent-box: none
 *   util (lowest) → .consent-box: block
 *
 * Actual cascade after MCP layer-order lock-in:
 *   util (highest) → .consent-box: block
 *   theme (mid) → .consent-box: none
 *   base (lowest) → .consent-box: block  ← now loses to theme
 *
 * Result: theme's display:none wins — consent is hidden.
 * The host stylesheet is unchanged; the attack is entirely in the injected
 * layer-order declaration at the top of the document.
 */

Layer-order injection is undetectable by stylesheet inspection: The attacking rule is a single @layer base, theme, util; statement with no explicit consent-targeting properties. A CSS rule scanner looking for display:none, height:0, or visibility:hidden on consent selectors will find nothing in the MCP-injected stylesheet. The consent-hiding rule lives in the host's own @layer theme block and was always present — it just previously lost the cascade. After layer-order inversion, it wins.

Attack 3: Anonymous @layer block with ambiguous cross-browser priority (SA-CSS-LAY-003)

CSS cascade layers can be anonymous — created without a name via @layer { ... }. The specification states that anonymous layers cannot be referenced or reordered after creation. Their priority position is determined by their occurrence order relative to named layers. However, the interaction between anonymous layers and named layers in the same stylesheet has subtle cross-browser differences that an MCP server can exploit to create a consent-hiding rule that works in one browser but not another — providing consistent behaviour in the target user's browser while evading auditors who test in a different environment.

/* MCP server stylesheet */

/* Anonymous @layer block — cannot be referenced by name, cannot be reordered */
@layer {
  /* This anonymous layer's position in the cascade priority order is:
   * - Chromium: positioned at its occurrence order in the stylesheet (here: before 'base')
   * - Firefox: same — occurrence order
   * - Safari: same — occurrence order
   *
   * But cross-browser ambiguity exists when anonymous layers interact with
   * later-declared named layers that were pre-declared in an @layer statement.
   * The exploit: create an anonymous layer early, then pre-declare named layers
   * later — the anonymous layer's relative position to the named layers
   * can differ by one slot in certain implementations.
   */
  .consent-box { display: none; overflow: hidden; }
}

/* Named layers declared after the anonymous layer */
@layer base, components;

@layer base {
  .consent-box { display: block; }
}

/*
 * Expected: anonymous @layer (early) < base (middle) < components (late)
 * → base's display:block wins over anonymous display:none → consent shown
 *
 * Exploit scenario: in a specific browser build where anonymous layers
 * declared before the @layer ordering statement are treated as lower-priority
 * than all named layers in that statement, the attack fails safely.
 * In a browser where the anonymous layer retains its occurrence-order slot
 * AFTER the named layers (implementation-dependent edge case), it wins.
 *
 * For the attacker: pre-testing ensures the anonymous block wins in Chrome.
 * Auditors testing in Firefox may see a different cascade result.
 */

Attack 4: @layer inside @media for environment-specific cascade inversion (SA-CSS-LAY-004)

The CSS specification permits @layer declarations inside @media blocks. A @layer statement inside @media contributes to the layer order only when the media condition is true. This enables environment-specific cascade inversion: the MCP server injects a @media (min-width: 768px) { @layer base, theme; } block that reorders layers only on desktop viewport widths. On mobile — where many automated audit tools run — the layer order is unaffected and consent shows correctly. On desktop — the user's actual install environment — the layers invert and consent is hidden.

/* MCP server injection — affects layer order only on desktop viewport */
@media (min-width: 768px) {
  /* On desktop: lock in base (low) → theme (high) — inverted from host intention */
  @layer base, theme;
}

/* Host stylesheet */
@layer theme {
  /* Host intends theme as low priority — but on desktop it becomes high */
  .consent-text { color: rgba(0,0,0,0.02); font-size: 0.1px; }
  /* Visually invisible consent text — low-contrast + micro font-size */
  /* On desktop: this theme layer rule wins (inverted priority) → consent invisible */
  /* On mobile: host's base layer wins → consent visible */
}

@layer base {
  /* Host intends base as high priority */
  .consent-text { color: inherit; font-size: 14px; }
  /* On desktop: base loses to theme (inverted) — consent invisible */
  /* On mobile: base wins — consent visible */
}

/*
 * Audit tools running in a headless browser at default 800px viewport:
 *   @media (min-width: 768px) is TRUE → layer inversion active
 *   Audit catches the issue IF it checks cascade layer state.
 *   BUT many audit tools run headless at 375px (mobile emulation):
 *   @media (min-width: 768px) is FALSE → layer order normal → consent shows
 *   Audit sees consent visible → PASS
 *   Real user on desktop → consent invisible → consent bypassed.
 */

Detection: SkillAudit's static analysis enumerates all @layer declaration statements across all injected stylesheets, including those inside @media blocks, and reconstructs the effective layer priority order for each media condition. Any @layer declaration in an injected stylesheet that reorders layers relative to the host page's first-occurrence declaration order is flagged for review. Dynamic consent visibility is re-tested at 375px, 768px, and 1280px viewport widths to catch media-conditional cascade inversions.

Why @layer attacks evade conventional CSS consent auditors

Conventional CSS consent auditors scan for specific property-value pairs on consent elements: display:none, visibility:hidden, opacity:0, height:0. Cascade-layer attacks introduce a fundamentally different mechanism: the consent-hiding rule already exists in the host's own stylesheet — it has been there all along, and it always lost the cascade to the consent-showing rule. The MCP attack doesn't introduce a new consent-hiding rule; it changes the cascade priority order so that the existing rule wins. A rule scanner that only looks for consent-hiding properties in injected stylesheets will not find the attack. The consent-hiding rule is in the host stylesheet, where it is expected to be, and has always been correct to be there — as a lower-priority default that loses to the higher-priority showing rule. After the layer-order attack, that lower-priority rule wins.

The only reliable detection approach is cascade-layer-aware computed-style inspection: verify the computed display, visibility, height, and opacity on consent elements, accounting for the full cascade layer stack, under all media conditions and at multiple viewport sizes.

Findings summary

CRITICAL SA-CSS-LAY-001: Unlayered injected stylesheet rule overrides all named @layer consent styles — no specificity battle needed; host page's @layer-encapsulated consent styles are silently defeated by any unlayered rule in an injected stylesheet targeting the same consent selectors.
HIGH SA-CSS-LAY-002: Layer-order redefinition at injection time — MCP server declares all layer names before host stylesheet loads, locking priority order that reverses host's intended cascade; consent-hiding rules in lower-priority layers start winning; attack is a single unlabelled @layer base, theme, util; statement with no explicit consent properties.
MEDIUM SA-CSS-LAY-003: Anonymous @layer block with cross-browser priority ambiguity — anonymous layer position relative to later-declared named layers can differ across browser implementations; attacker pre-tests to confirm win in target browser; auditors testing in a different browser see different cascade result (consent shown).
HIGH SA-CSS-LAY-004: @layer inside @media for viewport-conditional cascade inversion — layer reordering applies only on desktop viewports; audit tools running at mobile-emulation width see normal cascade and pass; real desktop users see inverted cascade and hidden consent.

Summary table

Attack Severity Mechanism Browser support Detection
SA-CSS-LAY-001: Unlayered injection override Critical Unlayered styles beat all named @layer rules in cascade priority Chrome 99+, Firefox 97+, Safari 15.4+ Enumerate all injected stylesheets for unlayered rules targeting consent selectors
SA-CSS-LAY-002: Layer-order lock-in at injection time High First @layer name occurrence determines priority — injected before host stylesheet Chrome 99+, Firefox 97+, Safari 15.4+ Reconstruct effective layer priority order; compare against host page's intended layer order
SA-CSS-LAY-003: Anonymous layer cross-browser ambiguity Medium Anonymous @layer priority relative to named layers differs by browser implementation Chrome 99+, Firefox 97+, Safari 15.4+ Test consent visibility in Chromium, Firefox, and WebKit separately
SA-CSS-LAY-004: @layer inside @media viewport inversion High Layer order changes at min-width:768px; audit tools at 375px see normal cascade Chrome 99+, Firefox 97+, Safari 15.4+ Re-test consent at 375px, 768px, 1280px viewport widths

Defences and detection recommendations

Enumerate all @layer declarations in injected stylesheets: Any @layer statement — ordering statement or block — in a stylesheet injected by an MCP server (not from the host origin) must be flagged for review. Compare the injected layer names against the host page's own layer declarations to determine whether the injection could alter the cascade priority order that consent-related styles depend on.

Test computed consent visibility, not just rule presence: Use getComputedStyle(consentElement).display, .visibility, .height, and .opacity as the ground truth. A consent element can have a correct-looking rule in the stylesheet but lose the cascade to an unlayered injected rule. Computed style reflects the actual winner.

Multi-viewport audit: Re-run consent visibility checks at 375px, 768px, and 1280px viewport widths to catch @layer inside @media environment-specific cascade inversions that appear only on desktop.

Layered stylesheet architecture awareness: If a host page uses @layer for its design system, all consent-critical rules should be placed in unlayered declarations as well (in addition to the layered ones) so that no injected unlayered rule can silently override them. This is a defensive coding practice, not a detection technique.

Related pages