XML External Entity (XXE) injection via XMLInputFactory external entities

Critical Risk application-security
javaxmlxxexmlinputfactoryexternal-entitiesfile-disclosuressrfinjection

What it is

XML External Entity (XXE) vulnerabilities occur when XML parsers process external entity references without proper security controls. This allows attackers to read local files, perform server-side request forgery (SSRF), cause denial of service, and potentially execute remote code by exploiting the XML parser's ability to resolve external entities.

// VULNERABLE: XMLInputFactory with default settings
import javax.xml.stream.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import java.io.*;
import java.util.*;

// VULNERABLE: XMLInputFactory without security settings
public class VulnerableXMLProcessor {
    
    public void processXMLStream(String xmlContent) throws XMLStreamException {
        // VULNERABLE: Default XMLInputFactory allows external entities
        XMLInputFactory factory = XMLInputFactory.newInstance();
        
        StringReader reader = new StringReader(xmlContent);
        XMLStreamReader xmlReader = factory.createXMLStreamReader(reader);
        
        while (xmlReader.hasNext()) {
            int event = xmlReader.next();
            
            if (event == XMLStreamConstants.START_ELEMENT) {
                String elementName = xmlReader.getLocalName();
                System.out.println("Element: " + elementName);
                
                // VULNERABLE: Processing element content without validation
                if (xmlReader.hasText()) {
                    String text = xmlReader.getElementText();
                    System.out.println("Content: " + text);
                }
            }
        }
        
        xmlReader.close();
    }
    
    // VULNERABLE: DocumentBuilder without security settings
    public void parseXMLDocument(String xmlContent) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        
        // VULNERABLE: Default settings allow external entities
        DocumentBuilder builder = factory.newDocumentBuilder();
        
        StringReader reader = new StringReader(xmlContent);
        InputSource inputSource = new InputSource(reader);
        
        // VULNERABLE: Parse XML with external entity processing enabled
        Document document = builder.parse(inputSource);
        
        // Process document
        processDocument(document);
    }
    
    // VULNERABLE: SAXParser without security settings
    public void parseXMLWithSAX(String xmlContent) throws Exception {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        
        // VULNERABLE: Default SAX parser settings
        SAXParser parser = factory.newSAXParser();
        
        DefaultHandler handler = new DefaultHandler() {
            @Override
            public void startElement(String uri, String localName, 
                                   String qName, Attributes attributes) {
                System.out.println("SAX Element: " + qName);
            }
            
            @Override
            public void characters(char[] ch, int start, int length) {
                String content = new String(ch, start, length).trim();
                if (!content.isEmpty()) {
                    System.out.println("SAX Content: " + content);
                }
            }
        };
        
        StringReader reader = new StringReader(xmlContent);
        InputSource inputSource = new InputSource(reader);
        
        // VULNERABLE: Parse with external entities enabled
        parser.parse(inputSource, handler);
    }
    
    // VULNERABLE: XML processing for configuration files
    public Properties loadConfigFromXML(String xmlConfig) throws Exception {
        Properties config = new Properties();
        
        // VULNERABLE: Process XML configuration without security
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        
        Document doc = builder.parse(new InputSource(new StringReader(xmlConfig)));
        
        NodeList properties = doc.getElementsByTagName("property");
        for (int i = 0; i < properties.getLength(); i++) {
            Element property = (Element) properties.item(i);
            String key = property.getAttribute("name");
            String value = property.getTextContent();
            
            // VULNERABLE: External entity content loaded as config
            config.setProperty(key, value);
        }
        
        return config;
    }
    
    // VULNERABLE: XML-based data import
    public List<User> importUsersFromXML(InputStream xmlStream) throws Exception {
        List<User> users = new ArrayList<>();
        
        // VULNERABLE: XMLInputFactory for user data import
        XMLInputFactory factory = XMLInputFactory.newInstance();
        XMLStreamReader reader = factory.createXMLStreamReader(xmlStream);
        
        String currentElement = "";
        User currentUser = null;
        
        while (reader.hasNext()) {
            int event = reader.next();
            
            switch (event) {
                case XMLStreamConstants.START_ELEMENT:
                    currentElement = reader.getLocalName();
                    if ("user".equals(currentElement)) {
                        currentUser = new User();
                    }
                    break;
                    
                case XMLStreamConstants.CHARACTERS:
                    String text = reader.getText().trim();
                    if (!text.isEmpty() && currentUser != null) {
                        switch (currentElement) {
                            case "username":
                                // VULNERABLE: External entity content as username
                                currentUser.setUsername(text);
                                break;
                            case "email":
                                currentUser.setEmail(text);
                                break;
                            case "role":
                                currentUser.setRole(text);
                                break;
                        }
                    }
                    break;
                    
                case XMLStreamConstants.END_ELEMENT:
                    if ("user".equals(reader.getLocalName()) && currentUser != null) {
                        users.add(currentUser);
                        currentUser = null;
                    }
                    break;
            }
        }
        
        reader.close();
        return users;
    }
    
    private void processDocument(Document document) {
        // Process DOM document
        NodeList elements = document.getElementsByTagName("*");
        for (int i = 0; i < elements.getLength(); i++) {
            Node node = elements.item(i);
            System.out.println("Processing: " + node.getNodeName());
        }
    }
}

