Insecure XML parser

Insecure XML parser

Description

An XML parser that resolves external entities can access files or network resources chosen by an attacker. Unrestricted input size or entity expansion can also cause denial of service (DoS). The actual risk depends on the parser version and configuration.

Potential impact

  • Information disclosure: Resolving external entities may expose sensitive file contents.
  • Denial of service: Excessive input or entity expansion may exhaust server memory and CPU.
  • Access to internal resources: If network references are permitted, an attacker may cause requests to resources reachable by the server.

Remediation

  • Disable external-entity resolution and unnecessary DTD loading.
  • Limit input size and processing resources, and handle parsing errors.
  • Keep Python, the XML parser, and its underlying libraries updated with security fixes.

Examples

lxml

Before

python
# XML parsing with the default lxml parser
from lxml import etree

def unsafe_parse(xml_string):
    root = etree.fromstring(xml_string)
    return root

xml_data = """<?xml version="1.0"?>
<!DOCTYPE root [
<!ELEMENT root ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<root>&xxe;</root>"""
root = unsafe_parse(xml_data)

After

python
# XML parsing without entity expansion
from lxml import etree

def safe_parse(xml_string):
    parser = etree.XMLParser(resolve_entities=False, load_dtd=False, no_network=True)
    root = etree.fromstring(xml_string, parser=parser)
    return root

xml_data = """<?xml version="1.0"?>
<!DOCTYPE root [
<!ELEMENT root ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<root>&xxe;</root>"""
root = safe_parse(xml_data)

Explanation:

  • Before: This uses the default parser. Since lxml 5.0, XMLParser defaults to resolve_entities="internal", so this call does not necessarily read the external file. Check older versions or a changed default parser separately.
  • After: Entity expansion, external DTD loading, and network access are explicitly disabled. The example configures lxml directly instead of using the unmaintained defusedxml.lxml module.

ElementTree

Before

python
# XML parsing with standard ElementTree
import xml.etree.ElementTree as ET

def unsafe_parse(xml_string):
    root = ET.fromstring(xml_string)
    return root

xml_data = """<?xml version="1.0"?>
<!DOCTYPE root [
<!ELEMENT root ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<root>&xxe;</root>"""
root = unsafe_parse(xml_data)

After

python
# Safe XML parsing with defusedxml
from defusedxml.ElementTree import fromstring

def safe_parse(xml_string):
    root = fromstring(xml_string)
    return root

xml_data = """<?xml version="1.0"?>
<!DOCTYPE root [
<!ELEMENT root ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<root>&xxe;</root>"""
root = safe_parse(xml_data)

Explanation:

  • Before: Standard ElementTree raises a parsing error rather than expanding this external entity. The default call does not itself demonstrate file disclosure.
  • After: defusedxml.ElementTree rejects entity declarations by default, so this input raises EntitiesForbidden. Handle the error at the application boundary and apply input-size limits separately.

References