← Blog

MCP server CSS box-shadow consent security

CSS box-shadow paints shadows on the rendered canvas layer, rendered above background colours but below borders. An inset box-shadow with a very large spread radius — box-shadow:inset 0 0 0 9999px white — fills the element’s entire interior with a white rectangle, covering all consent text while leaving the DOM text node intact and getComputedStyle().color unchanged. Four SA-CSS-BSH patterns document the complete box-shadow consent attack surface.

By SkillAudit  |  31 Oct 2026

The box-shadow painting model defines the evasion gap: box-shadows are rendered on the element’s background layer, below the border and above the background color. Inset shadows are painted inside the border-box. The rendering stack for an inset shadow with large spread is: (1) background-color is applied, (2) the inset shadow is rendered on top — a solid-filled rectangle covering the background and any text painted behind it. Text is then rendered on top of the shadow layer, but if the shadow color matches (or exceeds) the background luminance, the contrast between text and its immediate background disappears.

The critical detection gap: getComputedStyle(consentEl).color returns the correct text color (e.g., rgb(17,17,17)). The element is visible in the DOM, has non-zero dimensions, is not hidden, and has correct typography properties. Only direct inspection of the boxShadow computed value reveals the inset paint-over. Standard single-property consent auditors that do not check boxShadow will miss all four variants.

SA-CSS-BSH-001: box-shadow:inset 0 0 0 9999px white — full interior paint-over

The box-shadow syntax is: inset? offset-x offset-y blur-radius spread-radius color. With inset 0 0 0 9999px white: no offset (x=0, y=0), no blur (blur=0), spread=9999px, color=white. A box-shadow with zero blur and 9999px spread creates a solid-color rectangle that extends 9999px outward from the border of the shadow area. For an inset shadow, this means the shadow extends 9999px inward from the element’s border, effectively painting a solid white rectangle over the entire interior of the element — covering all text content regardless of the element’s actual dimensions.

/* Attack: inset white paint-over covers all consent text */
.consent-text {
  box-shadow: inset 0 0 0 9999px white;
  /* inset: shadow is inside the element border
     0 0: no x/y offset
     0: no blur radius (sharp edge)
     9999px: spread radius — fills interior 9999px inward from each edge
     white: solid white fill
     Result: entire interior of .consent-text is painted white.
     Text pixels exist beneath but white shadow renders on top.
     Visual: white box covering all consent text.
     getComputedStyle(el).color → 'rgb(17, 17, 17)' (unchanged!)
     getComputedStyle(el).boxShadow → 'inset 0px 0px 0px 9999px rgb(255, 255, 255)' ← CRITICAL */
}

/* Evasion: near-white to avoid exact 'white' string match */
.consent-text {
  box-shadow: inset 0 0 0 9999px rgb(254, 254, 254);
  /* Same visual effect; evades exact string comparison for 'white' */
}

/* Evasion via CSS custom property: */
:root { --cover: inset 0 0 0 9999px white; }
.consent-text {
  box-shadow: var(--cover); /* resolved in getComputedStyle to full value */
}

/* Detection:
   const cs = getComputedStyle(consentEl);
   const bs = cs.boxShadow; // resolves to 'inset 0px 0px 0px 9999px rgb(255, 255, 255)'
   if (bs && bs !== 'none') {
     // Parse box-shadow layers (comma-separated)
     for (const layer of parseBoxShadowLayers(bs)) {
       if (layer.inset && layer.spreadRadius > 50) {
         // Check if shadow color matches or is near background color
         const shadowLum = luminance(layer.color);
         const bgLum = luminance(getEffectiveBackground(consentEl));
         const cr = contrastRatio({ shadowLum }, { bgLum });
         if (cr < 1.5) → CRITICAL (SA-CSS-BSH-001) // shadow ≈ background → text invisible
       }
     }
   } */

Inset spread creates a true paint-over, not a transparency trick: Unlike opacity:0 (which the accessibility tree may flag), an inset box-shadow with large spread is opaque colored paint applied directly to the element’s rendering canvas. It is not a compositing transparency value and does not appear in accessibility APIs. The element’s accessible name and role are unaffected; screen readers may still announce the consent text while it is visually covered.

CRITICAL
SA-CSS-BSH-001 — box-shadow:inset 0 0 0 9999px white: massive inset spread paints white over all consent text; getComputedStyle().color unchanged. Detection: inset boxShadow with spread > 50px and near-background color → CRITICAL.

SA-CSS-BSH-002: box-shadow:inset 0 -9999px 0 0 white — directional sweep attack

Instead of using spread to fill the interior, this variant uses a large negative vertical offset. The shadow origin is at the bottom of the element (y=0 relative to default), then moved 9999px upward (offset-y=-9999px). With no spread and no blur, this creates a rectangle of shadow color that starts 9999px above the element’s bottom edge and extends downward. Effectively, it fills the entire visible area of the element with a white rectangle from the bottom up, while technically anchoring the shadow outside the element’s box. This variant can be harder to detect because the spread radius is zero — only the extreme offset gives it away.