// VULNERABLE: User class for data import
class User {
    private String username;
    private String email;
    private String role;
    
    // Getters and setters
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getRole() { return role; }
    public void setRole(String role) { this.role = role; }
}

// VULNERABLE: Example usage that would be exploited
public class VulnerableXMLExample {
    public static void main(String[] args) {
        VulnerableXMLProcessor processor = new VulnerableXMLProcessor();
        
        // VULNERABLE: Malicious XML with external entity
        String maliciousXML = 
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
            "<!DOCTYPE root [" +
            "  <!ENTITY xxe SYSTEM \"file:///etc/passwd\">" +
            "]>" +
            "<root>" +
            "  <data>&xxe;</data>" +  // This would read /etc/passwd
            "</root>";
        
        try {
            // VULNERABLE: This would expose file contents
            processor.processXMLStream(maliciousXML);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// SECURE: XMLInputFactory with proper security configuration
import javax.xml.stream.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import java.io.*;
import java.util.*;
import java.util.regex.Pattern;

// SECURE: XML processor with comprehensive security settings
public class SecureXMLProcessor {
    
    // SECURE: Create secure XMLInputFactory
    private XMLInputFactory createSecureXMLInputFactory() {
        XMLInputFactory factory = XMLInputFactory.newInstance();
        
        // SECURE: Disable external entity processing
        factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
        factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
        
        // SECURE: Additional security properties
        try {
            factory.setProperty("javax.xml.stream.isSupportingExternalEntities", false);
            factory.setProperty("javax.xml.stream.supportDTD", false);
        } catch (IllegalArgumentException e) {
            // Some implementations may not support these properties
        }
        
        return factory;
    }
    
    public void processXMLStream(String xmlContent) throws XMLStreamException {
        // Input validation
        if (!isValidXMLContent(xmlContent)) {
            throw new XMLStreamException("Invalid or potentially malicious XML content");
        }
        
        // SECURE: Use secure XMLInputFactory
        XMLInputFactory factory = createSecureXMLInputFactory();
        
        StringReader reader = new StringReader(xmlContent);
        XMLStreamReader xmlReader = null;
        
        try {
            xmlReader = factory.createXMLStreamReader(reader);
            
            while (xmlReader.hasNext()) {
                int event = xmlReader.next();
                
                if (event == XMLStreamConstants.START_ELEMENT) {
                    String elementName = xmlReader.getLocalName();
                    
                    // SECURE: Validate element names
                    if (isValidElementName(elementName)) {
                        System.out.println("Element: " + elementName);
                        
                        // SECURE: Safe text processing
                        String text = getSecureElementText(xmlReader);
                        if (text != null) {
                            System.out.println("Content: " + sanitizeText(text));
                        }
                    }
                }
            }
        } finally {
            if (xmlReader != null) {
                xmlReader.close();
            }
        }
    }
    
    // SECURE: DocumentBuilder with security settings
    public void parseXMLDocument(String xmlContent) throws Exception {
        // Input validation
        if (!isValidXMLContent(xmlContent)) {
            throw new Exception("Invalid or potentially malicious XML content");
        }
        
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        
        // SECURE: Disable external entity processing
        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);
        
        // SECURE: Additional security features
        factory.setXIncludeAware(false);
        factory.setExpandEntityReferences(false);
        
        DocumentBuilder builder = factory.newDocumentBuilder();
        
        // SECURE: Custom error handler
        builder.setErrorHandler(new SecurityErrorHandler());
        
        StringReader reader = new StringReader(xmlContent);
        InputSource inputSource = new InputSource(reader);
        
        // SECURE: Parse with security restrictions
        Document document = builder.parse(inputSource);
        
        // SECURE: Process document safely
        processDocumentSecurely(document);
    }
    
    // SECURE: SAXParser with security configuration
    public void parseXMLWithSAX(String xmlContent) throws Exception {
        if (!isValidXMLContent(xmlContent)) {
            throw new Exception("Invalid or potentially malicious XML content");
        }
        
        SAXParserFactory factory = SAXParserFactory.newInstance();
        
        // SECURE: Disable external entity processing
        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);
        
        SAXParser parser = factory.newSAXParser();
        
        // SECURE: Custom content handler with validation
        DefaultHandler handler = new SecureContentHandler();
        
        StringReader reader = new StringReader(xmlContent);
        InputSource inputSource = new InputSource(reader);
        
        // SECURE: Parse with security restrictions
        parser.parse(inputSource, handler);
    }
    
    // SECURE: Configuration loading with validation
    public Properties loadConfigFromXML(String xmlConfig) throws Exception {
        // SECURE: Validate configuration content
        if (!isValidConfigXML(xmlConfig)) {
            throw new Exception("Invalid configuration XML");
        }
        
        Properties config = new Properties();
        
        // SECURE: Use secure document builder
        DocumentBuilderFactory factory = createSecureDocumentBuilderFactory();
        DocumentBuilder builder = factory.newDocumentBuilder();
        builder.setErrorHandler(new SecurityErrorHandler());
        
        Document doc = builder.parse(new InputSource(new StringReader(xmlConfig)));
        
        NodeList properties = doc.getElementsByTagName("property");
        for (int i = 0; i < properties.getLength(); i++) {
            Element property = (Element) properties.item(i);
            
            String key = sanitizeConfigKey(property.getAttribute("name"));
            String value = sanitizeConfigValue(property.getTextContent());
            
            // SECURE: Validate configuration values
            if (isValidConfigProperty(key, value)) {
                config.setProperty(key, value);
            }
        }
        
        return config;
    }
    
    // SECURE: Data import with comprehensive validation
    public List<User> importUsersFromXML(InputStream xmlStream) throws Exception {
        List<User> users = new ArrayList<>();
        
        // SECURE: Pre-validate stream content
        String xmlContent = readStreamSafely(xmlStream);
        if (!isValidUserDataXML(xmlContent)) {
            throw new Exception("Invalid user data XML format");
        }
        
        XMLInputFactory factory = createSecureXMLInputFactory();
        XMLStreamReader reader = null;
        
        try {
            reader = factory.createXMLStreamReader(new StringReader(xmlContent));
            
            String currentElement = "";
            User currentUser = null;
            int userCount = 0;
            final int MAX_USERS = 10000; // Prevent DoS
            
            while (reader.hasNext() && userCount < MAX_USERS) {
                int event = reader.next();
                
                switch (event) {
                    case XMLStreamConstants.START_ELEMENT:
                        currentElement = reader.getLocalName();
                        if ("user".equals(currentElement)) {
                            currentUser = new User();
                            userCount++;
                        }
                        break;
                        
                    case XMLStreamConstants.CHARACTERS:
                        String text = reader.getText().trim();
                        if (!text.isEmpty() && currentUser != null) {
                            // SECURE: Validate and sanitize user data
                            text = sanitizeUserData(text);
                            
                            switch (currentElement) {
                                case "username":
                                    if (isValidUsername(text)) {
                                        currentUser.setUsername(text);
                                    }
                                    break;
                                case "email":
                                    if (isValidEmail(text)) {
                                        currentUser.setEmail(text);
                                    }
                                    break;
                                case "role":
                                    if (isValidRole(text)) {
                                        currentUser.setRole(text);
                                    }
                                    break;
                            }
                        }
                        break;
                        
                    case XMLStreamConstants.END_ELEMENT:
                        if ("user".equals(reader.getLocalName()) && currentUser != null) {
                            // SECURE: Validate complete user object
                            if (isValidUser(currentUser)) {
                                users.add(currentUser);
                            }
                            currentUser = null;
                        }
                        break;
                }
            }
        } finally {
            if (reader != null) {
                reader.close();
            }
        }
        
        return users;
    }
    
    // SECURE: Helper methods for security validation
    private boolean isValidXMLContent(String xmlContent) {
        if (xmlContent == null || xmlContent.trim().isEmpty()) {
            return false;
        }
        
        // Check for suspicious patterns
        String upperContent = xmlContent.toUpperCase();
        
        // SECURE: Detect potential XXE patterns
        if (upperContent.contains("<!DOCTYPE") ||
            upperContent.contains("<!ENTITY") ||
            upperContent.contains("SYSTEM") ||
            upperContent.contains("PUBLIC") ||
            upperContent.contains("&") && upperContent.contains(";")) {
            return false;
        }
        
        // Check for excessive size
        if (xmlContent.length() > 1024 * 1024) { // 1MB limit
            return false;
        }
        
        return true;
    }
    
    private DocumentBuilderFactory createSecureDocumentBuilderFactory() throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        
        // SECURE: Comprehensive security configuration
        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);
        
