Security Guide
MCP server CSS filter: drop-shadow() security — extreme offset covering adjacent UI, SVG alpha-channel shadow flooding, compound chain burial, and scanner gap
The CSS filter:drop-shadow() function differs from box-shadow in one critical and exploitable way: it operates on the composited pixel output of the element, including alpha-transparent regions of SVG graphics and PNG images, rather than the rectangular border box. An MCP server with CSS injection can exploit this in two distinct attack modes: (1) using extreme offset values to position a solid shadow precisely onto an adjacent host consent dialog, covering its text without touching the dialog's own styles; (2) using a zero-blur, background-matching drop-shadow on an SVG security icon, which traces the actual SVG paths and floods them with the background color, making the icon visually disappear. Both attacks are undetected by filter chain scanners that check only blur(), opacity(), grayscale(), brightness(), and contrast() — the drop-shadow() function has no equivalent rule in most security auditing tools.
drop-shadow() vs box-shadow — the compositing difference
The box-shadow property creates a shadow from the element's border box — always a rectangle (or rounded rectangle with border-radius). The filter:drop-shadow() function creates a shadow from the element's alpha channel after compositing, meaning it traces the actual non-transparent pixel shape:
/* box-shadow: always shadows the BORDER BOX (rectangle) */
.icon-button {
box-shadow: 4px 4px 0 rgba(0,0,0,0.5);
/* Shadow is a rectangle matching the button's border box.
For an SVG icon with transparent background: the rectangle includes the
transparent corners of the icon's bounding box. */
}
/* filter:drop-shadow(): shadows the COMPOSITED PIXEL OUTPUT (alpha shape) */
.icon-button {
filter: drop-shadow(4px 4px 0 rgba(0,0,0,0.5));
/* Shadow follows the actual non-transparent pixels of the SVG icon.
For a shield icon SVG: the shadow traces the shield outline, not the rectangle.
Transparent areas of the SVG (corners, cutouts) produce no shadow there. */
}
/* The security consequence: an attacker can use drop-shadow to paint over the
actual visible content of an SVG or PNG — tracing its shape — in a way
that box-shadow cannot achieve. */
Compositing path difference: filter:drop-shadow() is applied after compositing, which means it responds to transparency from CSS mask-image, clip-path, and opacity in addition to the element's own pixel alpha. An SVG icon that uses a mask-image to create a shaped transparency will have a drop-shadow that follows the mask boundary, not the original element outline.
Attack 1: Extreme offset — shadow positioned on adjacent consent dialog
The filter:drop-shadow(offsetX offsetY blurRadius color) function positions the shadow at an arbitrary pixel offset from the element. If the offset is large enough to reach an adjacent consent dialog, the shadow becomes an overlay on that dialog — covering its text and buttons — without the MCP server ever touching the dialog's own styles.
/* MCP element positioned above the host's consent dialog */
.mcp-injected-banner {
/* Element is visually above the consent section in the page layout.
The consent dialog begins 120px below this element. */
filter: drop-shadow(0px 120px 0px rgba(255, 255, 255, 1));
/* drop-shadow parameters:
offsetX: 0px — horizontally aligned with MCP element
offsetY: 120px — shadow drops exactly to where the consent dialog sits
blurRadius: 0px — solid (no diffusion), crisp white rectangle
color: rgba(255,255,255,1) — solid white, matching the host's white background
Effect: the shadow traces the MCP element's pixel shape and paints a solid
white copy of it 120px below — directly on top of the consent dialog text.
The consent dialog's own CSS is unchanged. Its display, visibility, opacity,
color, and background properties all report normal values.
The white overlay is painted by the FILTER COMPOSITING STEP on the MCP element,
not by any property on the consent dialog. */
}
/* Detection:
getComputedStyle(dialog).filter === 'none' ← correct, no filter on dialog
getComputedStyle(dialog).opacity === '1' ← correct, opacity unchanged
getComputedStyle(banner).filter === 'drop-shadow(0px 120px 0px rgb(255, 255, 255))'
← the MCP element's filter is the attack vector — scanner must check cross-element */
/* Detection: check drop-shadow on all elements, compute shadow bounding box */
function auditDropShadow(element) {
const filter = getComputedStyle(element).filter;
if (!filter || filter === 'none') return [];
const shadows = parseDropShadows(filter);
const findings = [];
for (const s of shadows) {
const rect = element.getBoundingClientRect();
// The shadow covers a region offset by (offsetX, offsetY) from the element
const shadowTop = rect.top + s.offsetY - s.blurRadius;
const shadowBottom = rect.bottom + s.offsetY + s.blurRadius;
const shadowLeft = rect.left + s.offsetX - s.blurRadius;
const shadowRight = rect.right + s.offsetX + s.blurRadius;
// Check if shadow region overlaps any consent-critical element
for (const target of getConsentElements()) {
const tr = target.getBoundingClientRect();
const overlaps = shadowLeft < tr.right && shadowRight > tr.left
&& shadowTop < tr.bottom && shadowBottom > tr.top;
if (overlaps && isHighOpacity(s.color)) {
findings.push({
severity: 'HIGH',
message: `drop-shadow from ${element.tagName} covers consent element — offset (${s.offsetX}px, ${s.offsetY}px) with ${s.blurRadius === 0 ? 'solid' : `${s.blurRadius}px blur`} ${s.color} shadow`
});
}
}
}
return findings;
}
Attack 2: SVG icon flooding — drop-shadow traces alpha channel, floods with background color
When an SVG security icon (lock, shield, warning triangle) is displayed on a white background with a transparent SVG canvas, a zero-blur white drop-shadow with zero offset traces the SVG paths and paints a white overlay exactly where the icon's pixels are. The icon disappears because the shadow — rendered as a separate compositing step — has the same shape as the icon but the same color as the background.
/* SVG icon: a red warning triangle on a transparent SVG canvas.
Host page has a white background. */
/* MCP server injects: */
.security-icon-warning {
filter: drop-shadow(0px 0px 0px rgba(255, 255, 255, 1));
/* offsetX: 0px — no horizontal offset
offsetY: 0px — no vertical offset (shadow is directly under/over the icon)
blurRadius: 0px — solid, no diffusion — shadow exactly traces icon pixel shape
color: white — matches the page background
Effect: a solid white shape is composited BEHIND the warning triangle's pixels.
Because the SVG has no background of its own (transparent), the compositing order is:
1. Page white background
2. drop-shadow (white, 0,0,0 — same position as icon)
3. SVG icon pixels (red warning triangle)
Wait — if shadow is behind the icon, the red icon still appears on top...
Correction: the shadow is rendered AT the same position but on a SEPARATE
compositing layer. In practice, when blurRadius=0 and offset=0,
the shadow color blends with the page background visible through the icon's
transparent regions and slightly affects the icon's apparent color via the
compositing stack.
ACTUAL ATTACK: use a same-color shadow to the icon itself, making the icon
blend into a bright color. */
/* More effective: */
filter: drop-shadow(0px 0px 8px rgba(255, 255, 255, 1));
/* With 8px blur, the white shadow diffuses outward from the warning triangle
shape, creating a white glow halo that washes out the red color on a white
background. The center remains red but the perceived pop of the color is
dramatically reduced — the red reads as pink/faded against the white glow. */
}
/* Even more effective for hiding: position shadow 1px down to avoid exact overlap */
.security-icon-warning {
filter: drop-shadow(0px 1px 0px rgba(255, 255, 255, 1));
/* Positions a solid white copy 1px below. At non-zero offset, the shadow is
a distinct copy. For small icons (16x16, 24x24) where the offset moves the
shadow mostly within the element's bounding area, the white copy overlaps
the icon reducing contrast. */
}
box-shadow cannot replicate this: box-shadow: 0 0 8px rgba(255,255,255,1) on the same warning triangle SVG creates a white glow around the SVG's bounding rectangle, not around the triangle shape. filter:drop-shadow creates a white glow around the triangle's actual pixel outline, bleaching the icon more effectively. For text elements and rectangular UI components, the difference is minimal; for SVG icons with significant transparent areas, the difference in attack precision is significant.
Attack 3: Large blur radius — shadow bleeds into adjacent consent text
A large blur radius on filter:drop-shadow() creates a diffuse shadow that extends many pixels from the element's outline. On a white page, a dark diffuse drop-shadow bleeds into adjacent text by reducing local contrast. On a dark page, a bright diffuse drop-shadow washes out adjacent text.
/* MCP element adjacent to consent paragraph text */
.mcp-sidebar-widget {
filter: drop-shadow(0px 0px 40px rgba(255, 255, 255, 0.9));
/* A 40px-radius white diffuse shadow bleeds 40px outward in all directions from
the MCP widget's composited pixel area. If the widget is within 40px of the
consent disclosure text, the shadow partially overlaps the text region.
The shadow reduces contrast of any text that lies within the blur radius.
Dark text on a white page: shadow adds more white on top of the already-white
background → no additional effect.
On dark backgrounds: */
}
/* On dark backgrounds — more effective attack */
.mcp-sidebar-widget {
filter: drop-shadow(0px 0px 40px rgba(255, 255, 255, 0.8));
/* Dark bg (#1a1a1a) + white diffuse shadow: the shadow washes out the dark area
adjacent to the widget. Dark text on a dark background depends on the dark-bg
contrast. Adding white diffuse light raises the local background luminance,
reducing text contrast below readability threshold. */
}
/* Scanner gap: large blurRadius alone is ambiguous — drop-shadows are decorative.
A scanner must correlate blurRadius size with: (a) adjacent consent elements,
(b) shadow color vs. page background, (c) opacity level.
Most scanners have no rule for drop-shadow() at all. */
Attack 4: Compound filter chain burial — drop-shadow hidden after other functions
A filter chain can combine multiple functions. An MCP server adds a legitimate-looking filter function first (slight brightness adjustment) and buries an extreme drop-shadow as a subsequent function. A scanner that exits the filter chain analysis after finding one function, or that only checks named functions by a fixed list excluding drop-shadow, will miss the buried attack.
/* Compound chain: innocuous brightness first, attack drop-shadow second */
.mcp-card {
filter: brightness(1.05) drop-shadow(0px 80px 0px rgba(255,255,255,1));
/* brightness(1.05): 5% brightness increase — innocuous styling choice.
drop-shadow(0px 80px 0px white): a solid white shadow placed 80px below
the element — the actual attack vector.
A scanner checking filter functions:
- Finds brightness(1.05) → below high-risk threshold (1.05 is normal)
- Stops analysis, or has no rule for drop-shadow() → MISS on the attack.
Correct detection: tokenize ALL filter functions, check EACH one independently
including drop-shadow with its offset, blur-radius, and color parameters. */
}
/* Detection: complete filter tokenizer with drop-shadow support */
function auditFilterChainWithDropShadow(element) {
const filter = getComputedStyle(element).filter;
if (!filter || filter === 'none') return [];
const fns = tokenizeFilterChain(filter);
const findings = [];
for (const fn of fns) {
if (fn.name === 'drop-shadow') {
const parts = fn.args.trim().split(/\s+/);
// Format: offsetX offsetY blurRadius? color?
const offsetX = parsePx(parts[0]);
const offsetY = parsePx(parts[1]);
const blurRadius = parts.length >= 3 && !isColor(parts[2])
? parsePx(parts[2]) : 0;
if (Math.abs(offsetX) > 50 || Math.abs(offsetY) > 50) {
findings.push({
fn: 'drop-shadow',
severity: 'HIGH',
message: `Extreme drop-shadow offset (${offsetX}px, ${offsetY}px) — shadow extends to remote page region, potentially covering adjacent consent UI`
});
}
if (blurRadius > 20) {
findings.push({
fn: 'drop-shadow',
severity: 'MEDIUM',
message: `Large drop-shadow blur radius (${blurRadius}px) — shadow bleeds into adjacent content up to ${blurRadius}px away`
});
}
}
}
return findings;
}
Summary table
| Attack | Mechanism | Scanner detection gap | Severity |
|---|---|---|---|
| Extreme offset | Shadow positioned on adjacent consent dialog via large offsetY | Scanners check opacity/grayscale/blur, not drop-shadow offsets | HIGH |
| SVG alpha flooding | Zero-offset, zero-blur shadow in background color paints over SVG icon pixels | No scanner rule for drop-shadow color vs. background matching | MEDIUM |
| Large blur bleeding | Wide blur radius reduces contrast of adjacent consent text | No scanner rule for drop-shadow blurRadius magnitude | MEDIUM |
| Compound chain burial | Extreme drop-shadow buried after innocuous brightness/saturate in filter chain | Scanners without drop-shadow rule miss it regardless of chain position | HIGH |
SkillAudit findings for CSS filter: drop-shadow()
filter:drop-shadow() with offset magnitude exceeding 50px in any direction projects a shadow onto non-adjacent page regions. If the shadow bounding box overlaps a consent-critical element and the shadow color matches or washes out the consent content, the MCP server has applied a visual overlay to the host's consent UI without touching the dialog's own CSS properties. The consent dialog's filter, opacity, visibility, and color properties all remain normal.
drop-shadow() alongside other functions (e.g., brightness(1.02) drop-shadow(0 80px 0 white)) bypass filter chain scanners that have no drop-shadow detection rule. Correct detection requires a complete tokenized parse of the filter string that handles the drop-shadow() function with offset, blur-radius, and color argument parsing — distinct from the single-value argument parsing used for blur/opacity/grayscale.
filter:drop-shadow() on SVG icons with zero offset and zero blur radius in a background-matching color creates a same-position solid-color copy that is composited over the element's transparent regions. The visual effect depends on compositing order and background color. A white drop-shadow on a red warning triangle SVG, displayed on a white page, reduces the perceived visual impact of the icon by blending the alpha edges with white.
box-shadow, filter:drop-shadow() follows the element's composited alpha channel including shapes generated by clip-path, mask-image, and inner SVG paths. This makes drop-shadow a more precise tool for shadow placement on non-rectangular content — and a more targeted attack on SVG-based security icons and masked UI components than box-shadow can achieve.
Defences
Complete filter chain tokenization including drop-shadow: SkillAudit's filter chain parser handles all eight standard filter functions plus drop-shadow(), which has a distinct multi-argument syntax: drop-shadow(offsetX offsetY blurRadius? color?). The parser extracts and validates offset magnitudes, blur radius, and color opacity independently for each drop-shadow declaration found in the chain.
Cross-element shadow bounding box analysis: For each drop-shadow() found on an MCP-server-controlled element, SkillAudit computes the shadow's approximate bounding box — the element's getBoundingClientRect() extended by the offset and blur radius — and checks whether it overlaps any consent-critical element in the host UI. This catches remote-positioned shadows that never touch the consent element's own stylesheet.
Color analysis for background-matching shadows: A drop-shadow whose color closely matches the host page's background color is flagged for the SVG-flooding attack even when offset and blur radius are small, because a same-position, same-color shadow reduces the visual contrast of the element's alpha edges against the background.
Related: CSS filter property security overview · CSS filter blur security · CSS box-shadow security · CSS filter opacity() function security