XXE - XML External Entity Processing
An XML parser is left in a configuration that resolves external entities, so a document the attacker controls can name a SYSTEM entity and make the parser read a local file or issue a request from the server. Findings are reported under API8:2023 Security Misconfiguration.
What Vulkro detects
Vulkro runs several parser-specific XXE detectors. All of them are affirmative configuration shapes rather than taint proofs: the detector reads the parser construction, it does not run the parser.
Python: an lxml.etree.XMLParser constructed with any of resolve_entities=True, no_network=False, or load_dtd=True (High severity, High confidence, no taint required). XXE-PY-001 (High) covers an xml.sax parser that affirmatively enables the external general or parameter entity feature, and is suppressed when the file imports defusedxml.
JavaScript: XXE-JS-001 (High) fires on a libxmljs / libxmljs2 parse called with noent: true, which turns on libxml2 entity substitution. XXE-JS-002 (Medium, heuristic) fires on xmldom / @xmldom/xmldom DOMParser().parseFromString(...), which is Medium precisely because the installed runtime version cannot be read from source.
Go: XXE-GO-001 (Medium, heuristic) fires on a parse through a third-party libxml2 binding. The stdlib encoding/xml never resolves external SYSTEM or DTD entities, so a binding is the only Go XXE vector, and reachability with untrusted input is not proven.
Java: JAVA-XXE-001 (High severity, Medium confidence) fires when a JAXP DocumentBuilderFactory / SAXParserFactory / XMLInputFactory / TransformerFactory, a dom4j SAXReader, or a JDOM SAXBuilder parses a request-derived value in a file where no hardening feature runs. If a hardening call runs unconditionally in the same file the detector stays silent, because a single textual pass cannot prove which parser instance it guards. Hardening fenced behind a branch whose opening brace sits on the branch line (if (securityEnabled) { xif.setProperty(ACCESS_EXTERNAL_DTD, ""); }) does NOT count: it leaves the parser wide open on every path that skips it, and every caller may pass false. Allman brace style, where the brace sits on its own following line, is a known gap and still suppresses the file.
Two companion Java rules cover the case where the parser and the request are in different files, which is how the shape usually appears in real code. JAVA-XXE-002 (Medium severity, Medium confidence) reports the parser site on its own terms: a method that builds an XML parser in its own body and parses one of its own PARAMETERS, with no unconditional hardening, is an unsafe parsing helper whatever the caller passes. No taint is required, which is the point - the caller may be anywhere. JAVA-XXE-003 (High severity, Medium confidence) then makes one hop outward and reports call sites that hand a request-derived value to a helper JAVA-XXE-002 proved unhardened. The hop is matched by three things together: the call receiver must resolve to the helper's own type (the type itself for a static call, or a variable the calling file declares with it), the method name must match, and the argument must be request-tainted at the call site. Method name alone is not enough, because helper names like parse, read and unmarshal collide with the JDK. It is still a textual match rather than a resolved call graph, so an unrelated overload on the same type remains possible: that is what holds it at Medium confidence. A receiver the pass cannot resolve is skipped rather than assumed, so a helper reached through a chained expression is reported by JAVA-XXE-002 at the parser and not by JAVA-XXE-003 at the call site.
What this does not claim: a finding says the parser is configured to resolve external entities (or, for Java, that no hardening is visible in the file), not that a working exploit exists. The entity-limit / billion-laughs denial-of-service variant is a separate, lower-severity reliability signal.
Remediation. Use the hardened wrapper for your language: defusedxml in Python, a factory with disallow-doctype-decl and FEATURE_SECURE_PROCESSING set in Java, libxml2 parsed without NOENT and with network access off, or the Go stdlib encoding/xml.
Non-compliant code (examples)
Python - lxml parser with every protection flipped off
from lxml import etree
parser = etree.XMLParser(resolve_entities=True, load_dtd=True, no_network=False)
doc = etree.fromstring(request_body, parser) # SYSTEM entities resolve
Compliant code (examples)
Python - defusedxml, or lxml with the knobs set safely
from defusedxml.lxml import fromstring
doc = fromstring(request_body)
# If lxml must stay, be explicit:
# parser = etree.XMLParser(resolve_entities=False, load_dtd=False, no_network=True)
See also
- Confidence model - what
High,Medium, andLowmean for findings in this category. - Privacy - what Vulkro does and does not access on your machine.
References
This page is generated by vulkro rules export <out-dir> from Vulkro's built-in detector catalogue. Edits made by hand are overwritten on the next regeneration.