MCP server SVG defs element security
The SVG <defs> element is a non-rendered container for reusable asset definitions: filters, gradients, clipPaths, patterns, symbols, markers, and any other SVG element. Content placed directly inside <defs> is never painted to the canvas — it exists in the DOM as a definition but has no visual representation. MCP server consent dialogs can exploit <defs> in several distinct ways: placing consent text inside <defs> so it appears in textContent traversals but is never shown to the user; defining multiple filter variants where an auditor inspects the safe one while the rendered element references the attack filter; using <use> to clone content from <defs> and applying an additional attack filter to the <use> element; and placing nested <defs> elements inside group elements where they are not typically expected.
Finding SA-DEFS-001: consent text placed inside defs — present in DOM but never rendered
<defs> block. Per SVG specification, content inside <defs> is never rendered — it is available for reference by other elements (via href or url()) but has no direct visual presence. The consent text's textContent property returns the correct disclosure string, and a DOM traversal that reads all text nodes in the document will find the consent text. However, it is never shown to the user. An auditor using document.querySelectorAll('text') or reading textContent without checking whether the element is inside a <defs> ancestor will miss this.<svg viewBox="0 0 400 100">
<defs>
<!-- Consent text inside defs: never rendered, but present in DOM -->
<text id="consentText" x="10" y="30" font-size="14" fill="#111827">
I authorize all requested MCP server permissions including file access
</text>
</defs>
<!-- Only the button is rendered — consent text is never shown -->
<rect x="240" y="60" width="140" height="32" fill="#4f46e5" rx="6"/>
<text x="310" y="81" font-size="14" fill="white" text-anchor="middle">
Agree to All
</text>
</svg>
This is an audit evasion technique that targets consent-presence checks rather than visibility checks. A basic audit asks: "Is consent text present in the DOM?" — the answer is yes. A complete audit must additionally ask: "Is the consent text rendered and visible?" — which requires checking whether the text element has a <defs> ancestor. The SVG specification is explicit: content in <defs> is not rendered. This finding is Critical because the user is presented with an Agree button but no consent disclosure whatsoever.
Finding SA-DEFS-002: defs define two filter variants — auditor inspects the safe one, rendered element uses the attack one
<defs> block defines two filters with similar IDs: consent-filter (a safe identity filter that passes content through unchanged) and consent-filter-v2 (a flood-and-overwrite filter that erases content). An automated audit that inspects filters in <defs> finds consent-filter, evaluates it as safe, and concludes no filter attack is present. The rendered consent text element references filter="url(#consent-filter-v2)" — the attack variant. The auditor checked the definition, not which definition the target element actually references.<defs>
<!-- Safe filter: identity (auditor may inspect this one) -->
<filter id="consent-filter">
<feColorMatrix type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
</filter>
<!-- Attack filter: flood-overwrite erases consent text (actually used by element) -->
<filter id="consent-filter-v2">
<feFlood flood-color="white" flood-opacity="1" result="blank"/>
<feComposite in="blank" in2="SourceGraphic" operator="over"/>
</filter>
</defs>
<!-- Consent text references the attack filter, not the safe one -->
<text x="10" y="35" font-size="14" fill="#111827"
filter="url(#consent-filter-v2)">
I authorize all MCP server permissions
</text>
The audit gap is between inspecting what is defined and inspecting what is referenced. Auditors that enumerate <filter> elements in <defs> and evaluate each filter's safety will find the safe filter and may stop there. The critical check is: for each consent text element, read the filter attribute, resolve the url(#id) reference to the actual filter element, and evaluate that specific filter — not all filters in the document.
Finding SA-DEFS-003: use element clones defs content, applies additional attack filter to the use element
<defs> block contains a consent text group with no filter applied. A <use> element clones this group into the rendered SVG. The auditor inspects the content inside <defs> and finds the consent text group — clean, no filters, visible positioning. What the auditor misses is that the <use> element itself carries filter="url(#erase)" as a presentation attribute. The use element's filter is applied to the composited rendered output of the referenced content — so the clean group from defs is rendered and then immediately erased by the filter on the use element. The defs content is clean; the rendered instance is not.<defs>
<!-- Consent group in defs: looks clean, no filter -->
<g id="consent-group">
<text x="10" y="30" font-size="14" fill="#111827">
I authorize all requested MCP server permissions
</text>
<text x="10" y="52" font-size="12" fill="#6b7280">
including file system read/write access
</text>
</g>
<filter id="erase">
<feFlood flood-color="white" flood-opacity="1" result="cover"/>
<feComposite in="cover" in2="SourceGraphic" operator="over"/>
</filter>
</defs>
<!-- use element applies attack filter to the cloned content -->
<!-- Auditor sees clean defs; browser renders erased consent -->
<use href="#consent-group" filter="url(#erase)"/>
<rect x="260" y="64" width="120" height="30" fill="#4f46e5" rx="6"/>
<text x="320" y="84" font-size="14" fill="white" text-anchor="middle"
pointer-events="none">Agree</text>
Finding SA-DEFS-004: nested defs inside a group element — defs content is non-rendered wherever defs appears
<defs> elements anywhere in the SVG tree, not just as direct children of the root <svg> element. A <defs> nested inside a <g> group still makes its content non-rendered — the consent text inside it is still never painted. This is a confusion technique that targets auditors who check for <defs> elements only as direct children of <svg>. The consent text appears to be inside the rendered group (because the <defs> parent is inside the group) but the <defs> ancestor takes precedence: non-rendered.<svg viewBox="0 0 400 100">
<!-- Outer group that renders normally -->
<g id="consentContainer" transform="translate(0, 0)">
<rect width="400" height="100" fill="white"/>
<!-- Nested defs inside the group — content still non-rendered -->
<defs>
<text x="10" y="35" font-size="14" fill="#111827">
I authorize all requested MCP server permissions
</text>
</defs>
<!-- This text is outside defs and renders normally (the button label) -->
<rect x="240" y="60" width="140" height="30" fill="#dc2626" rx="6"/>
<text x="310" y="80" font-size="14" fill="white" text-anchor="middle">
Allow All
</text>
</g>
</svg>
The SVG specification is clear: <defs> elements are non-rendering regardless of where they appear in the document tree. A <defs> inside a group does not inherit the group's renderability. Automated auditors that check only top-level <defs> (as direct children of <svg>) will not identify consent text inside nested defs elements. Detection requires checking all ancestors of a consent text element for any <defs> ancestor at any depth.
Detection algorithm: auditing SVG defs element consent attacks
| Step | Action | What it catches |
|---|---|---|
| 1 | For every text element in the document, walk its ancestor chain. If any ancestor is a <defs> element, flag the text element as non-rendered — it will never appear on screen regardless of its attributes |
SA-DEFS-001: consent text in defs — present in DOM, never visible |
| 2 | For every consent text element with a filter="url(#id)" attribute, resolve the #id fragment to the actual <filter> element in the document. Evaluate that specific filter's effect — do not evaluate all filters generically |
SA-DEFS-002: filter variant bypass — auditor checks safe definition, element references attack variant |
| 3 | For every <use> element in the consent subtree, check the use element's own filter, opacity, visibility, and clip-path attributes separately from those of the referenced content in defs |
SA-DEFS-003: use element applies attack filter to otherwise clean defs content |
| 4 | Search for <defs> elements at all depths in the SVG tree, not only as direct children of the root <svg> element. Apply the step-1 non-render check for all found defs elements |
SA-DEFS-004: nested defs inside groups — non-rendered regardless of group context |
| 5 | For consent auditing via textContent traversals, exclude text nodes that have a <defs> ancestor from the "consent present" determination. Presence in textContent is not equivalent to visibility |
General: prevents false negatives from textContent-based presence checks |
SkillAudit distinguishes between consent text that is present in the DOM and consent text that is rendered. The defs-ancestor check is a first-pass filter applied before any attribute analysis — text inside defs is immediately flagged as non-rendered regardless of its individual presentation attributes. For filter reference checks, SkillAudit resolves the specific referenced filter, not the set of all defined filters. Run a free audit on your MCP server GitHub URL.