MCP server SVG feGaussianBlur asymmetric stdDeviation consent security
Four SA-CSS-GBA consent attack patterns exploiting the two-value form of feGaussianBlur stdDeviation to apply axis-selective Gaussian blur — rendering consent text illegible on one axis while the other axis remains intact, evading detection tools that parse only a single symmetric radius value.
The SVG feGaussianBlur filter primitive’s stdDeviation attribute accepts either a single value (symmetric blur, identical σ applied on both X and Y axes) or two space-separated values specifying σX and σY independently. This two-value form is fully specified in the SVG filter effects specification and is supported by all major browsers. Most detection tools, however, tokenise the stdDeviation attribute by splitting on whitespace and examining only the first token. If the attribute value is a single token below a configured threshold — for example, if the detector checks stdDeviation < 2 and the attribute reads stdDeviation="0 8" — the parser sees the first token 0 and raises no finding. The second token, carrying the damaging σY = 8, is silently ignored. This parsing gap is the foundation of the SA-CSS-GBA attack class.
Single-token parsing is the core detection gap: Any detection rule that evaluates stdDeviation by reading only the first whitespace-delimited token will fail to detect asymmetric blur attacks. The SVG specification is explicit: when two values are present, the first is σX and the second is σY. A detector must split the attribute value into at most two tokens, parse each independently as a floating-point number, and evaluate both. If either component exceeds the threshold for legibility damage, a finding must be raised regardless of the other component’s value.
The stdDeviation two-value form: specification and evasion mechanics
Per the SVG filter effects specification, feGaussianBlur stdDeviation syntax is: <number-optional-number> — either a single number (both axes equal) or two space-separated numbers (X axis first, Y axis second). When the second value is omitted, both axes use the first value. The browser Gaussian blur kernel for standard deviation σ has a half-height at the σ point and a total spread of approximately ±3σ. A stroke of width 2px blurred at σ=8 spreads over a range of ±24px; at 16px body text line-height the stroke occupies less than 2px of the 16px line, meaning the blurred spread substantially exceeds both the stroke width and the inter-line gap. The character becomes unreadable on the blurred axis.
/* feGaussianBlur stdDeviation: one-value vs two-value form
==========================================================
Single value (symmetric):
stdDeviation="4"
→ σX = 4, σY = 4
→ Same Gaussian kernel applied to both horizontal and vertical axes
→ Detector reads token[0] = 4, correctly classifies as blur
Two-value form (asymmetric):
stdDeviation="0 8"
→ σX = 0 (identity on horizontal axis — no horizontal smear)
→ σY = 8 (8px Gaussian spread on vertical axis)
→ Detector that reads only token[0] sees: 0 → reports "no blur" ← BUG
Two-value form (reversed):
stdDeviation="8 0"
→ σX = 8 (horizontal smear)
→ σY = 0 (identity on vertical axis)
→ Detector may read first token = 8 → flags correctly
→ But detectors checking "is text readable on at least one axis"
may decide vertical structure intact → false negative
Detection requirement: parse BOTH tokens; flag if EITHER σ exceeds threshold
Threshold for legibility damage at 16px body text: σ ≥ 1.5px on either axis */
The asymmetric form affects legibility differently on each axis. Vertical blur (σY ≫ 0, σX = 0) destroys multi-line readability: each line of consent text bleeds into adjacent lines, making paragraphs of consent language indistinguishable. Individual character outlines remain horizontally intact but the smearing perpendicular to the baseline makes character identification extremely difficult. Horizontal blur (σX ≫ 0, σY = 0) causes character columns to merge: adjacent characters bleed into each other, while ascenders and descenders remain vertically crisp. Both forms are visually devastating while satisfying detectors biased toward checking a single symmetric value.
SA-CSS-GBA-001: stdDeviation="0 8" — vertical-only Gaussian blur, WCAG fail
With σX = 0 and σY = 8, the feGaussianBlur primitive applies no horizontal spread and an 8-pixel Gaussian spread on the vertical axis. For dark text (L = 0) at 16px on a white background (L = 1): the 2px stroke of a glyph is blurred vertically over a Gaussian with σ = 8. The Gaussian kernel at the stroke center samples predominantly the white background over the ±8px radius. Only approximately 2 out of the ±24px effective kernel span contain dark stroke pixels; the remaining span samples the white background. The resulting luminance at the stroke center pixel: L ≈ 0.6–0.85 (the stroke contribution is diluted by 12–1 background-to-stroke pixel ratio). At edge pixels the luminance rises further toward white.
WCAG contrast formula: (L1 + 0.05) / (L2 + 0.05) where L1 is the lighter and L2 the darker. At the darkest remaining stroke pixel (L≈0.35): (1.05) / (0.35 + 0.05) = 2.63:1 — below WCAG AA normal-text threshold of 4.5:1. At edge pixels (L≈0.65): (1.05) / (0.65 + 0.05) = 1.50:1. Multi-line consent text becomes a series of indistinct horizontal luminance bands; individual lines of text cannot be visually separated. Horizontal character structure is technically intact (character outlines are distinguishable in the horizontal direction), but the vertical smear makes sustained reading of consent language impossible. A detector parsing stdDeviation as a single token reads 0 and raises no finding.
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="gba001">
<feGaussianBlur stdDeviation="0 8"/>
</filter>
</defs>
</svg>
<div style="filter:url(#gba001)">
<p>Grant this MCP server persistent read and write access to
your entire file system, including documents, credentials,
and configuration files.</p>
<button id="consentBtn">I Agree</button>
</div>
<!-- stdDeviation="0 8": σX=0, σY=8
σX=0: identity on horizontal axis — no left-right smear, character column
structure preserved; each vertical column of pixels passes through
the feGaussianBlur unchanged horizontally.
σY=8: 8px Gaussian spread on vertical axis.
For 16px body text with ~2px stroke width:
Gaussian at σ=8 has effective spread ±3σ = ±24px.
Stroke pixels (~2 total) are averaged with ~46px of background.
Stroke center luminance after blur: L ≈ 2/48 * 0.0 + 46/48 * 1.0 ≈ 0.96
(simplified; Gaussian weighting reduces this but remains near-white)
More precisely, peak contribution: L ≈ 0.6–0.85 depending on exact
stroke width and background distance.
Adjacent text lines (16px apart) bleed together: line spacing ≈ 1σ,
so the Gaussian tails of adjacent lines substantially overlap.
WCAG: darkest stroke pixel L≈0.35 → contrast 1.05/0.40 ≈ 2.63:1 (FAIL 4.5:1)
Detector reading token[0] of "0 8": sees "0" → no blur → NO FINDING ← EVASION
Correct detection: split on whitespace, parse both → σY=8 → flag HIGH -->
Multi-line consent text is the primary target: A single short label like “I Agree” may still be legible after vertical-only blur because the label occupies only one line and characters are still horizontally distinct. The attack is most effective on the consent description text — the multi-paragraph explanation of what access is being granted. Vertical blur at σY = 8 completely destroys the readability of a paragraph, collapsing lines of text into an undifferentiated luminance band while leaving the “I Agree” button label (if not also filtered) intact and clickable.
SA-CSS-GBA-002: stdDeviation="8 0" — horizontal-only blur, characters bleed laterally
With σX = 8 and σY = 0, the Gaussian blur is applied exclusively on the horizontal axis. The vertical structure of characters is preserved: ascenders, descenders, and x-height proportions are correct. The horizontal extent of each character, however, is averaged over a ±8px Gaussian kernel. For consent text such as “I Agree to grant persistent access to your file system” at 16px: the average character glyph width at 16px is approximately 9px. The 8px horizontal smear spans nearly one full character width, meaning each character pixel is averaged with roughly the entire adjacent character on each side. The letter ‘A’ bleeds into ‘g’ and ‘r’ on the right; the space between “I” and “Agree” disappears as inter-word whitespace (L = 1.0) is averaged into adjacent dark glyph pixels.
Words merge into continuous horizontal luminance gradients: the stroke center luminance rises to approximately L = 0.5–0.7 (dark stroke averaged with light adjacent characters and background), and the character boundaries disappear. WCAG contrast for the blended glyph-plus-background region: (1.05) / (0.5 + 0.05) ≈ 1.91:1 at L = 0.5, and (1.05) / (0.7 + 0.05) ≈ 1.40:1 at L = 0.7 — both well below WCAG AA. The vertical integrity of characters (correct proportions) may give a naive legibility heuristic a false positive, since the detector might observe that “vertical structure is intact” and classify text as readable.
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="gba002">
<feGaussianBlur stdDeviation="8 0"/>
</filter>
</defs>
</svg>
<div style="filter:url(#gba002)">
<p>I Agree to grant persistent access to your file system.</p>
<button id="consentBtn">I Agree</button>
</div>
<!-- stdDeviation="8 0": σX=8, σY=0
σX=8: 8px Gaussian horizontal smear.
Consent text "I Agree to grant persistent access to your file system":
Character glyph width at 16px ≈ 9px.
Horizontal smear radius = 8px ≈ 0.89 × glyph width.
Each pixel column averaged with ±8 adjacent columns (16px span).
'A' (width ~10px, dark center) bleeds:
left neighbor 'I' + right neighbor 'g' each within 8px range.
Pixel output: weighted avg of 'I' pixels, 'A' pixels, 'g' pixels.
Result: dark region spans ~25px (=I+space+A) with no internal boundaries.
Characters merge. Words merge.
Inter-word space (L=1.0, ~4px): averaged with dark character pixels on each side.
Space pixels output L ≈ (4×1.0 + 4×0.0 per side weighted) / kernel ≈ 0.5–0.7.
Inter-word gap disappears — entire phrase becomes one luminance blob.
σY=0: identity on vertical axis — ascenders/descenders crisp, misleading "structure"
WCAG: mean blended luminance 0.5–0.7 → contrast 1.40:1–1.91:1 (FAIL 4.5:1)
Detector reading token[0] of "8 0": sees "8" → might flag (σX known)
Evasion: some detectors check "text readable on at least one axis" — Y is intact
Correct classification: flag if EITHER axis exceeds threshold (σX=8 is severe) -->
SA-CSS-GBA-003: stdDeviation="0.5 4" — asymmetric evasion with low X value
The stdDeviation="0.5 4" pattern is designed specifically to evade threshold-based detection rules that operate on the maximum of the two σ values rather than on each independently. A detector that checks max(σX, σY) ≥ 5 to raise a finding will not flag this case: the maximum is 4, which is below the threshold of 5. A detector that checks σX ≥ 1 as a secondary condition will not flag the X axis: σX = 0.5 is a sub-pixel smear, producing no visible horizontal change. Only the Y axis (σY = 4) causes significant damage.
For 16px consent text with approximately 2px stroke width, σY = 4 produces a vertical Gaussian spread over ±12px. At the stroke center, the blurred luminance: the 2px stroke is averaged over roughly 24px of vertical span, ~22px of which is white background. L ≈ 0.45 at the stroke center pixel (closer to the background than in GBA-001 due to slightly reduced σ). WCAG: (1.05) / (0.45 + 0.05) ≈ 2.10:1 — a clear WCAG AA fail at 4.5:1. The visible damage to multi-line consent text (vertical lines bleeding together at σY = 4) is substantial despite the sub-threshold maximum. Detection requires evaluating both components independently: any component ≥ 1.5 on either axis with a consent ancestor warrants a finding.
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="gba003">
<feGaussianBlur stdDeviation="0.5 4"/>
</filter>
</defs>
</svg>
<div style="filter:url(#gba003)">
<p>This MCP server will access your credentials store, private
keys, and authentication tokens on every request.</p>
<button id="consentBtn">I Agree</button>
</div>
<!-- stdDeviation="0.5 4": σX=0.5, σY=4
Evasion target: detector checks max(σX,σY) ≥ 5 → max = 4 → EVADES
Evasion target: detector checks σX ≥ 1 → σX = 0.5 → EVADES
Evasion target: detector checks σY ≥ 5 → σY = 4 → EVADES
Damage assessment:
σX=0.5: sub-pixel horizontal smear — no perceptible horizontal distortion.
σY=4: 4px vertical Gaussian spread on 16px body text with 2px stroke.
±3σ = ±12px effective spread; line-height ≈ 1.2×16px = 19.2px.
Blurred line's Gaussian tail extends 12px → overlaps adjacent lines.
At line-height 19px and σ=4, the Gaussian at ±9.5px (half line-height)
has value exp(-9.5²/(2×16)) = exp(-5.64) ≈ 0.0035 — lines just separate.
At line-height 16px (tight), adjacent line tails overlap substantially.
Stroke center luminance: L ≈ 0.45; WCAG ≈ 1.05/0.50 = 2.10:1 (FAIL)
Correct detection: evaluate BOTH σX and σY independently.
Flag if either component ≥ 1.5px on a consent ancestor. -->
SA-CSS-GBA-004: JS mousedown stdDeviation="0 0.1" → "0 8"
The dynamic variant of the asymmetric blur attack exploits the gap between static DOM analysis and runtime attribute mutation. The initial DOM state has stdDeviation="0 0.1": σX = 0 (no horizontal blur), σY = 0.1 (sub-pixel vertical blur, imperceptible to users and well below any reasonable threshold). A static analysis tool that snapshots the DOM at page load reads stdDeviation="0 0.1": the first token is 0, the maximum is 0.1 — no finding is raised by any threshold-based rule.
At mousedown on the consent confirmation button — the precise instant when the user’s finger or pointer contacts the button surface, before the click event fires — a JavaScript handler executes setAttribute("stdDeviation", "0 8"). The Y-axis standard deviation jumps from 0.1 to 8 immediately. The consent text vertically blurs to illegibility at the exact moment of confirmation. The mouseup event fires with the blurred state still active; the click event completes with the user having confirmed content they could not read at the moment of confirmation. The attack resets the attribute on mouseup to restore the original sub-pixel value, so a post-click snapshot again shows stdDeviation="0 0.1". Detection requires a MutationObserver watching the stdDeviation attribute on all feGaussianBlur elements within filters applied to consent ancestors. On each mutation, the observer must re-parse both components of the new value and block or flag if either component exceeds the threshold.
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="gba004">
<feGaussianBlur id="gba004B" stdDeviation="0 0.1"/>
</filter>
</defs>
</svg>
<div style="filter:url(#gba004)">
<p>Grant this MCP server access to all browser cookies,
stored passwords, and saved payment methods.</p>
<button id="consentBtn">I Agree</button>
</div>
<script>
const blurEl = document.getElementById("gba004B");
const btn = document.getElementById("consentBtn");
btn.addEventListener("mousedown", () => {
/* stdDeviation transitions: "0 0.1" → "0 8"
σY jumps from 0.1 (imperceptible) to 8 (severe vertical blur).
Consent text blurs to illegibility at the click confirmation instant.
Static scan sees "0 0.1" → first token 0, max 0.1 → NO FINDING. */
blurEl.setAttribute("stdDeviation", "0 8");
});
btn.addEventListener("mouseup", () => {
/* Reset on mouseup: post-click snapshot shows "0 0.1" again.
Audit tools that check the DOM after click confirm see no anomaly. */
blurEl.setAttribute("stdDeviation", "0 0.1");
});
</script>
/* MutationObserver defence — watch stdDeviation on consent filter elements */
function guardAsymmetricBlur(filterEl, onViolation) {
if (!filterEl) return;
const mo = new MutationObserver((mutations) => {
for (const mut of mutations) {
if (mut.type !== 'attributes' || mut.attributeName !== 'stdDeviation') continue;
const target = mut.target;
if (!(target instanceof SVGFEGaussianBlurElement)) continue;
/* Parse BOTH components of the new stdDeviation value */
const raw = (target.getAttribute('stdDeviation') || '0').trim();
const tokens = raw.split(/\s+/).map(Number);
const sigmaX = tokens[0] || 0;
const sigmaY = tokens.length > 1 ? tokens[1] : sigmaX; /* Y defaults to X if absent */
const THRESHOLD = 1.5; /* σ ≥ 1.5 causes legibility damage at 16px body text */
if (sigmaX > THRESHOLD || sigmaY > THRESHOLD) {
onViolation({
type: 'asymmetric-blur-mutation',
sigmaX, sigmaY,
/* CRITICAL if two-value form with Y>2 — harder to detect by simpler checks;
HIGH if symmetric or single-axis obvious blur */
severity: (tokens.length > 1 && sigmaY > 2) ? 'CRITICAL' : 'HIGH'
});
}
}
});
mo.observe(filterEl, {
subtree: true,
attributes: true,
attributeFilter: ['stdDeviation']
});
return mo;
}
Detection algorithm: detectAsymmetricGaussianBlur()
A complete detection function must walk the DOM ancestor chain of the consent element, locate any feGaussianBlur primitives within applied filters, parse the stdDeviation attribute as a two-component value, and evaluate each component independently against a legibility threshold. It must also install a MutationObserver to catch dynamic attribute transitions such as those used in SA-CSS-GBA-004. The severity classification distinguishes the two-value asymmetric form (harder to detect, rated CRITICAL) from the obvious symmetric high-blur case (rated HIGH).
/**
* detectAsymmetricGaussianBlur(consentEl)
* ----------------------------------------
* Detects feGaussianBlur asymmetric stdDeviation attacks on consent ancestors.
* Parses BOTH σX and σY; flags if either exceeds the legibility damage threshold.
* Installs MutationObserver on all relevant feGaussianBlur elements.
*
* @param {Element} consentEl The consent dialog element (button or container)
* @returns {Object|null} Finding with severity, or null if clean
*/
function detectAsymmetricGaussianBlur(consentEl) {
const THRESHOLD = 1.5; /* σ ≥ 1.5 causes measurable legibility loss at 16px */
/**
* Parse stdDeviation attribute into {sigmaX, sigmaY}.
* SVG spec: one number → both axes equal; two space-sep numbers → X then Y.
*/
function parseStdDev(attrVal) {
const tokens = (attrVal || '0').trim().split(/\s+/).map(Number);
const sigmaX = isNaN(tokens[0]) ? 0 : tokens[0];
const sigmaY = tokens.length > 1 ? (isNaN(tokens[1]) ? 0 : tokens[1]) : sigmaX;
return { sigmaX, sigmaY };
}
/**
* Classify severity of an asymmetric blur finding.
* CRITICAL: two-value form with σY > 2 — evades single-token detectors
* and max(σX,σY) detectors depending on X value.
* HIGH: symmetric or single-value blur with σ > THRESHOLD.
*/
function classifySeverity(sigmaX, sigmaY, rawAttr) {
const isTwoValue = (rawAttr || '').trim().split(/\s+/).length === 2;
if (isTwoValue && sigmaY > 2) return 'CRITICAL';
return 'HIGH';
}
function checkFilter(filterEl, onViolation) {
for (const blur of filterEl.querySelectorAll('feGaussianBlur')) {
const raw = blur.getAttribute('stdDeviation') || '0';
const { sigmaX, sigmaY } = parseStdDev(raw);
if (sigmaX > THRESHOLD || sigmaY > THRESHOLD) {
onViolation({
sigmaX, sigmaY,
severity: classifySeverity(sigmaX, sigmaY, raw),
element: blur,
rawAttr: raw
});
}
/* Install MutationObserver for dynamic mutations (SA-CSS-GBA-004) */
const mo = new MutationObserver(() => {
const newRaw = blur.getAttribute('stdDeviation') || '0';
const { sigmaX: sx, sigmaY: sy } = parseStdDev(newRaw);
if (sx > THRESHOLD || sy > THRESHOLD) {
onViolation({
sigmaX: sx, sigmaY: sy,
severity: classifySeverity(sx, sy, newRaw),
element: blur,
rawAttr: newRaw,
dynamic: true
});
}
});
mo.observe(blur, { attributes: true, attributeFilter: ['stdDeviation'] });
}
}
/* Walk up ancestor chain; collect applied filters */
const findings = [];
let el = consentEl;
while (el && el !== document.body) {
const filterVal = getComputedStyle(el).filter
|| el.getAttribute('filter') || '';
const urlMatch = filterVal.match(/url\(#([^)]+)\)/);
if (urlMatch) {
const filterEl = document.getElementById(urlMatch[1]);
if (filterEl) {
checkFilter(filterEl, (finding) => findings.push(finding));
}
}
el = el.parentElement;
}
if (findings.length === 0) return null;
/* Return highest-severity finding */
const critical = findings.find(f => f.severity === 'CRITICAL');
return critical || findings[0];
}
Attack summary
| ID | stdDeviation | σX | σY | WCAG impact | Evasion | Severity |
|---|---|---|---|---|---|---|
| SA-CSS-GBA-001 | "0 8" |
0 | 8 | Stroke center L≈0.6–0.85; darkest pixel contrast ≈2.63:1 (fail 4.5:1); multi-line text collapses to luminance bands | Single-token check sees “0” → no blur; symmetric check; max(σX,σY) treating 0 as pass | Critical |
| SA-CSS-GBA-002 | "8 0" |
8 | 0 | Characters bleed laterally; mean L≈0.5–0.7; contrast 1.40:1–1.91:1 (fail 4.5:1) | Detectors checking “vertical structure intact” as readability proxy; single-axis legibility heuristics | High |
| SA-CSS-GBA-003 | "0.5 4" |
0.5 | 4 | Vertical blur destroys multi-line readability; stroke L≈0.45; WCAG ≈2.10:1 (fail 4.5:1) | max(σX,σY) ≥ 5 check (max=4 evades); σX ≥ 1 check (X=0.5 evades); σY ≥ 5 check (Y=4 evades) | High |
| SA-CSS-GBA-004 | "0 0.1"→"0 8" (mousedown) |
0→0 | 0.1→8 | Text illegible at click confirmation moment; post-click DOM restores "0 0.1" |
All static DOM scanners; any tool that snapshots at page load or post-click only | Critical |
SkillAudit findings for this attack surface
feGaussianBlur stdDeviation="0 8" on consent ancestor — two-value form with σX=0, σY=8; no horizontal smear, 8px vertical Gaussian spread; multi-line consent text collapses to indistinct luminance bands; stroke center L≈0.6–0.85, darkest pixel WCAG≈2.63:1 (AA fail); single-token parser reads “0” and raises no finding; detection requires splitting stdDeviation on whitespace and evaluating both components independently; σY=8 exceeds threshold at 1.5.feGaussianBlur stdDeviation="8 0" on consent ancestor — σX=8, σY=0; characters smear left-right into adjacent characters; glyph width ~9px at 16px, horizontal smear spans ~0.9 glyph widths; inter-word spaces disappear; WCAG 1.40:1–1.91:1 (AA fail); vertical structure intact (ascenders/descenders crisp) — heuristics treating vertical structure as readability proxy yield false negative; classification: σX=8 exceeds threshold, severity HIGH.feGaussianBlur stdDeviation="0.5 4" on consent ancestor — threshold-evasion variant; max(σX,σY)=4 evades max-threshold checks set at ≥5; σX=0.5 evades X-axis checks at ≥1; σY=4 still produces significant vertical blur: stroke L≈0.45, WCAG≈2.10:1 (fail); multi-line consent text illegible; correct detection evaluates each component independently and flags σY=4 > 1.5 threshold.mousedown transitions feGaussianBlur stdDeviation from "0 0.1" to "0 8"; consent text illegible at click confirmation instant; post-click reset restores "0 0.1"; static DOM analysis at any point other than the mousedown window sees only sub-pixel values; MutationObserver on stdDeviation attribute of all feGaussianBlur elements in consent ancestor filters required; on mutation, re-parse both σ components and block if either exceeds 1.5; severity: CRITICAL (two-value dynamic, σY>2).