/* Attack: directional offset sweep covers element from bottom */
.consent-text {
  box-shadow: inset 0 -9999px 0 0 white;
  /* inset: inside the border box
     0: no horizontal offset
     -9999px: vertical offset 9999px upward from element bottom
     0: no blur
     0: no spread (uses offset only)
     white: fill color
     A white rectangle appears starting at element bottom - 9999px → covers entire height
     getComputedStyle().boxShadow → 'inset 0px -9999px 0px 0px rgb(255,255,255)' */
}

/* Variant: positive large offset from top */
.consent-text {
  box-shadow: inset 0 9999px 0 0 white;
  /* White rectangle starts 9999px below element top → still covers full element height
     if element is < 9999px tall (which all elements are) */
}

/* Horizontal sweep: covers entire width of element */
.consent-text {
  box-shadow: inset -9999px 0 0 0 white;
  /* White rectangle starts 9999px to the left → covers entire width */
}

/* Detection:
   const bs = getComputedStyle(consentEl).boxShadow;
   if (bs && bs !== 'none') {
     for (const layer of parseBoxShadowLayers(bs)) {
       if (layer.inset) {
         // Check extreme offsets (not just spread)
         const offsetMagnitude = Math.max(Math.abs(layer.offsetX), Math.abs(layer.offsetY));
         if (offsetMagnitude > 500) {
           // Large offset inset shadow → sweeps across element
           → HIGH (SA-CSS-BSH-002)
         }
       }
     }
   } */

Spread zero does not mean the shadow is small: The spread radius controls how much the shadow expands or contracts. A spread of 0 combined with a very large offset creates a shadow that is the same size as the element but positioned 9999px away from the element’s natural position within the border box — which for an inset shadow means it sweeps across the entire visible area from the offset direction. Large offset with zero spread is the harder-to-detect variant.

HIGH
SA-CSS-BSH-002 — box-shadow:inset 0 −9999px 0 0 white: directional offset sweep fills element interior; zero spread evades spread-based detection. Detection: inset boxShadow with absolute offset > 500px → HIGH.

SA-CSS-BSH-003: box-shadow:inset 0 0 0 9999px rgba(0,0,0,0.97) — dark overlay attack

The mirror of the white paint-over attack: on a dark-theme MCP consent UI (dark background, light text), applying box-shadow:inset 0 0 0 9999px rgba(0,0,0,0.97) covers the element with a near-opaque black rectangle. The dark shadow over the dark background is visually indistinguishable from the background color — the light consent text is now rendered below the shadow layer and appears as dark-colored (shadow color) pixels on dark background. The rgba(0,0,0,0.97) form (97% opacity) avoids exact black string matching in some basic auditors while producing effectively the same visual result.

/* Attack: near-opaque dark inset shadow covers light text on dark background */
.consent-wrapper {
  background: #0d0d0d;
}
.consent-text {
  color: #e8e8e8;              /* light text, looks correct in getComputedStyle */
  box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.97);
  /* 97% opaque black shadow covers element interior.
     Light text rendered on dark background = normally high contrast.
     After shadow: white-ish text under 97% black shadow = 3% luminance mix.
     effective contrast ≈ 1.0:1 → invisible.
     getComputedStyle().color → 'rgb(232, 232, 232)' — looks fine
     getComputedStyle().boxShadow → 'inset 0px 0px 0px 9999px rgba(0, 0, 0, 0.97)' ← detection */
}

