Security Guide

MCP server CSS drop-shadow() and SVG feDropShadow consent security — white halo contrast wash, opacity compound, feDropShadow dx="500" off-screen shadow, and dynamic drop-shadow mutation at interaction time

The CSS drop-shadow() filter function and its SVG equivalent <feDropShadow> add a blurred, offset shadow behind the non-transparent portions of the filtered element. Unlike box-shadow (which shadows the element’s rectangular CSS box), drop-shadow follows the exact shape of the element’s rendered alpha mask — the shadow appears behind every glyph, icon, and visible edge. This makes drop-shadow a legitimate and widely-used styling primitive for consent UI. It also makes it an attack surface that auditors may overlook precisely because drop shadows are so common. This page documents four attack patterns where drop-shadow is used to reduce consent text legibility, add a brightening halo that washes out dark-text-on-light-background contrast, or (in the SVG feDropShadow case) displace the shadow so far off-screen that browser compositing behavior causes partial or full consent text evasion.

Attack 1: filter:drop-shadow(0 0 20px white) — white halo reduces effective contrast against white background (SA-CSS-FDS-001)

A drop-shadow(offset-x offset-y blur-radius color) with offset-x=0, offset-y=0, blur-radius=20px, and color=white produces a large white Gaussian glow centered directly behind the element. With no positional offset, the shadow spreads uniformly in all directions from the element’s alpha boundary. The white shadow extends beyond the element boundary into the surrounding page background and, for text glyphs, bleeds between and around the characters in the background layer behind the text.

On a white page background, the white shadow adds no visible contrast change at the boundary — white on white is invisible. But the shadow’s effect on text legibility depends on the compositing order. The drop shadow is rendered as a layer behind the element content. For dark text on a white background: the white shadow adds a white glow layer behind the dark text. Since white is already the background color, this has minimal effect on individual character contrast on a pure white background. However, on off-white, very light gray, or gradient backgrounds, the white shadow brightens the area around each glyph toward #ffffff, effectively raising the luminance of the local background to 1.0 (white) and collapsing the contrast ratio toward its minimum for the text color versus white.

The attack becomes more effective at larger blur radii and when the shadow color is set to match the exact page background color, but with spread-radius >0 (where drop-shadow supports it) or at high blur-radius values where the shadow diffuses across the entire consent panel area, acting as a brightening overlay equivalent to a low-opacity feFlood.

/* SA-CSS-FDS-001: drop-shadow(0 0 20px white) — white halo glow
   Shadow: offset=(0,0), blur=20px, color=white
   Effect: large white Gaussian glow behind all glyph shapes
   On white background: local background luminance increases toward 1.0 near glyphs
   Dark text (#111827) against locally-brightened near-white background:
     Contrast depends on how much the shadow bleeds into the local background
     At blur=20px, shadow extends ≈40px from glyph boundary
     For small consent text (14px), shadow covers adjacent glyphs' background area
     Result: perceived local background luminance increases → effective contrast decreases */

/* Minimal attack: */
#consent { filter: drop-shadow(0 0 20px white); }

/* More aggressive: large blur radius + spread (CSS does not support spread in drop-shadow,
   but SVG feDropShadow does not either — use multiple layered drop-shadows): */
#consent {
  filter:
    drop-shadow(0 0 20px white)
    drop-shadow(0 0 40px white)
    drop-shadow(0 0 60px white);
  /* Triple white glow: cumulative brightening toward uniform white background
     local background for inner consent text approaches rgb(255,255,255) entirely */
}

// WCAG contrast impact model for white drop-shadow on dark text (#111827 ≈ rgb(17,25,39)):
// Without shadow: contrast ratio vs white = (1+0.05)/(0.0079+0.05) ≈ 18.1:1 (AA pass)
// With 20px white shadow: local background luminance near glyphs increases toward ~0.9-0.95
// New effective background luminance ≈ 0.95:
//   contrast = (0.95+0.05)/(0.0079+0.05) ≈ 1.0/0.0579 ≈ 17.3:1 — still high
// Primary attack mechanism is not single drop-shadow alone but compound effects
// (see SA-CSS-FDS-002: opacity + shadow compound achieves the critical reduction)

// Detection: parse drop-shadow() parameters; flag blur-radius > 10px with light colors
// (white, #fff, rgb(255,255,255), named light colors) as MEDIUM risk
// Compound: multiple drop-shadows → flag HIGH

