XML in Production · Security and Integration
The Upload That Read the Server's Files
An invoice-upload endpoint parsed XML with external entities enabled. A tester uploaded a document that echoed the server's configuration file back in the error message.
Scenario
Example team: an accounts-payable portal lets suppliers upload invoices as XML. The Java service parses them with a default DocumentBuilderFactory, validates a few fields and, on error, returns a message that includes the invalid value.
What you see in production
A penetration tester uploads an invoice whose <supplier> is &cfg;, with <!ENTITY cfg SYSTEM "file:///app/config/application.properties">. The "invalid supplier" error response contains the database password. A second payload points the entity at the cloud metadata endpoint.
The naive code
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); // defaults
Document invoice = builder.parse(upload);
Reproduce it
The same attack against Python's lxml, with and without safe settings (a local file stands in for the secrets):
from lxml import etree
with open("application.properties", "w") as f:
f.write("db.password=s3cr3t")
upload = b"""<!DOCTYPE invoice [ <!ENTITY cfg SYSTEM "application.properties"> ]>
<invoice><supplier>&cfg;</supplier><total>99.00</total></invoice>"""
def handle(parser):
try:
supplier = etree.fromstring(upload, parser).findtext("supplier")
return f"error: unknown supplier {supplier!r}" # the value is echoed back to the client
except etree.XMLSyntaxError as e:
return f"rejected: {e.msg}"
vulnerable = etree.XMLParser(resolve_entities=True, no_network=False)
hardened = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
print("vulnerable:", handle(vulnerable))
print("hardened: ", handle(hardened))
def reject_doctype(data):
return "rejected: DOCTYPE not allowed" if b"<!DOCTYPE" in data[:1024].upper() else "accepted"
print("pre-check: ", reject_doctype(upload))
Output
vulnerable: error: unknown supplier 'db.password=s3cr3t' hardened: error: unknown supplier '' pre-check: rejected: DOCTYPE not allowed
Root cause
DTDs let a document declare external entities that the parser fetches from files or URLs. Java's default DocumentBuilderFactory, older libxml2 settings and many libraries resolve them. Combined with an error message that echoes input, the parser becomes a file reader and SSRF client for the attacker.
The fix
- Disallow DTDs for untrusted input. Java:
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true). Python:defusedxmlfor the standard library, or lxml withresolve_entities=False, no_network=True. PHP: never passLIBXML_NOENT. .NET:DtdProcessing.Prohibit. - Centralise parser creation in one helper so every endpoint gets the safe configuration.
- Don't echo raw input in error messages.
- Block outbound network access from parsing services (egress rules, IMDSv2 on AWS).
Trade-offs
- Some legitimate formats use DTDs (DocBook, old SGML-derived documents). Allow them only from trusted sources, with a local catalogue and no network access.
- A blanket
<!DOCTYPEpre-check is crude but effective as defence in depth.
Detect it in production
- SAST rules for unsafe parser construction (Semgrep, CodeQL, SpotBugs find-sec-bugs).
- WAF or logs alerting on
<!DOCTYPEand<!ENTITYin request bodies. - Egress logs showing parser hosts calling unexpected URLs.
Key points
- XXE turns an XML parser into a file reader and SSRF client.
- Disable DTDs and external entities for untrusted input, in one shared helper.
- Don't reflect parsed input in errors; restrict egress.
Practice questions
How do you make Java's DocumentBuilderFactory safe against XXE?
Answer
Set the feature http://apache.org/xml/features/disallow-doctype-decl to true (and enable FEATURE_SECURE_PROCESSING, disable XInclude and entity expansion). This rejects any document with a DOCTYPE.
Why can XXE lead to SSRF?
Answer
An external entity can be an http:// URL, so the server's parser makes requests to internal services or cloud metadata endpoints on the attacker's behalf.
Open in the app to mark lessons complete, take notes and track your progress.