Safeguard
Vulnerabilities

Fixing XXE in Java: A Parser-by-Parser Hardening Guide

A parser-by-parser XXE fix for Java, covering DocumentBuilderFactory, SAXParser, XMLInputFactory, TransformerFactory, and the XML libraries that still ship unsafe defaults.

Safeguard Team
Product
Updated 6 min read

The XXE fix in Java is to disable DTD processing and external entity resolution on every XML parser your code creates, because Java's XML factories resolve external entities by default. There is no single global switch. Each factory (DocumentBuilderFactory, SAXParserFactory, XMLInputFactory, TransformerFactory, and third-party readers) needs its own set of feature flags, and missing any one of them leaves that code path exploitable. This guide walks through the exact settings per parser so the xxe fix is complete rather than partial. If you want a concrete XXE example in Java before you dig into the fix, skip ahead to the regression-test section below, which walks through the exact payload a test should send.

Why does Java need a per-parser fix at all?

Java has no shortage of ways to parse XML, and they were written across different eras with different security assumptions. The DOM, SAX, and StAX APIs each expose their own factory, and each honors a different set of feature URIs. A team that hardens DocumentBuilderFactory and calls it done often still has a SAXParser or a TransformerFactory elsewhere quietly resolving entities. That inconsistency is why XXE keeps reappearing in codebases that "already fixed it."

The safest baseline for every parser is the same idea: forbid the DOCTYPE declaration entirely. If your application does not need DTDs, disallowing them blocks both classic and blind XXE in one move. The examples below use disallow-doctype-decl where the parser supports it, with fallbacks for parsers that do not.

How do you harden DocumentBuilderFactory?

DocumentBuilderFactory is the DOM parser and the most common XXE sink in Java web apps.

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.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);

Setting disallow-doctype-decl to true is the strongest control. If you legitimately need doctypes, drop that line but keep the external-entity flags false. setExpandEntityReferences(false) and setXIncludeAware(false) close the remaining gaps.

What about SAXParser and XMLReader?

SAX is a streaming parser with the same defaults problem. Configure the factory before you create the parser.

SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
SAXParser parser = spf.newSAXParser();

If you work with an XMLReader directly, set the same three features on it. The dom4j SAXReader, still common in older Spring and Hibernate-adjacent code, wraps SAX and needs identical treatment:

SAXReader reader = new SAXReader();
reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

How do you fix StAX (XMLInputFactory)?

StAX is the pull parser, and its feature names differ from SAX. Disable DTD support and external entities through its own properties.

XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);

SUPPORT_DTD set to false is the StAX equivalent of disallowing doctypes. Some implementations honor only one of these two properties, so set both.

What about TransformerFactory and XSLT?

XSLT transforms parse XML too, and TransformerFactory plus SchemaFactory and Validator are frequently overlooked. Restrict external access using the JAXP secure-processing properties.

TransformerFactory tf = TransformerFactory.newInstance();
tf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");

For SchemaFactory and Validator, set ACCESS_EXTERNAL_DTD and ACCESS_EXTERNAL_SCHEMA to empty strings. An empty string means "no protocols allowed," which blocks the parser from reaching out at all.

Which libraries have their own XXE gotchas?

Beyond the JDK, several popular libraries parse XML and carry their own defaults:

  • jackson-dataformat-xml. The XmlMapper uses a WstxInputFactory under the hood. Recent versions default SUPPORT_DTD to safe values, but pin a current release and confirm the setting rather than assuming.
  • Spring OXM and JAXB. Unmarshaller implementations wrap SAX; configure the underlying XMLReader with the SAX flags above before unmarshalling untrusted input.
  • Apache Commons and Xerces. Older xerces2 versions have had DTD-handling CVEs of their own, so version currency matters alongside configuration.

This is where an SCA scan earns its place: it inventories exactly which XML-parsing libraries and versions are in your build, so you are not hardening code while an outdated dependency quietly reintroduces the risk.

How do you keep the fix from regressing?

Parser configuration is fragile because a well-meaning refactor can drop a flag and nothing visibly breaks. Two habits keep the xxe fix in java durable:

  1. Centralize parser creation. Wrap each factory in a single hardened helper (SafeXml.documentBuilder()) and forbid direct newInstance() calls in review. One safe constructor beats twenty scattered ones.
  2. Add a payload regression test. Feed a known XXE document (a file:/// entity and an out-of-band DTD reference) into every parsing path and assert the entity is not resolved. This is the same XXE example in Java that a security reviewer would use to prove the bug exists — running it as a regression test freezes the safe behavior so a future change that reopens the hole fails CI instead of shipping.

Combining SAST and DAST closes the loop: static analysis flags parsers created without the hardening flags, and a dynamic probe confirms a live payload no longer resolves. Safeguard's static engine specifically checks for unsafe XML factory configurations and ties them back to the exact file and line, so remediation is a targeted edit rather than a codebase-wide hunt.

FAQ

What is the single most important XXE fix flag in Java?

disallow-doctype-decl set to true on your factory. It rejects any XML that declares a doctype, which blocks both classic file-read XXE and blind out-of-band XXE in one setting. Use it wherever your application does not genuinely need DTD processing.

Do I still need the external-entity flags if I disallow doctypes?

Setting them is belt and suspenders, and it costs nothing. If disallow-doctype-decl is honored, external entities can never be declared. But not every parser or version honors that feature identically, so keeping external-general-entities and external-parameter-entities false guarantees the fix holds even where the doctype flag is ignored.

Does the JDK now protect against XXE by default?

No, not universally. The JDK added FEATURE_SECURE_PROCESSING and JAXP access-control properties, and some higher-level libraries default to safer settings, but the core DocumentBuilderFactory and SAXParserFactory still resolve external entities unless you configure them. Never assume the default is safe; set the flags explicitly.

How do I test that my XXE fix actually works?

Write a unit test that parses a document containing an external entity pointing at a known local file, then assert the parsed output does not contain that file's contents and that a SAXParseException or similar is thrown. Run it against every parsing path so a regression fails the build.

Never miss an update

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