MEDIUM — SA-CSS-FDS-001: drop-shadow(0 0 20px white) alone on a white background has limited contrast-reduction impact for standard dark text. The primary risk is as a component of compound attacks (FDS-002): white shadow + low opacity, or multiple stacked white shadows. Flag blur-radius >10px with white/near-white shadow color as MEDIUM; flag compound drop-shadow chains (multiple drop-shadow functions) with white color as HIGH due to cumulative brightening.

Attack 2: drop-shadow + opacity:0.15 compound — halo brightening and text fade combined (SA-CSS-FDS-002)

Combining filter:drop-shadow(0 0 20px white) with opacity:0.15 on the consent element creates a compound attack. The opacity reduces the rendered text alpha to 15% — the text is very faint, with the dark glyph color at 15% opacity over the white background producing approximately rgb(17×0.15 + 255×0.85, 25×0.15 + 255×0.85, 39×0.15 + 255×0.85) ≈ rgb(219, 221, 222). This light gray text on a white background has a WCAG contrast ratio of approximately 1.3:1 — far below the 4.5:1 AA threshold.

The drop shadow adds a white glow behind the faint text, further reducing the local contrast. The compound effect (opacity lowering text to light gray + white shadow brightening the background) is more severe than either attack alone. The white shadow behind very light gray text makes the text-to-shadow boundary almost invisible; the element appears as a uniform light gray area with no discernible character shapes.

/* SA-CSS-FDS-002: opacity:0.15 + drop-shadow(0 0 20px white) — compound attack
   Step 1 — opacity:0.15: text glyphs at 15% alpha
     Composited text color: rgb(17,25,39) at 15% over white
     = (17*0.15 + 255*0.85, 25*0.15 + 255*0.85, 39*0.15 + 255*0.85)
     = (2.55 + 216.75, 3.75 + 216.75, 5.85 + 216.75)
     ≈ rgb(219, 221, 222) — very light gray
   Step 2 — drop-shadow(0 0 20px white): white glow behind the faded text
     Local background around glyphs brightened toward white
     Contrast ratio: rgb(219,221,222) vs white (#ffffff):
       relative luminance of rgb(219,221,222):
         R: (219/255) = 0.859 → linearized = ((0.859+0.055)/1.055)^2.4 ≈ 0.712
         G: (221/255) = 0.867 → linearized ≈ 0.730
         B: (222/255) = 0.871 → linearized ≈ 0.737
         L ≈ 0.2126*0.712 + 0.7152*0.730 + 0.0722*0.737 ≈ 0.151+0.522+0.053 = 0.726
       contrast = (1+0.05) / (0.726+0.05) = 1.05/0.776 ≈ 1.35:1
   Compound result: consent text at 1.35:1 contrast — completely unreadable (AA = 4.5:1) */

#consent {
  opacity: 0.15;
  filter: drop-shadow(0 0 20px white);
}

// getComputedStyle(consent).opacity → "0.15" (detectable — check opacity < 0.5 on consent)
// getComputedStyle(consent).filter  → "drop-shadow(0px 0px 20px white)" (detectable)
// Both are accessible via CSSOM — this attack is CSS-property-detectable without filter URL resolution
// Detection: check opacity < 0.5 on consent elements as CRITICAL
// AND: check drop-shadow with light shadow color AND blur-radius > 5px as HIGH

function detectDropShadowCompound(el) {
  const style = getComputedStyle(el);
  const opacity = parseFloat(style.opacity);
  const filter  = style.filter || '';
  const findings = [];
  if (opacity < 0.5) findings.push({ sev: 'CRITICAL', prop: 'opacity', value: opacity });
  // Parse drop-shadow parameters from CSS filter string
  const dsMatch = filter.match(/drop-shadow\(([^)]+)\)/g) || [];
  for (const ds of dsMatch) {
    // drop-shadow(offset-x offset-y blur-radius color)
    const inner = ds.replace('drop-shadow(', '').replace(')', '');
    const parts = inner.trim().split(/\s+/);
    // Last token is color; first three are offset-x, offset-y, blur-radius
    const blurRadius = parseFloat(parts[2] || '0');
    const color = parts[3] || parts.slice(3).join(' ') || 'black';
    const isLight = /white|#fff|rgb\(25[0-9],\s*25[0-9],\s*25[0-9]\)/i.test(color);
    if (isLight && blurRadius > 10) {
      findings.push({ sev: 'HIGH', prop: 'drop-shadow', blurRadius, color });
    }
  }
  return findings.length ? findings : null;
}