        factory.setXIncludeAware(false);
        factory.setExpandEntityReferences(false);
        
        return factory;
    }
    
    private String readStreamSafely(InputStream stream) throws IOException {
        StringBuilder content = new StringBuilder();
        byte[] buffer = new byte[8192];
        int totalBytes = 0;
        final int MAX_SIZE = 10 * 1024 * 1024; // 10MB limit
        
        int bytesRead;
        while ((bytesRead = stream.read(buffer)) != -1) {
            totalBytes += bytesRead;
            if (totalBytes > MAX_SIZE) {
                throw new IOException("XML content exceeds maximum allowed size");
            }
            content.append(new String(buffer, 0, bytesRead, "UTF-8"));
        }
        
        return content.toString();
    }
    
    private String getSecureElementText(XMLStreamReader reader) throws XMLStreamException {
        if (reader.hasText()) {
            String text = reader.getElementText();
            return (text.length() > 10000) ? text.substring(0, 10000) : text;
        }
        return null;
    }
    
    private String sanitizeText(String text) {
        if (text == null) return "";
        
        // Remove potentially dangerous characters
        return text.replaceAll("[<>&\"']", "")
                  .replaceAll("\\p{Cntrl}", "") // Remove control characters
                  .trim();
    }
    
    private String sanitizeUserData(String data) {
        if (data == null) return "";
        
        return data.replaceAll("[<>&\"'\n\r\t]", "")
                  .replaceAll("\\p{Cntrl}", "")
                  .trim();
    }
    
    // Validation methods
    private boolean isValidElementName(String name) {
        return name != null && 
               name.matches("[a-zA-Z][a-zA-Z0-9_-]*") && 
               name.length() <= 50;
    }
    
    private boolean isValidUsername(String username) {
        return username != null && 
               username.matches("[a-zA-Z0-9_-]+") && 
               username.length() >= 3 && 
               username.length() <= 50;
    }
    
    private boolean isValidEmail(String email) {
        return email != null && 
               email.matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}") &&
               email.length() <= 254;
    }
    
    private boolean isValidRole(String role) {
        Set<String> validRoles = Set.of("user", "admin", "moderator", "viewer");
        return role != null && validRoles.contains(role.toLowerCase());
    }
    
    private boolean isValidUser(User user) {
        return user != null && 
               user.getUsername() != null && 
               user.getEmail() != null && 
               user.getRole() != null;
    }
    
    private boolean isValidConfigXML(String xml) {
        return isValidXMLContent(xml) && 
               xml.contains("<property") && 
               !xml.contains("<!ENTITY");
    }
    
    private boolean isValidUserDataXML(String xml) {
        return isValidXMLContent(xml) && 
               xml.contains("<user") && 
               !xml.contains("<!ENTITY");
    }
    
    private String sanitizeConfigKey(String key) {
        if (key == null) return "";
        return key.replaceAll("[^a-zA-Z0-9._-]", "").toLowerCase();
    }
    
    private String sanitizeConfigValue(String value) {
        if (value == null) return "";
        return value.replaceAll("[<>&\"']", "").trim();
    }
    
    private boolean isValidConfigProperty(String key, String value) {
        return key != null && !key.isEmpty() && 
               value != null && 
               key.length() <= 100 && 
               value.length() <= 1000;
    }
    
    private void processDocumentSecurely(Document document) {
        NodeList elements = document.getElementsByTagName("*");
        int nodeCount = 0;
        final int MAX_NODES = 10000;
        
        for (int i = 0; i < elements.getLength() && nodeCount < MAX_NODES; i++) {
            Node node = elements.item(i);
            String nodeName = sanitizeText(node.getNodeName());
            
            if (isValidElementName(nodeName)) {
                System.out.println("Processing: " + nodeName);
                nodeCount++;
            }
        }
    }
}

