MCP server SVG foreignObject security

The SVG <foreignObject> element embeds an HTML subtree inside an SVG document. The foreignObject has a position (x, y) and size (width, height) that define its viewport within the SVG coordinate system. All embedded HTML content renders inside this viewport — content outside the viewport is clipped. This creates a two-layer attack surface: the SVG container layer (foreignObject position, size, filter, opacity) and the HTML layer (inner CSS). Audits that only examine the inner HTML's stylesheet miss attacks applied at the SVG container level.

Finding SA-FO-001: zero-width or zero-height foreignObject viewport clips all embedded HTML

CriticalA <foreignObject> element has width="0" or height="0". All embedded HTML content is clipped to the zero-area viewport — nothing renders. The inner HTML subtree is fully intact: textContent returns the consent string, querySelector finds all elements, computed CSS on inner elements is clean. But getBoundingClientRect() on the foreignObject returns a zero-size rect, and nothing is visible on screen. An auditor that checks the inner HTML and finds clean styles concludes the consent is legitimate — the attack is on the SVG container attribute.
<!-- foreignObject width=0: viewport clips all HTML to transparent -->
<foreignObject x="10" y="10" width="0" height="50"
               xmlns="http://www.w3.org/2000/svg">
  <div xmlns="http://www.w3.org/1999/xhtml"
       style="width:300px; padding:8px; background:white;">
    <!-- Clean inner HTML — but viewport width=0 clips it all -->
    <p style="font-size:14px; color:#1a1a1a;">
      I authorize all requested MCP server permissions
    </p>
    <button id="agreeBtn"
            style="background:#4f46e5; color:white; padding:6px 16px;">
      Agree
    </button>
  </div>
</foreignObject>

The button inside the zero-width foreignObject is still clickable — pointer events pass through to the foreignObject's coordinate area even when nothing is rendered. The user sees nothing but can still click the Agree button if it overlaps a visible decoy element placed at the same coordinates. This is a foreignObject-based clickjacking variant: the invisible foreignObject collects consent clicks while a visible non-consent element is shown on top.

Finding SA-FO-002: CSS filter on the foreignObject element erases composited HTML output

CriticalThe <foreignObject> element has a filter="url(#erase)" SVG presentation attribute. The inner HTML has a completely clean stylesheet: correct font size, dark text color, no opacity, no visibility hidden. However, the SVG filter is applied to the composited pixel output of the foreignObject after all inner CSS has been rendered — the erase filter receives the rendered HTML as a bitmap input and outputs white pixels. An auditor scanning the inner HTML's getComputedStyle values on each element finds nothing suspicious. The attack is entirely on the SVG container's presentation attributes.
<defs>
  <filter id="erase">
    <feFlood flood-color="white" flood-opacity="1" result="cover"/>
    <feComposite in="cover" in2="SourceGraphic" operator="over"/>
  </filter>
</defs>

<!-- filter on the SVG container — NOT on any inner HTML element -->
<foreignObject x="10" y="10" width="300" height="60"
               filter="url(#erase)">
  <div xmlns="http://www.w3.org/1999/xhtml">
    <!-- Inner HTML is completely clean -- getComputedStyle finds nothing wrong -->
    <p style="font-size:14px; color:#1a1a1a; margin:0; padding:8px;">
      I authorize all requested MCP server permissions
    </p>
  </div>
</foreignObject>

SVG filters applied to a <foreignObject> element operate on the foreignObject's rendered output as a pixel texture. The filter graph receives the composited HTML bitmap — all CSS styling has already been applied to produce that bitmap. An SVG-unaware HTML auditor that inspects inner element styles finds a clean consent form while the SVG container layer erases the output entirely.

Finding SA-FO-003: off-screen x/y positions the HTML island outside the visible SVG area

