CWE-611: Improper Restriction of XML External Entity Reference - Java
Overview
XXE vulnerabilities occur when XML parsers process external entity references in untrusted XML, allowing attackers to read files, perform SSRF attacks, or cause denial of service.
Java is the ecosystem where this is still live by default. On JDK 26, an unconfigured DocumentBuilderFactory or SAXParserFactory resolves a file entity and hands back its contents:
bare default DocumentBuilderFactory -> ACCEPTED, text = "TOP-SECRET-CONTENT"
bare default SAXParserFactory -> ACCEPTED, text = "TOP-SECRET-CONTENT"
Other platforms have moved: .NET's XmlDictionaryReader refuses a DOCTYPE outright and Go's encoding/xml treats an unknown entity as a parse error, so their equivalent findings are often already closed. In Java the defaults are the vulnerability, every factory is configured independently, and nothing warns you when one is missed. Treat an unconfigured factory as a finding rather than checking whether the runtime happens to be safe.
Primary Defence: Disable DTDs entirely where possible. Also enable FEATURE_SECURE_PROCESSING, set ACCESS_EXTERNAL_DTD and ACCESS_EXTERNAL_SCHEMA to the empty string where supported, disable external general and parameter entities, and install a fail-closed EntityResolver.
Common Vulnerable Patterns
DocumentBuilderFactory (Default Configuration)
// VULNERABLE - Default settings allow XXE
import javax.xml.parsers.*;
import org.w3c.dom.*;
public class XmlParser {
public Document parse(String xml) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
// DANGEROUS: No explicit DTD/entity restrictions
return builder.parse(new InputSource(new StringReader(xml)));
}
}
// Attacker sends:
// <?xml version="1.0"?>
// <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
// <root>&xxe;</root>
Why this is vulnerable:
- External access and entity handling are not explicitly restricted.
- The JDK default resolves the entity, as the Overview shows, so
&xxe;comes back as the contents of/etc/passwd. The same route reaches internal URLs (SSRF) and entity expansion DoS.
SAXParserFactory (Default Configuration)
// VULNERABLE - SAX parser without security
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
public void parseSax(String xml) throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
// DANGEROUS: External entities enabled
parser.parse(new InputSource(new StringReader(xml)), new DefaultHandler());
}
Why this is vulnerable:
- DTDs/external entities are not explicitly disabled.
- The default
SAXParserFactoryresolves them too, so the same file disclosure, SSRF, and DoS apply.
XMLReader (Default Configuration)
// VULNERABLE - XMLReader without features
import org.xml.sax.XMLReader;
import javax.xml.parsers.SAXParserFactory;
public void parseXml(String xml) throws Exception {
XMLReader reader = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
// DANGEROUS: No security features set
reader.parse(new InputSource(new StringReader(xml)));
}
Why this is vulnerable:
- DTDs and entities are processed without feature flags.
- Enables file:// disclosure, SSRF, and resource exhaustion.
Unmarshaller (JAXB)
// VULNERABLE - JAXB without secure XML input factory
import jakarta.xml.bind.*;
public User unmarshal(String xml) throws Exception {
JAXBContext context = JAXBContext.newInstance(User.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
// DANGEROUS: Uses an unconfigured XML parser
return (User) unmarshaller.unmarshal(new StringReader(xml));
}
Why this is vulnerable:
- Parser behavior is implementation- and version-dependent when not explicitly configured.
- Enables file disclosure and SSRF via crafted XML.
Secure Patterns
DocumentBuilderFactory (Secure Configuration)
// SECURE - Disable all dangerous features
import javax.xml.parsers.*;
import javax.xml.XMLConstants;
import org.w3c.dom.*;
public class SecureXmlParser {
public Document parse(String xml) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
// Disable DTDs entirely
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// If DTDs must be allowed, disable external entities
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Disable external DTDs
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
// Block external access through JAXP properties
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
// Disable XInclude
factory.setXIncludeAware(false);
// Disable entity expansion
factory.setExpandEntityReferences(false);
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(new InputSource(new StringReader(xml)));
}
}
Why this works:
- Rejects DOCTYPE, disables external entities/DTDs, and blocks external JAXP access.
- Disables XInclude, a separate feature that reaches the same file read or SSRF, and disables entity expansion to prevent DoS.
SAXParserFactory (Secure Configuration)
// SECURE - SAX parser with security features
import javax.xml.parsers.*;
import javax.xml.XMLConstants;
import org.xml.sax.*;
public class SecureSaxParser {
public void parse(String xml, DefaultHandler handler) throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
// Disable DTDs
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// Disable external entities
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Disable external DTD loading
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
SAXParser parser = factory.newSAXParser();
XMLReader reader = parser.getXMLReader();
reader.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
reader.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
// Set secure entity resolver
reader.setEntityResolver((publicId, systemId) -> {
throw new SAXException("External entities are not allowed");
});
reader.setContentHandler(handler);
reader.parse(new InputSource(new StringReader(xml)));
}
}
Why this works:
- Feature flags disable DOCTYPE and external entity processing, while JAXP properties block external access where supported.
- EntityResolver fail-safe blocks any resolution attempts.
XMLReader (Secure Configuration)
// SECURE - XMLReader with all protections
import org.xml.sax.*;
import javax.xml.XMLConstants;
import javax.xml.parsers.SAXParserFactory;
public void parseSecure(String xml) throws Exception {
// XMLReaderFactory.createXMLReader() has been deprecated since Java 9.
// SAXParserFactory is the supported route, and it is where the features
// belong anyway - they are set before the reader exists.
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
// Disable DTDs. This alone stops XXE: any DOCTYPE is now a fatal error,
// so there is no declaration for an entity to be defined in.
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// Belt and braces for parsers where the above is unavailable
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
XMLReader reader = factory.newSAXParser().getXMLReader();
reader.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
reader.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
// Secure entity resolver
reader.setEntityResolver((publicId, systemId) -> {
throw new SAXException("External entities are blocked");
});
reader.parse(new InputSource(new StringReader(xml)));
}
Why this works:
disallow-doctype-declis the load-bearing setting: with no DOCTYPE permitted, an attacker has nowhere to declare an entity. Verified against a file-disclosure payload, which fails with "DOCTYPE is disallowed" before any entity is resolved, while a document without a DOCTYPE still parses.- The remaining features and properties matter only where that one cannot be set, which is why they are worth keeping but not worth relying on alone.
- EntityResolver is the last line: it turns any resolution that does get attempted into an exception rather than a fetch.
XMLInputFactory (StAX Parser - Secure Configuration)
// SECURE - StAX parser with external entity restrictions
import javax.xml.stream.*;
import javax.xml.XMLConstants;
public void parseWithStax(String xml) throws Exception {
XMLInputFactory factory = XMLInputFactory.newFactory();
// Disable DTD processing entirely
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
// Disable external entity processing
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
try (StringReader reader = new StringReader(xml)) {
XMLStreamReader streamReader = factory.createXMLStreamReader(reader);
while (streamReader.hasNext()) {
int event = streamReader.next();
if (event == XMLStreamConstants.START_ELEMENT) {
String elementName = streamReader.getLocalName();
// Process elements securely
}
}
streamReader.close();
}
}
Why this works:
- DTD support, external entities, and external access are disabled.
- The parser still streams, so it stays usable for large inputs.
XPath with Secure Parsing
// SECURE - XPath expression with secure document parsing
import javax.xml.xpath.*;
import javax.xml.XMLConstants;
public String evaluateXPath(String xml, String xpathExpression) throws Exception {
// First parse XML securely
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// Restrict external access
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
DocumentBuilder builder = dbf.newDocumentBuilder();
Document doc = builder.parse(new ByteArrayInputStream(xml.getBytes("UTF-8")));
// Now XPath evaluation is safe
XPathFactory xpathFactory = XPathFactory.newInstance();
XPath xpath = xpathFactory.newXPath();
XPathExpression expr = xpath.compile(xpathExpression);
return expr.evaluate(doc);
}
Why this works:
- Secure parsing blocks DOCTYPE and external access first.
- XPath runs on a pre-parsed safe DOM.
JAXB (Secure Configuration)
// SECURE - JAXB with secure XML input factory
// JAXB left the JDK in Java 11 and moved namespace in Jakarta EE 9, so on
// Java 17+ it is a dependency:
// jakarta.xml.bind:jakarta.xml.bind-api (the jakarta.xml.bind API)
// org.glassfish.jaxb:jaxb-runtime (the implementation, runtime scope)
import jakarta.xml.bind.*;
import javax.xml.parsers.*;
import javax.xml.transform.sax.SAXSource;
import javax.xml.XMLConstants;
public User unmarshalSecure(String xml) throws Exception {
// Create secure SAX parser
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
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 saxParser = spf.newSAXParser();
XMLReader reader = saxParser.getXMLReader();
reader.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
reader.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
// Use secure reader with JAXB
JAXBContext context = JAXBContext.newInstance(User.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
SAXSource source = new SAXSource(reader, new InputSource(new StringReader(xml)));
return (User) unmarshaller.unmarshal(source);
}
Why this works:
- Uses a secure SAX parser instead of JAXB defaults.
- Blocks DOCTYPE and external entities before unmarshalling.
Framework-Specific Guidance
Spring Framework
// SECURE - Spring XML configuration
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class XmlConfig {
@Bean
public Jaxb2Marshaller marshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(User.class);
// Configure secure properties
Map<String, Object> properties = new HashMap<>();
properties.put("jaxb.encoding", "UTF-8");
marshaller.setMarshallerProperties(properties);
// Set secure XML input factory
marshaller.setProcessExternalEntities(false);
return marshaller;
}
}
// Controller usage:
@RestController
public class UserController {
@Autowired
private Jaxb2Marshaller marshaller;
@PostMapping(value = "/users", consumes = "application/xml")
public ResponseEntity<User> createUser(@RequestBody String xml) throws Exception {
// Create secure SAX source
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
SAXParser parser = spf.newSAXParser();
XMLReader reader = parser.getXMLReader();
SAXSource source = new SAXSource(reader, new InputSource(new StringReader(xml)));
User user = (User) marshaller.unmarshal(source);
return ResponseEntity.ok(user);
}
}
JAX-RS (RESTful Web Services)
// SECURE - JAX-RS with secure XML
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;
import javax.xml.parsers.*;
@Path("/users")
@Produces(MediaType.APPLICATION_XML)
@Consumes(MediaType.APPLICATION_XML)
public class UserResource {
@POST
public Response createUser(String xml) throws Exception {
// Parse with secure DocumentBuilder
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader(xml)));
// Extract data from DOM
String name = doc.getElementsByTagName("name").item(0).getTextContent();
String email = doc.getElementsByTagName("email").item(0).getTextContent();
User user = new User(name, email);
userService.save(user);
return Response.ok(user).build();
}
}
Reusable Secure Parser Utility
// Utility class for secure XML parsing
public class SecureXmlUtil {
/**
* Creates a secure DocumentBuilderFactory
*/
public static DocumentBuilderFactory createSecureDocumentBuilderFactory() throws ParserConfigurationException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
// Completely disable DTDs
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// If you can't disable DTDs, at least disable external entities
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
return factory;
}
/**
* Creates a secure SAXParserFactory
*/
public static SAXParserFactory createSecureSAXParserFactory() throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
return factory;
}
/**
* Parse XML string securely to Document
*/
public static Document parseXml(String xml) throws Exception {
DocumentBuilderFactory factory = createSecureDocumentBuilderFactory();
DocumentBuilder builder = factory.newDocumentBuilder();
// Block all entity resolution
builder.setEntityResolver((publicId, systemId) -> {
throw new SAXException("External entities are not allowed");
});
return builder.parse(new InputSource(new StringReader(xml)));
}
}
// Usage:
Document doc = SecureXmlUtil.parseXml(untrustedXml);
Why this works:
- One hardened factory per parser type gives every call site the same settings, instead of each one being configured on its own.
- The EntityResolver fail-safe blocks any resolution attempt, and leaves a reviewer one place to check.
Input Validation
// Validate XML structure after parsing
import org.w3c.dom.*;
public User parseAndValidate(String xml) throws Exception {
Document doc = SecureXmlUtil.parseXml(xml);
// Validate structure
NodeList users = doc.getElementsByTagName("user");
if (users.getLength() != 1) {
throw new IllegalArgumentException("Expected exactly one user element");
}
Element userEl = (Element) users.item(0);
String name = getElementText(userEl, "name");
String email = getElementText(userEl, "email");
// Validate content
if (name == null || name.isEmpty() || name.length() > 100) {
throw new IllegalArgumentException("Invalid name");
}
if (email == null || !email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$")) {
throw new IllegalArgumentException("Invalid email");
}
return new User(name, email);
}
private String getElementText(Element parent, String tagName) {
NodeList nodes = parent.getElementsByTagName(tagName);
if (nodes.getLength() == 0) return null;
return nodes.item(0).getTextContent();
}
Common Pitfalls
- Setting
FEATURE_SECURE_PROCESSINGalone and assuming it disables DTDs - it primarily enables resource-limit heuristics (entity expansion limits) but does not by itself disable DOCTYPE or external entity resolution;disallow-doctype-declor the explicit external-entity features still need to be set. - Hardening
DocumentBuilderFactoryfor the main parsing path while aSAXParserFactory, anXMLInputFactory(StAX), or a JAXBUnmarshallerused elsewhere in the same application is configured independently, or left at defaults - JAXP factories don't share configuration, so each instance needs the same feature flags applied on its own. - Assuming the two families of control fail the same way.
ACCESS_EXTERNAL_DTDand theexternal-general-entitiesfeature both stop a<!ENTITY xxe SYSTEM "file:///etc/passwd">in the document's own internal subset, but they do it differently, and the difference decides whether you find out.accessExternalDTD=""raises a fatalSAXParseExceptionnaming the property, so the request fails loudly. Clearingexternal-general-entitiesexpands the reference to nothing instead: the parse succeeds, the element is empty, and a handler that treats a missing value as optional carries on with silently wrong data. Preferdisallow-doctype-decl, which rejects the document at the DOCTYPE and leaves no ambiguity about which of the two happened.