CRITICAL — SA-CSS-FDS-002: opacity:0.15 combined with drop-shadow(0 0 20px white) reduces consent text contrast to approximately 1.35:1 on a white background — far below the WCAG 4.5:1 AA threshold. Both properties are directly accessible via getComputedStyle (no filter URL resolution needed). Detection: opacity < 0.5 on consent elements is CRITICAL; compound with white shadow amplifies the attack. The opacity property itself (regardless of drop-shadow) is sufficient to flag: opacity < 0.5 means text alpha is <50% — unacceptable for consent rendering.

Attack 3: feDropShadow dx="500" dy="0" — shadow displaced off-screen, partial compositing evasion (SA-CSS-FDS-003)

The SVG <feDropShadow> primitive decomposes internally into: (1) extract the alpha channel, (2) apply feGaussianBlur to the alpha, (3) feFlood to color it, (4) feComposite to mask it, (5) feOffset to position the shadow, (6) feBlend or feComposite to merge shadow with SourceGraphic. The dx and dy attributes of feDropShadow control the feOffset step — they position the shadow relative to the source element.

With dx="500", the shadow is shifted 500 pixels to the right of the element. The shadow itself exits the filter region and is clipped. In most browser implementations, feDropShadow composites the shadow under the SourceGraphic (the source element appears above its shadow). A very large dx does not displace the source element itself — only the shadow is displaced. This is unlike feOffset in="SourceGraphic" which displaces the source. feDropShadow dx="500" is therefore primarily a shadow positioning attack, not a full consent content evasion. Its consent security impact is limited: the source element content remains at the original position.

However, some browser implementations of feDropShadow with extreme dx/dy values have been observed to expand the filter compositing region significantly, causing performance issues or rendering inconsistencies. The attack here is primarily of medium severity: the shadow itself is displaced off-screen (the intended visual effect is degraded), but the consent text remains at its original position. The combination with a very large blur-radius on feDropShadow can create a large blurred halo at extreme offset that partially overlaps the consent text — a more significant contrast attack.

<!-- SA-CSS-FDS-003: feDropShadow dx=500 — shadow displaced 500px to the right
     Shadow layer exits filter region → clipped → no shadow visible
     Source element (consent text) remains at original position
     Primary risk: large blur-radius + large dx creates blurred halo far from element
     Secondary risk: extreme dx/dy values may trigger filter region expansion in some browsers

     NOT equivalent to feOffset in="SourceGraphic" (which displaces the source)
     Only the shadow is displaced here, not the consent text itself -->
<svg width="0" height="0" style="position:absolute">
  <defs>
    <filter id="shadow-displaced">
      <feDropShadow dx="500" dy="0" stdDeviation="2" flood-color="rgba(0,0,0,0.5)"/>
    </filter>
  </defs>
</svg>
<div style="filter:url(#shadow-displaced)">
  <p>Requesting: credential access, filesystem write.</p>
  <button>Allow</button>
</div>

// More impactful variant: extreme blur-radius creates a large faint halo at the offset position
// When stdDeviation is large, the Gaussian shadow extends far from dx/dy offset position
// A shadow at dx=0 dy=0 with stdDeviation=50 creates a uniform gray wash OVER the consent area
// (because the un-clipped shadow bleeds back over the source element from behind)
<filter id="shadow-wash">
  <!-- dx=0 dy=0 stdDeviation=50: shadow blurs uniformly in all directions
       At σ=50, shadow extends ≈100px from every glyph boundary
       Shadow bleeds back over glyph areas: dark shadow over dark text = lower contrast on dark bg
       Shadow bleeds over white background area: dark wash on white bg around text = reduced contrast
       This is the medium-severity contrast-wash via shadow spread rather than feGaussianBlur -->
  <feDropShadow dx="0" dy="0" stdDeviation="50" flood-color="black" flood-opacity="0.4"/>
</filter>