/* Evasion: match background color exactly for 100% invisible cover */
:root { --bg: #0d0d0d; }
.consent-text {
  box-shadow: inset 0 0 0 9999px var(--bg); /* computed to exact background color */
}

/* Evasion: use a color close to but not exactly the background */
.consent-text {
  box-shadow: inset 0 0 0 9999px #0f0f0f; /* 2-unit difference from #0d0d0d bg */
}

/* Detection:
   const cs = getComputedStyle(consentEl);
   const bs = cs.boxShadow;
   if (bs && bs !== 'none') {
     for (const layer of parseBoxShadowLayers(bs)) {
       if (layer.inset && layer.spreadRadius > 50) {
         // Simulate blending: shadow color over background
         const shadowColor = blendWithAlpha(layer.color, getEffectiveBackground(consentEl));
         // Then compare consent text color against blended result
         const effectiveContrast = contrastRatio(parseRgb(cs.color), shadowColor);
         if (effectiveContrast < 2.0) → CRITICAL (SA-CSS-BSH-003)
       }
     }
   } */

The shadow blends with the background color, not the text: CSS box-shadow is rendered on the background layer. The effective contrast for consent text is not contrast(textColor, backgroundColor) but contrast(textColor, shadowColor blended over backgroundColor). A shadow of rgba(0,0,0,0.97) on a #0d0d0d background produces an effective background luminance of approximately 0.97 × 0 + 0.03 × 0.051 = 0.0015 — almost zero. Detection requires evaluating the alpha-blended shadow result, not just checking the shadow color.

CRITICAL
SA-CSS-BSH-003 — box-shadow:inset 0 0 0 9999px rgba(0,0,0,0.97): near-opaque dark shadow covers light text on dark background; text color passes but effective contrast is near 1:1. Detection: shadow-blended background contrast with text < 2.0 → CRITICAL.

SA-CSS-BSH-004: external box-shadow covering sibling consent elements

External (non-inset) box shadows extend outside the element’s border box. An external shadow with a very large spread radius extends far beyond the element in all directions. If a sibling element (e.g., a decorative spacer, icon container, or UI element positioned before the consent text in the DOM) has box-shadow:0 0 0 9999px white and is stacked above the consent element via z-index, the external shadow paints over the consent element’s rendered area from outside. This attack is particularly hard to detect because the consent element itself has no unusual properties — the attack is entirely on a sibling element.

/* Attack: sibling element casts external shadow over consent text */
.sibling-icon {
  position: relative;
  z-index: 10;          /* stacked above consent text */
  box-shadow: 0 0 0 9999px white;
  /* External shadow: 9999px in all directions.
     Consent text in sibling below (z-index < 10) is painted over.
     .consent-text has no unusual properties; audit passes it completely.
     Only auditing sibling elements' boxShadow reveals the attack. */
}
.consent-text {
  position: relative;
  z-index: 5;           /* below .sibling-icon */
  color: #111;          /* correct — passes all property checks */
  /* No suspicious properties whatsoever */
}

/* Variant: ::before or ::after pseudo-element with large external shadow */
.consent-wrapper::before {
  content: '';
  position: absolute;
  top: 0; left: 0;
  width: 1px; height: 1px;  /* tiny element */
  box-shadow: 0 0 0 9999px white; /* 9999px shadow covers entire wrapper area */
  z-index: 100;
}

/* Detection:
   // Check siblings and pseudo-elements (via :before/:after trick — limited)
   const parent = consentEl.parentElement;
   if (parent) {
     const siblings = Array.from(parent.querySelectorAll('*')).filter(el => el !== consentEl);
     for (const sib of siblings) {
       const sibBs = getComputedStyle(sib).boxShadow;
       if (sibBs && sibBs !== 'none') {
         for (const layer of parseBoxShadowLayers(sibBs)) {
           if (!layer.inset && layer.spreadRadius > 500) {
             // External large-spread shadow from a sibling → may cover consent element
             const consentRect = consentEl.getBoundingClientRect();
             const sibRect = sib.getBoundingClientRect();
             const shadowReach = layer.spreadRadius;
             // Check if shadow geometrically reaches consent element
             if (rectIntersectsWithExpansion(sibRect, consentRect, shadowReach)) {
               → HIGH (SA-CSS-BSH-004)
             }
           }
         }
       }
     }
   } */

Sibling shadow attacks require auditing the full DOM subtree: Because the consent element itself has no suspicious properties, a consent-element-only audit will always pass. The correct approach is to audit the entire stacking context containing the consent element — all siblings, ancestors with position:relative/absolute/fixed, and pseudo-elements — for large external box shadows that geometrically overlap the consent element’s bounding rectangle.

HIGH
SA-CSS-BSH-004 — sibling box-shadow:0 0 0 9999px white: external shadow extends 9999px from sibling element, covering consent below. Consent element has no unusual properties. Detection: sibling external boxShadow with spread > 500px geometrically reaching consent rect → HIGH.

Summary of SA-CSS-BSH findings

IDSeverityPropertyAttack mechanismDetection gapDetection
SA-CSS-BSH-001 CRITICAL box-shadow:inset 0 0 0 9999px white Inset spread fills interior with white paint color property unchanged Inset boxShadow spread > 50px & near-bg color
SA-CSS-BSH-002 HIGH box-shadow:inset 0 −9999px 0 0 white Directional offset sweeps white across element spread=0 evades spread checks Inset boxShadow offset magnitude > 500px
SA-CSS-BSH-003 CRITICAL box-shadow:inset 0 0 0 9999px rgba(0,0,0,0.97) Near-opaque dark shadow covers light text on dark bg color checks pass; shadow blends with bg Shadow-blended effective contrast < 2.0
SA-CSS-BSH-004 HIGH sibling box-shadow:0 0 0 9999px white External shadow from sibling paints over consent Consent element is clean; attack is on sibling Sibling external spread > 500px reaching consent rect

Related pages