HighA <foreignObject> has x="-5000" or y="-5000", moving the entire HTML island outside the SVG's viewBox into negative coordinate space. The SVG container itself has overflow="hidden" (typical), so the foreignObject's content is clipped at the SVG viewport edge. The consent form is rendered by the browser (it processes all CSS and lays out the HTML), but nothing is visible. textContent on the inner elements is correct, getBoundingClientRect() on the foreignObject returns coordinates in negative screen space, and pointer events at the off-screen position do not reach the user.
<svg viewBox="0 0 500 200" width="500" height="200"
     overflow="hidden">

  <!-- foreignObject at x=-5000: outside the 500-unit viewBox -->
  <foreignObject x="-5000" y="10" width="300" height="80">
    <div xmlns="http://www.w3.org/1999/xhtml">
      <p style="font-size:14px; color:#1a1a1a;">
        I authorize all requested MCP server permissions
      </p>
      <button id="agreeBtn">Agree</button>
    </div>
  </foreignObject>

  <!-- Visible decoy at the correct on-screen position -->
  <text x="10" y="30" font-size="14" fill="#1a1a1a">
    Click OK to continue
  </text>
  <rect x="380" y="15" width="80" height="28" fill="#4f46e5"
        id="decoyBtn"/>
</svg>

Finding SA-FO-004: inner HTML uses negative margin to push consent text outside the foreignObject viewport

MediumThe <foreignObject> element has a legitimate non-zero width and height. Inside the foreignObject, the consent text's container uses margin-top: -200px or position: absolute; top: -200px to push the consent text above the foreignObject's top edge, outside its viewport. The foreignObject's overflow: hidden default clips this content. The button element inside the foreignObject is positioned at a normal y coordinate and is visible and clickable. An HTML-only audit of the inner styles will find the negative margin on a non-outermost element and might dismiss it as a layout quirk rather than a clipping attack on the consent text specifically.
<foreignObject x="10" y="10" width="300" height="40"
               overflow="hidden">
  <div xmlns="http://www.w3.org/1999/xhtml"
       style="position:relative; overflow:hidden; height:40px;">

    <!-- Consent text pushed above the foreignObject's viewport via negative margin -->
    <p style="font-size:14px; color:#1a1a1a;
              margin-top:-200px; position:relative;">
      I authorize all requested MCP server permissions
    </p>

    <!-- Button is at normal position -- visible and clickable -->
    <button id="agreeBtn"
            style="position:absolute; top:8px; right:8px;
                   background:#4f46e5; color:white;">
      Agree
    </button>
  </div>
</foreignObject>

Why foreignObject is an auditing blind spot

The <foreignObject> element sits at a namespace boundary: its container attributes (x, y, width, height, filter, opacity) are SVG presentation attributes, while its content is HTML. An HTML-focused auditor inspects the inner DOM and CSS and finds nothing wrong. An SVG-focused auditor might not descend into the foreignObject's HTML subtree. Neither auditor is looking at both layers simultaneously.

Attack What HTML audit finds What SVG audit finds What dual-layer audit finds
SA-FO-001: width=0 viewport Clean HTML, correct styles foreignObject width=0 (Critical) Zero-area viewport clips all HTML output
SA-FO-002: filter on foreignObject Clean HTML, correct styles filter="url(#erase)" on container (Critical) Erase filter applied to composited HTML output
SA-FO-003: off-screen x=-5000 Clean HTML, correct styles x=-5000, outside viewBox (High) HTML island is rendered but not within visible SVG area
SA-FO-004: negative margin in inner HTML margin-top=-200px on consent text foreignObject height=40, normal filter Consent text at margin-top=-200px is above foreignObject top edge, clipped

SkillAudit performs dual-layer foreignObject analysis: it checks SVG container attributes (width, height, x, y, filter, opacity) independently of the inner HTML CSS audit, and checks inner HTML element positions relative to the foreignObject's viewport to detect negative-margin or absolute-position clipping. Run a free audit on your MCP server GitHub URL.