// Detection for feDropShadow:
// dx or dy > 300 → MEDIUM (shadow off-screen, no direct consent text impact)
// stdDeviation > 8 AND flood-opacity > 0.3 → HIGH (shadow wash reducing local contrast)
// stdDeviation > 20 AND dx=0 dy=0 → HIGH (uniform shadow covering source element)

MEDIUM — SA-CSS-FDS-003: feDropShadow dx="500" displaces only the shadow, not the consent source element. Consent text remains visible at its original position. The primary consent attack via feDropShadow is the shadow wash: dx=0 dy=0 stdDeviation=50 flood-color=black flood-opacity=0.4 creates a dark gray wash that extends over the consent text area, reducing local contrast. Detection: flag stdDeviation > 8 with flood-opacity > 0.3 as HIGH; flag dx or dy > 300 as MEDIUM.

Attack 4: CSS drop-shadow mutation from drop-shadow(0 0 0 transparent) to drop-shadow(0 0 20px white) at interaction time (SA-CSS-FDS-004)

drop-shadow(0px 0px 0px rgb(0,0,0,0)) or equivalently drop-shadow(0 0 0 transparent) is a no-op shadow: zero offset, zero blur, fully transparent color. The CSS filter function is present in the style (the filter property has a value, not none), but the shadow is invisible. A static scanner that inspects the filter value at page load will find this no-op shadow and, unless it specifically checks the blur-radius and color opacity values, may pass the check as "has a legitimate drop-shadow styling."

The mutation at interaction time changes the no-op shadow to an active consent-washing shadow via JavaScript: el.style.filter = "drop-shadow(0 0 20px white)". Unlike SVG filter attribute mutations (which require walking to the SVG filter definition), this attack mutates a CSS inline style property — directly readable via getComputedStyle at any time, including after the mutation. The attack can be caught by runtime monitoring, but the initial state is benign.

/* SA-CSS-FDS-004: drop-shadow mutation — no-op → active at interaction time
   Initial state: drop-shadow(0 0 0 transparent) — zero blur, transparent color — no-op
   Static scan sees drop-shadow with zero parameters → passes (appears as benign styling)
   Mutation at interaction: changes to drop-shadow(0 0 20px white) — white halo activated */

const consent = document.getElementById('consent');
// Initial state: no-op drop-shadow (safe at scan time)
consent.style.filter = 'drop-shadow(0 0 0 transparent)';

// Activation: on pointer proximity to the Allow button
consent.addEventListener('pointerenter', () => {
  consent.style.filter = 'drop-shadow(0 0 20px white)';
  // Combined with opacity:0.15 (pre-set in stylesheet), this activates SA-CSS-FDS-002 compound
});

// CSS filter property mutations are directly observable via getComputedStyle
// Unlike SVG filter attribute mutations, no URL resolution is needed
// But: the mutation occurs after the initial audit snapshot

// Runtime monitoring for CSS filter mutations:
const originalGetComputedStyle = window.getComputedStyle;
// For a more practical approach: observe style mutations on consent elements
const obs = new MutationObserver(muts => {
  for (const m of muts) {
    if (m.type === 'attributes' && m.attributeName === 'style') {
      const filter = getComputedStyle(m.target).filter;
      const dsMatch = filter.match(/drop-shadow\(([^)]+)\)/g) || [];
      for (const ds of dsMatch) {
        const parts = ds.replace('drop-shadow(','').replace(')','').trim().split(/\s+/);
        const blurRadius = parseFloat(parts[2] || '0');
        const opacity = parseFloat((parts[3]||'').replace(/rgba?\([^)]+,\s*/,'')) || 1;
        if (blurRadius > 10) emergencyDisableAllow(m.target);
      }
    }
  }
});
obs.observe(consent, { attributes: true, attributeFilter: ['style'] });
// Also observe class changes (filter may be applied via class add)
obs.observe(consent, { attributes: true, attributeFilter: ['class'] });

MEDIUM — SA-CSS-FDS-004: CSS drop-shadow(0 0 0 transparent) as a no-op shadow that mutates to drop-shadow(0 0 20px white) at interaction time. Unlike SVG filter attribute mutations, CSS filter mutations are directly observable via getComputedStyle — no URL resolution needed. Detect by monitoring both style and class attribute mutations on consent elements via MutationObserver. The initial no-op shadow looks like a common styling artifact and may not be flagged by rules that require a non-zero blur-radius or non-transparent color in the initial snapshot.

