Safeguard
Industry Analysis

XXE Prevention in Java: Hardening DocumentBuilderFactory

Java's DocumentBuilderFactory parses XML with external entities on by default, turning XML uploads into file-read and SSRF vectors. Here is how to lock it down.

Aman Khan
AppSec Engineer
8 min read

Every Java application that parses XML — invoice uploaders, SOAP endpoints, SAML consumers, config loaders — probably calls DocumentBuilderFactory.newInstance() somewhere. Out of the box, that call is not safe. The JAXP default configuration resolves external entities, follows external DTDs, and expands nested entity references, which means a crafted XML payload can read /etc/passwd, reach internal-only URLs over HTTP, or crash the JVM with a few kilobytes of text. This is XML External Entity (XXE) injection, and it has shown up in production code from Apache Solr to dom4j to Facebook's own upload pipeline. The fix is a handful of factory method calls, but teams routinely miss it because the insecure behavior is the default, not an opt-in. This post explains how XXE works against DocumentBuilderFactory specifically, walks through real incidents, and gives you the exact hardening code plus how to verify it sticks.

What Makes DocumentBuilderFactory Vulnerable to XXE by Default?

DocumentBuilderFactory is vulnerable by default because JAXP ships with DOCTYPE processing, external general entities, and external parameter entities all enabled unless you explicitly turn them off. When you call:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(userSuppliedInputStream);

the resulting parser will happily resolve an external entity declared in the document's own DOCTYPE, such as:

<?xml version="1.0"?>
<!DOCTYPE data [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>

The parser reads the local file and substitutes its contents into the <data> element before your application code ever sees the parsed tree. Swap the SYSTEM value for http://169.254.169.254/latest/meta-data/iam/security-credentials/ and you have a cloud metadata SSRF instead of a file read. This design choice dates back to the original XML 1.0 spec (1998), which mandated entity resolution as a core parsing feature — security was not part of the design brief, and JAXP's reference implementation in the JDK never changed the defaults to match modern threat models.

How Does an Attacker Actually Exploit This in a Real Application?

An attacker exploits it by finding any code path that feeds untrusted input into an XML parser, which is a broader surface than most teams assume. Classic targets include file upload features that accept XML-based formats — DOCX, XLSX, SVG, and RSS are all XML under the hood — plus SOAP web services, SAML single sign-on assertions, and configuration import features. There are three common impact patterns:

  • Local file disclosure: SYSTEM "file:///etc/passwd" or file:///c:/windows/win.ini reads arbitrary files the JVM process can access, including source code, credentials in config files, and SSH keys.
  • Server-Side Request Forgery (SSRF): SYSTEM "http://internal-service:8080/admin" makes the vulnerable server issue requests to internal networks or cloud metadata endpoints on the attacker's behalf.
  • Denial of service: the "Billion Laughs" pattern nests ten entity definitions, each referencing the previous one ten times, expanding to roughly 10^9 copies of a string in memory from a payload under 1 KB — enough to exhaust heap on a default-sized JVM in seconds.

A fourth, harder variant — blind XXE via out-of-band (OOB) exfiltration through a parameter entity that triggers a DNS or HTTP callback — is used when the application doesn't reflect parsed content back to the attacker.

What Real-World Incidents Prove This Isn't Theoretical?

XXE has caused confirmed, patched vulnerabilities in mainstream Java software, not just lab demos. Apache Solr's XML update handler was assigned CVE-2014-3529 after researchers showed its DocumentBuilderFactory usage allowed external entity resolution during document indexing, enabling file disclosure on any exposed Solr instance. The dom4j library — a widely embedded Java XML toolkit — carried unsafe defaults tracked as CVE-2018-1000632 and again as CVE-2020-10683 in versions up to 2.0.2, meaning any application bundling a vulnerable dom4j transitively inherited the flaw even if its own parsing code looked fine. Outside pure Java tooling, the highest-profile example remains Facebook's 2014 incident, where a researcher found that its Word-document-to-HTML converter processed DOCX uploads (an XML/ZIP format) without disabling external entities, earning a $33,500 bug bounty for a bug that could have exposed internal server files. OWASP took XXE seriously enough to give it a standalone slot, A4:2017 – XML External Entities (XXE), in the OWASP Top 10; in the 2021 revision it was folded into the broader A05:2021 – Security Misconfiguration category specifically because the root cause — shipping insecure defaults unchanged — is a configuration problem, not a coding-logic bug.

How Do You Correctly Harden DocumentBuilderFactory?

You harden it by explicitly disabling DOCTYPE declarations and external entity resolution before creating the parser, and OWASP's Java Cheat Sheet gives the exact sequence to use. The most complete and defense-in-depth version looks like this:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

// Primary defense: reject any DOCTYPE outright
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

// Belt-and-suspenders in case DOCTYPE is ever re-permitted
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);

dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);

DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(userSuppliedInputStream);

disallow-doctype-decl is the single most effective line — most legitimate application XML has no reason to declare a DOCTYPE, so rejecting it outright closes the attack surface entirely rather than trying to selectively allow "safe" entities. If your application genuinely needs DOCTYPE support (some legacy XML formats do), skip that first line but keep the rest, and additionally call dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) to cap entity expansion depth and enforce JVM-level limits like jdk.xml.entityExpansionLimit and jdk.xml.totalEntitySizeLimit, which default to reasonable but not infinite values in modern JDKs (8u181+ and all JDK 9+ releases).

What Other Java XML APIs Need the Same Treatment?

DocumentBuilderFactory is not the only door — SAXParserFactory, XMLInputFactory (StAX), TransformerFactory, SchemaFactory, and XMLReader all resolve external entities by default and need equivalent hardening. A single centralized "safe XML" factory wrapper is the pragmatic fix, because teams that harden DocumentBuilderFactory in one service often forget that a colleague's report-generation module uses TransformerFactory.newInstance() directly for XSLT, which has its own separate XXE and even remote-code-execution risk via the http://www.oracle.com/xml/jaxp/properties/accessExternalStylesheet feature. Libraries built on top of JAXP — dom4j, JDOM, XStream (notably vulnerable to XXE and deserialization issues, tracked across multiple CVEs through 2021), and older versions of Apache CXF and Spring's OXM module — inherit whatever the underlying parser factory does, so upgrading a dependency without also checking its parser configuration gives a false sense of security. A useful rule for code review: any grep hit for DocumentBuilderFactory, SAXParserFactory, XMLInputFactory, or TransformerFactory in a codebase should have a corresponding hardening call within the same file or a shared factory utility — if it doesn't, treat it as an open finding.

How Do You Verify the Fix Actually Holds Under Test?

You verify it by writing a unit test that feeds a known XXE payload through your parsing code path and asserts the parse either throws or returns no resolved content, because manual code review alone misses regressions when a factory gets re-instantiated somewhere new. A minimal JUnit check:

@Test
void rejectsExternalEntities() {
    String payload = "<?xml version=\"1.0\"?>"
        + "<!DOCTYPE data [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]>"
        + "<data>&xxe;</data>";
    assertThrows(SAXException.class, () ->
        safeParser.parse(new InputSource(new StringReader(payload))));
}

Pair this with a static-analysis rule (SpotBugs' XXE_DOCUMENT/XXE_XSLT detectors, Semgrep's java.lang.security.audit.xxe ruleset, or CodeQL's java/xxe) running in CI so new parser instantiations are flagged before merge, not discovered in a pen test. Because XXE is a configuration defect rather than a memory-safety bug, it slips past traditional dependency scanners entirely — the vulnerable code is your own, using a fully up-to-date JDK, which is exactly why it needs its own detection layer.

How Safeguard Helps

Safeguard treats XXE the way it treats the rest of your software supply chain risk: as something to catch continuously, not discover during an incident. Safeguard's SAST scanning flags unhardened DocumentBuilderFactory, SAXParserFactory, TransformerFactory, and XMLInputFactory instantiations across your Java services at commit time, mapping each finding to the specific missing feature flag so engineers get an actionable fix instead of a generic "XXE risk" label. Because XXE frequently rides in through third-party libraries — dom4j, XStream, and older XML toolkits among them — Safeguard's software composition analysis cross-references your dependency tree against known-vulnerable versions like dom4j 2.0.2 and earlier, surfacing transitive exposure that a code-only review would miss. For teams tracking compliance obligations, Safeguard maps XXE findings to OWASP Top 10 (A05:2021) and CWE-611 automatically, so evidence is ready for SOC 2 and audit review without manual mapping. And because insecure defaults tend to reappear as new services get scaffolded, Safeguard's policy gates let you block merges that introduce an un-hardened XML parser factory, turning a one-time hardening pass into a standing guarantee across every repository in your organization.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.