// SECURE: Custom error handler for XML parsing
class SecurityErrorHandler implements ErrorHandler {
    @Override
    public void warning(SAXParseException exception) throws SAXException {
        throw new SAXException("XML parsing warning: " + exception.getMessage());
    }
    
    @Override
    public void error(SAXParseException exception) throws SAXException {
        throw new SAXException("XML parsing error: " + exception.getMessage());
    }
    
    @Override
    public void fatalError(SAXParseException exception) throws SAXException {
        throw new SAXException("XML parsing fatal error: " + exception.getMessage());
    }
}

// SECURE: Custom content handler with validation
class SecureContentHandler extends DefaultHandler {
    private int elementCount = 0;
    private final int MAX_ELEMENTS = 10000;
    
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) 
            throws SAXException {
        if (++elementCount > MAX_ELEMENTS) {
            throw new SAXException("Too many XML elements - potential DoS attack");
        }
        
        // Validate element name
        if (!isValidElementName(qName)) {
            throw new SAXException("Invalid element name: " + qName);
        }
        
        System.out.println("SAX Element: " + qName);
    }
    
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        String content = new String(ch, start, length).trim();
        if (!content.isEmpty()) {
            // Validate and sanitize content
            if (content.length() > 10000) {
                throw new SAXException("Element content too large");
            }
            
            String sanitized = content.replaceAll("[<>&\"']", "");
            System.out.println("SAX Content: " + sanitized);
        }
    }
    
    private boolean isValidElementName(String name) {
        return name != null && 
               name.matches("[a-zA-Z][a-zA-Z0-9_-]*") && 
               name.length() <= 50;
    }
}