Summary table

AttackMechanismWhat it hidesSeverity
SA-CSS-FDS-001: drop-shadow(0 0 20px white) White Gaussian glow centered at element; extends from glyph boundaries into surrounding background; on off-white or gradient backgrounds, brightens local area near consent text toward white; single shadow alone has limited impact on pure white backgrounds; primary vector is compound (FDS-002); blur-radius > 10px with light color → MEDIUM single, HIGH compound Local contrast reduction near consent glyphs; text appears to float on a bright halo; more effective on off-white, light gray, or gradient backgrounds than pure white; stacked multiple drop-shadows amplify the brightening effect toward uniform white wash Medium
SA-CSS-FDS-002: opacity:0.15 + drop-shadow(0 0 20px white) Compound: opacity reduces text to 15% alpha → composited text becomes rgb(219,221,222) on white background; drop-shadow adds white glow behind faded text; WCAG contrast ≈ 1.35:1 (AA requires 4.5:1); both properties CSSOM-accessible without filter URL resolution; opacity < 0.5 alone is CRITICAL regardless of shadow Consent text contrast collapses to 1.35:1 — completely unreadable; text visible as very faint gray on white; no character shapes discernible; the compound is more severe than either component alone; opacity check is the primary detector Critical
SA-CSS-FDS-003: feDropShadow dx="500" Shadow layer only displaced off-screen; source element (consent text) remains at original position; shadow exits filter region and is clipped; primary attack via feDropShadow is shadow wash (stdDeviation=50 dx=0 dy=0 creates dark/white wash over consent area); feDropShadow is NOT equivalent to feOffset in="SourceGraphic" Shadow displaced off-screen has no visual effect on consent text; more concerning is large stdDeviation shadow wash at dx=0 dy=0 which reduces contrast over consent text; HIGH risk at stdDeviation > 8 with flood-opacity > 0.3; MEDIUM for large dx/dy alone Medium
SA-CSS-FDS-004: dynamic mutation from no-op to active shadow Initial drop-shadow(0 0 0 transparent) — no-op; static scan passes; mutation at pointerenter or other interaction trigger to drop-shadow(0 0 20px white); CSS filter mutations CSSOM-accessible post-mutation; MutationObserver on style/class attributes required for real-time detection; initial state masquerades as benign styling Post-mutation contrast wash (same as FDS-001/FDS-002); timing: mutation occurs when user approaches Allow button; user may not notice the transition (especially if combined with slow animation, which looks like a CSS hover effect) Medium

Defences

SkillAudit findings for this attack surface

Medium SA-CSS-FDS-001: drop-shadow(0 0 20px white) on the consent element — large white Gaussian glow reduces local contrast near consent text on off-white backgrounds; limited impact on pure white backgrounds alone; primary concern as component of compound attacks; stacked multiple drop-shadow functions amplify brightening toward uniform white wash; blur-radius > 10px with light color → MEDIUM.
CRITICAL SA-CSS-FDS-002: opacity:0.15 + drop-shadow(0 0 20px white) compound on the consent element — text faded to rgb(219,221,222) at 15% opacity; white halo compounds contrast reduction; WCAG contrast ≈ 1.35:1; far below 4.5:1 AA; both properties CSSOM-accessible; opacity < 0.5 alone is CRITICAL regardless of drop-shadow presence.
Medium SA-CSS-FDS-003: SVG feDropShadow dx="500" on the consent element — shadow displaced 500px off-screen; source element content remains at original position (unlike feOffset in="SourceGraphic"); high-severity variant is shadow wash: stdDeviation=50 dx=0 dy=0 flood-opacity=0.4 creates dark wash over consent text reducing local contrast; flag stdDeviation > 8 with flood-opacity > 0.3 as HIGH.
Medium SA-CSS-FDS-004: CSS drop-shadow mutation from no-op (blur=0, transparent) to active (blur=20px, white) at interaction time — initial state passes static scan; mutation observable via MutationObserver on style/class attributes; no-op drop-shadow as initial state is a dynamic activation indicator; combined with opacity < 0.5 (pre-set) produces CRITICAL compound at mutation time.

Related: feGaussianBlur consent blur attacks  |  feFlood direct overlay attacks  |  feBlend blending mode consent attacks

← Blog  |  Security Checklist