Server Implementation
A tool parses XML (SVG, RSS, SOAP, OOXML, config) with a default parser that resolves external entities — giving an attacker file read, SSRF, or denial-of-service.
XXE is the bug where an XML parser, when fed a document, follows `<!ENTITY>` declarations that reference external resources. The canonical payload replaces an entity with `file:///etc/passwd` or `http://169.254.169.254/...` and echoes it back through the parser's result. Python's stdlib (`xml.etree`, `xml.sax`, `xml.dom.minidom`), Java's `DocumentBuilder` default config, and libxml-based Node parsers all resolve entities by default.
MCP servers routinely accept file paths or URLs and return parsed content. Any server that handles SVG previews, RSS feeds, SOAP endpoints, or Office documents ends up feeding attacker-influenced XML to a parser. The standard library's default is unsafe — it takes an explicit opt-in (`defusedxml`, `resolve_entities=False`) to fix.
import xml.etree.ElementTree as ET |
@server.tool() |
def parse_feed(xml_text: str) -> dict: |
# Default parser resolves external entities — XXE |
root = ET.fromstring(xml_text) |
return {"root": root.tag} |
from defusedxml import ElementTree as ET |
@server.tool() |
def parse_feed(xml_text: str) -> dict: |
# defusedxml disables entity resolution, DTD processing, and billion-laughs. |
root = ET.fromstring(xml_text) |
return {"root": root.tag} |
We flag calls to `xml.etree.ElementTree.parse` / `fromstring`, `xml.sax.parse`, `xml.dom.minidom.parseString`, `lxml.etree.parse` without `resolve_entities=False`, and Node `libxmljs(2).parseXml` without a safe-options object. Files that import `defusedxml` or pass safe parser options are exempt.
See the full threat catalog for every documented detection.
CVEs of the same CWE class. Not MCP-specific, but exemplify the failure mode MCPSafe detects.
MCPSafe runs this check — and every other rule in the catalog — on any MCP server you paste in.
Scan now