How unhardened XML parsers resolve external entity URI references (file://, http://) to leak sensitive server files and trigger internal network port scans.
XML has an old, obscure feature called 'External Entities' that acts like a shortcut variable. When parsing an XML document, you can define a variable that tells the computer: 'Whenever you see &secret;, load the file /etc/passwd from the server hard drive and put its text here'. If your XML parser has this turned on, anyone uploading an XML file can read any file on your computer.
The attacker prepares an XML document containing a custom DOCTYPE declaration defining an external system entity pointing to file:///etc/passwd.
The attacker uploads the XML via an invoice upload endpoint, SAML authentication handler, or legacy SOAP API.
The default parser engine resolves the &xxe; reference by reading /etc/passwd from the local filesystem.
The server includes the parsed XML data in its response, leaking root configuration files or cloud credentials to the attacker.
<!-- VULNERABLE: Malicious XML Payload with External Entity -->
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >
]>
<user>
<name>&xxe;</name>
</user>
# VULNERABLE PYTHON PARSER
from lxml import etree
def parse_xml_invoice(xml_data):
# CRITICAL: Default lxml or unconfigured parsers resolve external entities!
parser = etree.XMLParser(resolve_entities=True)
root = etree.fromstring(xml_data, parser=parser)
return root.find("name").text # Returns contents of /etc/passwd!
# SECURE: Explicitly Disable External DTD Resolution
from defusedxml import ElementTree as SafeET
from lxml import etree
# 1. BEST: Use hardened libraries specifically built to reject XXE & Billion Laughs
def parse_safe_invoice(xml_data):
root = SafeET.fromstring(xml_data)
return root.find("name").text
# 2. STANDARD: Manually configure lxml to completely disallow DTDs and entities
def parse_hardened_lxml(xml_data):
parser = etree.XMLParser(
resolve_entities=False, # Disallow entity resolution
no_network=True, # Disallow network lookups (SSRF defense)
dtd_validation=False, # Disable DTD validation
load_dtd=False # Refuse loading external DTDs
)
root = etree.fromstring(xml_data, parser=parser)
return root.find("name").text
defusedxml.XMLConstants.FEATURE_SECURE_PROCESSING and explicitly set disallow-doctype-decl to true.