// SECURE: User class with validation
class User {
    private String username;
    private String email;
    private String role;
    
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getRole() { return role; }
    public void setRole(String role) { this.role = role; }
    
    @Override
    public String toString() {
        return String.format("User{username='%s', email='%s', role='%s'}", 
                           username, email, role);
    }
}

// SECURE: Example usage demonstrating security
public class SecureXMLExample {
    public static void main(String[] args) {
        SecureXMLProcessor processor = new SecureXMLProcessor();
        
        // SECURE: Clean XML without external entities
        String safeXML = 
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
            "<root>" +
            "  <data>Safe content here</data>" +
            "  <users>" +
            "    <user>" +
            "      <username>john_doe</username>" +
            "      <email>john@example.com</email>" +
            "      <role>user</role>" +
            "    </user>" +
            "  </users>" +
            "</root>";
        
        try {
            // SECURE: This will process safely
            System.out.println("Processing safe XML:");
            processor.processXMLStream(safeXML);
            
            // SECURE: Malicious XML will be rejected
            String maliciousXML = 
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                "<!DOCTYPE root [" +
                "  <!ENTITY xxe SYSTEM \"file:///etc/passwd\">" +
                "]>" +
                "<root>" +
                "  <data>&xxe;</data>" +
                "</root>";
            
            System.out.println("\nAttempting to process malicious XML:");
            processor.processXMLStream(maliciousXML);
            
        } catch (Exception e) {
            System.out.println("SECURE: Malicious XML was blocked: " + e.getMessage());
        }
    }
}

💡 Why This Fix Works

The vulnerable examples show XML parsers using default configurations that allow external entity processing, making them susceptible to XXE attacks for file disclosure and SSRF. The secure alternatives implement comprehensive security measures including disabling external entities, DTD processing, validating input content, sanitizing data, implementing size limits, and using custom error handlers to prevent XXE vulnerabilities.

Why it happens

XMLInputFactory and other XML parsers are configured with external entity processing enabled by default, making applications vulnerable unless developers explicitly disable these features.

Root causes

Default Parser Configuration

XMLInputFactory and other XML parsers are configured with external entity processing enabled by default, making applications vulnerable unless developers explicitly disable these features.

Untrusted XML Processing

Applications process XML data from untrusted sources (user uploads, API requests, external feeds) without validating or sanitizing the content for malicious entity definitions.

Legacy XML Processing Code

Older codebases use XML parsing libraries without modern security configurations, often copying examples that don't include necessary security hardening.

Fixes

1

Disable External Entity Processing

Configure XMLInputFactory and other XML parsers to disable external entity processing, DTD processing, and external general entities to prevent XXE attacks.

2

Use Secure XML Libraries

Implement XML parsing using libraries that are secure by default, or use configuration builders that automatically apply security settings.

3

Input Validation and Sanitization

Validate and sanitize XML input by removing or escaping potentially dangerous constructs like DOCTYPE declarations and entity references before parsing.

Detect This Vulnerability in Your Code

Sourcery automatically identifies xml external entity (xxe) injection via xmlinputfactory external entities and many other security issues in your codebase.