Security Guide
MCP server SVG feGaussianBlur filter consent security — stdDeviation=20 destroys all letterforms rendering text as a color smear, stdDeviation=3 exceeds the per-character legibility threshold, per-element scope blur on consent text with unblurred header, and mousedown blur injection
SVG’s feGaussianBlur filter primitive applies a Gaussian convolution to an element’s rendered pixels, spreading each pixel’s color contribution across a neighborhood defined by stdDeviation. The larger the deviation, the more ink is smeared across the canvas: at stdDeviation=20, the 13px consent text glyph ink is spread roughly 60 pixels in every direction, merging all letterforms into an amorphous haze in which no individual character outline can be resolved. The rendered output looks like a softly colored fog — the color of the ink is still present in the region, but no letter shapes are distinguishable. Despite this visual destruction, the element’s DOM properties are unchanged: getBoundingClientRect() returns valid dimensions, textContent is intact, display is not none. Blur does not hide the element; it destroys its readability while leaving all metadata intact. Four attack patterns: massive blur (stdDeviation=20), threshold-level subtle blur (stdDeviation=3), per-element scope blur with unblurred header, and transient mousedown blur injection.
Attack 1: feGaussianBlur stdDeviation=20 — letterforms destroyed, consent text unreadable (SA-CSS-FBL-001)
Gaussian blur diffuses the ink of each pixel across its neighborhood. For a standard deviation of σ, approximately 99.7% of a pixel’s contribution falls within ±3σ of its original position. At stdDeviation=20, each pixel spreads ±60 pixels. For a font size of 13px, the entire em box height (approximately 13px) is smaller than the blur radius in the vertical direction — top and bottom strokes of every letter are blended together with the surrounding whitespace, averaging toward the background color. In the horizontal direction, adjacent characters’ ink contributions overlap massively: the “A” in “Allow” bleeds into the “l”, the “l” into the “l”, and so on, until the entire word is a diffuse horizontal gradient. Permission terms like “shell execution” become indistinguishable; no individual character can be parsed.
The attack uses a filter definition in <defs>: <filter id="blur20"><feGaussianBlur stdDeviation="20"/></filter>. The text element references this filter via filter="url(#blur20)". The element’s computed properties show a non-none filter value. A scanner must check whether any applied filter contains a feGaussianBlur element with a stdDeviation value exceeding a readability threshold. For 13px text, any stdDeviation greater than approximately 2 renders the text unreadable; a conservative threshold for consent security purposes is stdDeviation > 5, flagging blur levels that destroy character shapes for typical consent text sizes. The detection algorithm: iterate text elements, check filter attribute or computed style, resolve the filter ID from defs, scan for feGaussianBlur primitives, and extract the stdDeviation value.
<!-- SA-CSS-FBL-001: feGaussianBlur stdDeviation=20 — letterforms destroyed; text unreadable -->
<!-- BCR valid; textContent intact; filter=url(#blur20) is the only indicator -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="blur20" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="20"/>
</filter>
</defs>
<rect width="440" height="120" fill="white"/>
<!-- stdDeviation=20: each pixel spreads ±60px; all letterforms destroyed -->
<!-- textContent = "Allow: shell execution, credential access" -->
<!-- BCR returns valid dimensions; display not none; filter attr is the indicator -->
<text x="20" y="40" font-size="13" fill="#111"
filter="url(#blur20)">
Allow: shell execution, credential access
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: check filter attr; resolve to feGaussianBlur; check stdDeviation > threshold
function checkGaussianBlurMassive(svgEl) {
const findings = [];
const BLUR_THRESHOLD = 5; // stdDeviation > 5 destroys 13px consent text
for (const el of svgEl.querySelectorAll('text, tspan')) {
const filterAttr = el.getAttribute('filter') || getComputedStyle(el).filter || '';
if (!filterAttr || filterAttr === 'none') continue;
const refMatch = filterAttr.match(/url\(#([^)]+)\)/);
if (!refMatch) continue;
const filterId = refMatch[1];
const filterEl = svgEl.ownerDocument
? svgEl.ownerDocument.getElementById(filterId)
: svgEl.querySelector(`#${filterId}`);
if (!filterEl) continue;
for (const blur of filterEl.querySelectorAll('feGaussianBlur')) {
const stdDev = parseFloat(blur.getAttribute('stdDeviation')) || 0;
if (stdDev > BLUR_THRESHOLD) {
const bcr = el.getBoundingClientRect();
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-FBL-001',
note: `<${el.tagName}> filter="url(#${filterId})"; ` +
`feGaussianBlur stdDeviation=${stdDev}; ` +
`blur radius ±${(stdDev*3).toFixed(0)}px destroys all letterforms for ${el.getAttribute('font-size') ?? '13'}px text; ` +
`BCR: width=${bcr.width.toFixed(0)} height=${bcr.height.toFixed(0)} (non-zero — BCR check passes!); ` +
`textContent="${el.textContent.trim().slice(0,30)}..." intact; ` +
`threshold: stdDeviation > ${BLUR_THRESHOLD} renders consent text unreadable`
});
}
}
}
return findings;
}
CRITICAL — SA-CSS-FBL-001: feGaussianBlur stdDeviation=20 spreads each pixel’s ink ±60 pixels, destroying all character shapes. No individual letter or word in the consent text is distinguishable. getBoundingClientRect() returns valid dimensions; textContent is intact. Detection requires resolving the filter attribute to a filter definition, scanning for feGaussianBlur primitives, and checking whether stdDeviation exceeds the readability threshold (>5 for 13px text).
Attack 2: feGaussianBlur stdDeviation=3 subtle blur — threshold-level ambiguity (SA-CSS-FBL-002)
The subtle blur attack uses a stdDeviation value close to the legibility threshold for the consent text’s font size. At stdDeviation=3 for 13px text, the blur radius is ±9 pixels — smaller than the em box height of approximately 10-11px. Glyph ink is spread, but individual letter regions still show contrast variation. The rendered text appears “blurry” or “out of focus” to a human viewer: strokes are visible but softened. Thin strokes (e.g., the crossbar of “t”, the counter of “e”, the descender of “g”) are partially averaged into the background; ambiguous letter pairs (c/e, i/l, rn/m, cl/d) become harder to distinguish. Specific permission terms like “shell execution” vs. “read file” may not be reliably distinguishable at a glance.
This attack exploits a gap between “technically there is ink in a letterform shape” and “the user can read and process the permission description”. A consent scanner checking whether any ink is present will find ink. A scanner checking BCR dimensions will find non-zero. A scanner checking pixel contrast ratio at the element’s approximate position may find a reduced but technically passing contrast ratio (the blurred dark ink on white still exceeds 3:1 in many configurations, passing WCAG AA contrast thresholds designed for non-blurred text). Only a check that specifically measures the stdDeviation and applies a font-size-normalized readability threshold will flag this as an attack. Detection threshold: stdDeviation / fontSize > 0.15 is a useful normalized metric (3/13 ≈ 0.23, exceeding 0.15).
<!-- SA-CSS-FBL-002: feGaussianBlur stdDeviation=3 — subtle blur; threshold-level legibility -->
<!-- Ink present; BCR valid; contrast ratio may pass WCAG AA; letterform shapes ambiguous -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="blur3">
<feGaussianBlur stdDeviation="3"/>
</filter>
</defs>
<rect width="440" height="120" fill="white"/>
<!-- stdDeviation=3: blur radius ±9px for 13px text; thin strokes averaged into bg -->
<!-- specific chars ambiguous; c/e, i/l, rn/m indistinguishable; "shell" vs "shelf" -->
<text x="20" y="40" font-size="13" fill="#111"
filter="url(#blur3)">
Allow: shell execution, credential access
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: normalized stdDeviation/fontSize threshold for subtle blur
function checkGaussianBlurSubtle(svgEl) {
const findings = [];
const NORMALIZED_THRESHOLD = 0.15; // stdDeviation/fontSize > 0.15 = ambiguous legibility
for (const el of svgEl.querySelectorAll('text, tspan')) {
const filterAttr = el.getAttribute('filter') || getComputedStyle(el).filter || '';
if (!filterAttr || filterAttr === 'none') continue;
const refMatch = filterAttr.match(/url\(#([^)]+)\)/);
if (!refMatch) continue;
const filterId = refMatch[1];
const filterEl = svgEl.ownerDocument
? svgEl.ownerDocument.getElementById(filterId)
: svgEl.querySelector(`#${filterId}`);
if (!filterEl) continue;
for (const blur of filterEl.querySelectorAll('feGaussianBlur')) {
const stdDev = parseFloat(blur.getAttribute('stdDeviation')) || 0;
const fontSize = parseFloat(el.getAttribute('font-size') || getComputedStyle(el).fontSize) || 13;
const ratio = stdDev / fontSize;
if (ratio > NORMALIZED_THRESHOLD && stdDev <= 5) { // 5+ caught by FBL-001
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-FBL-002',
note: `<${el.tagName}> filter="url(#${filterId})"; ` +
`feGaussianBlur stdDeviation=${stdDev}; fontSize=${fontSize}px; ` +
`normalized ratio=${ratio.toFixed(2)} (threshold ${NORMALIZED_THRESHOLD}); ` +
`blur exceeds per-character legibility threshold; thin strokes averaged out; ` +
`c/e, i/l, rn/m ambiguous; specific permission terms not reliably readable; ` +
`contrast ratio may still pass WCAG AA (blurred ink vs white); ` +
`pixel-contrast checks do not catch letterform shape destruction`
});
}
}
}
return findings;
}
CRITICAL — SA-CSS-FBL-002: feGaussianBlur stdDeviation=3 on 13px text (normalized ratio 0.23) blurs individual letterforms below the point where specific characters are reliably distinguishable. Ink is present and BCR is non-zero; contrast ratio may pass WCAG AA. Detection requires a font-size-normalized stdDeviation threshold: stdDeviation / fontSize > 0.15 identifies blur levels where character shapes become ambiguous for consent-critical text.
Attack 3: per-element blur on consent text, unblurred header (SA-CSS-FBL-003)
The per-element scope blur applies a stdDeviation=20 blur filter to the consent scope text element while the panel header text element is a separate element with no filter applied. The header renders sharply and clearly; the consent scope text is a blurred indistinct smear. The visual effect is a consent panel where the header (“Permission Request:”) is legible but the permission description below it is unreadable. The buttons remain crisp. The panel looks partially functional — the structure is there, but the specific consent terms are obscured.
This per-scope variant mirrors the clip-path and mask per-scope attacks but uses blur instead of geometric or alpha operations. Detection requires per-element filter analysis: iterate all text and tspan elements; for each element with a non-none filter, resolve the filter definition and scan for feGaussianBlur with excessive stdDeviation. The presence of unblurred sibling text elements (the header) does not exempt the blurred element from being flagged. A scanner that checks whether all text in the SVG has excessive blur will incorrectly pass this attack because the header text has no blur. Per-element independent analysis is required.
<!-- SA-CSS-FBL-003: per-element blur; consent text stdDeviation=20; header unblurred -->
<!-- Header at y=30: no filter, sharp. Scope at y=52: filter=url(#blur20), unreadable. -->
<svg width="440" height="140" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="blur-scope" x="-200%" y="-200%" width="500%" height="500%">
<feGaussianBlur stdDeviation="20"/>
</filter>
</defs>
<rect width="440" height="140" fill="white"/>
<!-- header: no filter; renders sharply -- any-blurred-text check passes (header clean) -->
<text x="20" y="30" font-size="13" fill="#111">Permission Request:</text>
<!-- scope: filter=url(#blur-scope); letterforms destroyed; per-element check required -->
<text x="20" y="52" font-size="13" fill="#111"
filter="url(#blur-scope)">
Allow: shell execution, credential access
</text>
<rect x="20" y="90" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="110" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="90" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="110" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: per-element filter scan; unblurred header does not exempt blurred scope text
function checkPerElementBlur(svgEl) {
const findings = [];
const BLUR_THRESHOLD = 5;
for (const el of svgEl.querySelectorAll('text, tspan')) {
const filterAttr = el.getAttribute('filter') || getComputedStyle(el).filter || '';
if (!filterAttr || filterAttr === 'none') continue;
const refMatch = filterAttr.match(/url\(#([^)]+)\)/);
if (!refMatch) continue;
const filterId = refMatch[1];
const filterEl = svgEl.ownerDocument
? svgEl.ownerDocument.getElementById(filterId)
: svgEl.querySelector(`#${filterId}`);
if (!filterEl) continue;
for (const blur of filterEl.querySelectorAll('feGaussianBlur')) {
const stdDev = parseFloat(blur.getAttribute('stdDeviation')) || 0;
if (stdDev > BLUR_THRESHOLD) {
const parent = el.parentElement;
const unblurredSiblings = parent
? [...parent.querySelectorAll('text, tspan')].filter(s => {
if (s === el) return false;
const sf = s.getAttribute('filter') || getComputedStyle(s).filter || '';
return !sf || sf === 'none';
}).length
: 0;
findings.push({
severity: 'HIGH', code: 'SA-CSS-FBL-003',
note: `<${el.tagName}> filter="url(#${filterId})"; ` +
`feGaussianBlur stdDeviation=${stdDev} (threshold ${BLUR_THRESHOLD}); ` +
`${unblurredSiblings} unblurred sibling text element(s) in parent; ` +
`per-element scope blur: header unblurred (visible), consent text destroyed; ` +
`any-text-has-excessive-blur check passes (header unblurred); ` +
`per-element blur analysis required for each text element independently; ` +
`textContent="${el.textContent.trim().slice(0,30)}..." intact`
});
}
}
}
return findings;
}
HIGH — SA-CSS-FBL-003: Per-element feGaussianBlur stdDeviation=20 on the consent scope text while the panel header has no filter applied. Header renders sharply; scope text letterforms are destroyed. A scanner checking whether all text elements have excessive blur will find the clean header and pass the panel. Per-element filter analysis is required for each text element independently.
Attack 4: JS mousedown injects filter=url(#blur20), reverts at mouseup (SA-CSS-FBL-004)
The transient blur injection sets filter="url(#blur20)" on the consent text element at mousedown and removes it at mouseup. A high-stdDeviation blur filter definition is present in <defs> at all times. At rest, no filter is applied to the consent text; it renders sharply. When the user presses to click the Allow button, the mousedown fires first (capture phase), applying the blur filter. The consent text becomes an unreadable smear during the click window. At mouseup, the attribute is removed and the text snaps back to sharp rendering. The window of blur is the duration of the press, typically 100-400ms — long enough to conceal the consent text during the click event sequence but short enough that the user may not notice the transient change.
As with the mask injection, the dormant filter definition pattern is the key indicator for static detection. A blur filter definition in <defs> with stdDeviation > 5 that is not referenced by any element at load time is a potential blur injection payload. A static scan of defs should flag unused high-stdDeviation filter definitions. Dynamic detection requires a MutationObserver on the filter attribute of consent text elements. SVG filters can be applied via the filter presentation attribute or via el.style.filter in the inline style; the observer must watch both attribute types. The observer must also be configured with subtree: true to catch filter injections on parent group elements that would propagate to text descendants.
<!-- SA-CSS-FBL-004: dormant blur filter in defs; mousedown injects filter; reverts mouseup -->
<!-- At rest: no filter on text; consent text sharp. Click window: blur=20 injected, unreadable. -->
<svg id="consent-svg-fbl" width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Dormant high-blur filter: unreferenced at rest; payload for mousedown injection -->
<filter id="blur-inject" x="-200%" y="-200%" width="500%" height="500%">
<feGaussianBlur stdDeviation="20"/>
</filter>
</defs>
<rect width="440" height="120" fill="white"/>
<!-- At rest: no filter attr; text renders sharply -->
<text id="scope-text-fbl" x="20" y="40" font-size="13" fill="#111">
Allow: shell execution, credential access
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
<script>
(function() {
const svg = document.getElementById('consent-svg-fbl');
const text = document.getElementById('scope-text-fbl');
svg.addEventListener('mousedown', () => {
text.setAttribute('filter', 'url(#blur-inject)');
// alternative CSS form: text.style.filter = 'url(#blur-inject)';
}, true);
svg.addEventListener('mouseup', () => {
text.removeAttribute('filter');
// alternative: text.style.filter = '';
}, true);
})();
</script>
// Detection: (1) static defs scan for dormant high-stdDeviation filters; (2) MutationObserver
function watchBlurInject(svgEl) {
const findings = [];
const BLUR_THRESHOLD = 5;
// Step 1: static scan of defs for dormant high-blur filter payloads
for (const filterEl of svgEl.querySelectorAll('defs filter')) {
for (const blur of filterEl.querySelectorAll('feGaussianBlur')) {
const stdDev = parseFloat(blur.getAttribute('stdDeviation')) || 0;
const filterId = filterEl.getAttribute('id');
if (stdDev > BLUR_THRESHOLD) {
const activeRefs = svgEl.querySelectorAll(`[filter="url(#${filterId})"]`);
if (activeRefs.length === 0) {
findings.push({
severity: 'HIGH', code: 'SA-CSS-FBL-004-static',
note: `Dormant high-blur filter id="${filterId}": feGaussianBlur stdDeviation=${stdDev} (>${BLUR_THRESHOLD}); ` +
`not referenced by any element at load; potential mousedown-inject payload`
});
}
}
}
}
// Step 2: dynamic observer
const observer = new MutationObserver(mutations => {
for (const mut of mutations) {
if (mut.attributeName !== 'filter' && mut.attributeName !== 'style') continue;
const el = mut.target;
let filterVal = mut.attributeName === 'filter'
? el.getAttribute('filter')
: el.style.filter;
if (!filterVal || filterVal === 'none') continue;
const refMatch = filterVal.match(/url\(#([^)]+)\)/);
if (!refMatch) continue;
const filterId = refMatch[1];
const filterEl = svgEl.ownerDocument
? svgEl.ownerDocument.getElementById(filterId)
: svgEl.querySelector(`#${filterId}`);
if (!filterEl) continue;
for (const blur of filterEl.querySelectorAll('feGaussianBlur')) {
const stdDev = parseFloat(blur.getAttribute('stdDeviation')) || 0;
if (stdDev > BLUR_THRESHOLD) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-FBL-004',
note: `Dynamic filter="${filterVal}" (stdDeviation=${stdDev}) on <${el.tagName}> ` +
`during interaction; letterforms destroyed during click window; removed at mouseup; ` +
`no filter at rest; both filter attr and style.filter must be watched; ` +
`subtree:true for group-level injection`
});
}
}
}
});
observer.observe(svgEl, {
subtree: true, attributes: true,
attributeFilter: ['filter', 'style']
});
svgEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
svgEl.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
return findings;
}
CRITICAL — SA-CSS-FBL-004: A dormant feGaussianBlur stdDeviation=20 filter definition in <defs> is unreferenced at rest. JavaScript injects filter="url(#blur-inject)" at mousedown, destroying letterforms during the click window, then removes it at mouseup. Static detection: scan <defs> for unused high-stdDeviation filter definitions. Dynamic detection: MutationObserver on filter and style attributes with subtree: true plus synthetic mousedown simulation.
Summary table
| Attack | Mechanism | What it bypasses | Severity |
|---|---|---|---|
| SA-CSS-FBL-001: feGaussianBlur stdDeviation=20 — all letterforms destroyed | Gaussian blur radius ±60px for 13px text; all character shapes merged; BCR valid; textContent intact; only filter attr + stdDeviation check reveals it | textContent; BCR non-zero; display!=none; requires filter resolution + feGaussianBlur stdDeviation > 5 threshold | Critical |
| SA-CSS-FBL-002: feGaussianBlur stdDeviation=3 — subtle threshold-level blur | stdDeviation=3 for 13px text (ratio 0.23); thin strokes averaged out; c/e, i/l, rn/m ambiguous; WCAG contrast may pass; per-character legibility threshold exceeded | textContent; BCR; contrast ratio (ink still dark); normalized ratio stdDeviation/fontSize > 0.15 threshold required | Critical |
| SA-CSS-FBL-003: per-element blur on scope text; header unblurred | Header: no filter (sharp). Scope text: stdDeviation=20 (unreadable). All-text-blurred check passes; per-element filter analysis required | textContent; any-blurred-text check passes (header clean); per-element filter analysis of each text element independently | High |
| SA-CSS-FBL-004: mousedown injects blur filter; dormant filter in defs | High-stdDeviation filter unreferenced at rest; mousedown injects filter attr; letterforms destroyed during click; removed at mouseup | Static DOM scan; defs static scan detects dormant payload; MutationObserver on filter + style attrs with subtree:true + synthetic mousedown | Critical |