1. xml
  2. /best practices
  3. /security

XML Security

XML security is crucial for protecting applications from various attacks and vulnerabilities. XML documents can be vectors for security breaches if not handled properly, including XML External Entity (XXE) attacks, XML injection, denial of service attacks, and data exposure.

This guide provides essential security practices for processing, validating, and transmitting XML data safely in production environments.

XML External Entity (XXE) Attack Prevention

Understanding XXE Vulnerabilities

XXE attacks exploit XML parsers that process external entity references, potentially allowing attackers to:

  • Read local files
  • Access internal network resources
  • Cause denial of service
  • Execute remote code in some cases

Secure Parser Configuration

public class SecureXMLParserFactory {
    
    public static DocumentBuilder createSecureDocumentBuilder() throws ParserConfigurationException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        
        // Disable DTDs entirely
        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 entity expansion
        factory.setExpandEntityReferences(false);
        
        // Additional security features
        factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        factory.setXIncludeAware(false);
        
        DocumentBuilder builder = factory.newDocumentBuilder();
        
        // Set secure entity resolver
        builder.setEntityResolver(new SecureEntityResolver());
        
        return builder;
    }
    
    public static SAXParser createSecureSAXParser() throws ParserConfigurationException, SAXException {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        
        // 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);
        
        // Additional security
        factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        
        return factory.newSAXParser();
    }
    
    public static XMLStreamReader createSecureStAXReader(InputStream input) throws XMLStreamException {
        XMLInputFactory factory = XMLInputFactory.newInstance();
        
        // Disable DTD processing
        factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
        
        // Disable external entities
        factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
        
        // Disable entity replacement
        factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
        
        return factory.createXMLStreamReader(input);
    }
}

// Secure entity resolver that blocks all external entities
public class SecureEntityResolver implements EntityResolver {
    @Override
    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
        // Block all external entity resolution
        throw new SAXException("External entity resolution is disabled for security: " + systemId);
    }
}

XXE Attack Examples and Prevention

public class XXEAttackPrevention {
    
    // Vulnerable code - DO NOT USE
    public void vulnerableParsingExample(String xmlContent) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            
            // This is vulnerable to XXE attacks
            Document doc = builder.parse(new ByteArrayInputStream(xmlContent.getBytes()));
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    // Secure parsing implementation
    public void secureParsingExample(String xmlContent) {
        try {
            DocumentBuilder secureBuilder = SecureXMLParserFactory.createSecureDocumentBuilder();
            
            // This is protected against XXE attacks
            Document doc = secureBuilder.parse(new ByteArrayInputStream(xmlContent.getBytes()));
            
            // Process document safely
            processDocumentSecurely(doc);
            
        } catch (Exception e) {
            // Log security-related parsing errors
            System.err.println("Secure parsing failed: " + e.getMessage());
        }
    }
    
    private void processDocumentSecurely(Document doc) {
        // Safe document processing
        Element root = doc.getDocumentElement();
        
        // Validate document structure before processing
        if (!isValidDocumentStructure(root)) {
            throw new SecurityException("Invalid document structure detected");
        }
        
        // Process with security checks
        processElementsWithValidation(root);
    }
    
    private boolean isValidDocumentStructure(Element root) {
        // Implement structure validation
        String rootName = root.getTagName();
        
        // Check against whitelist of allowed root elements
        List<String> allowedRoots = Arrays.asList("library", "catalog", "configuration");
        return allowedRoots.contains(rootName);
    }
}

Input Validation and Sanitization

XML Input Validation

public class XMLInputValidator {
    private static final int MAX_DOCUMENT_SIZE = 10 * 1024 * 1024; // 10MB
    private static final int MAX_ELEMENT_DEPTH = 100;
    private static final Pattern SAFE_ELEMENT_NAME = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_-]*$");
    
    public ValidationResult validateXMLInput(String xmlContent) {
        // Size validation
        if (xmlContent.length() > MAX_DOCUMENT_SIZE) {
            return new ValidationResult(false, "Document exceeds maximum size limit");
        }
        
        // Basic structure validation
        if (!isWellFormedXML(xmlContent)) {
            return new ValidationResult(false, "Document is not well-formed XML");
        }
        
        // Content validation
        if (!isSecureContent(xmlContent)) {
            return new ValidationResult(false, "Document contains potentially dangerous content");
        }
        
        return new ValidationResult(true, "Document is valid");
    }
    
    private boolean isWellFormedXML(String xmlContent) {
        try {
            DocumentBuilder builder = SecureXMLParserFactory.createSecureDocumentBuilder();
            builder.parse(new ByteArrayInputStream(xmlContent.getBytes()));
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    private boolean isSecureContent(String xmlContent) {
        // Check for suspicious patterns
        String lowerContent = xmlContent.toLowerCase();
        
        // Block common attack patterns
        if (lowerContent.contains("<!entity") || 
            lowerContent.contains("<!doctype") ||
            lowerContent.contains("&xxe;") ||
            lowerContent.contains("file://") ||
            lowerContent.contains("http://") && !isAllowedURL(xmlContent)) {
            return false;
        }
        
        return true;
    }
    
    private boolean isAllowedURL(String xmlContent) {
        // Implement URL whitelist validation
        // Only allow specific trusted domains
        List<String> allowedDomains = Arrays.asList("api.example.com", "secure.example.com");
        
        for (String domain : allowedDomains) {
            if (xmlContent.contains(domain)) {
                return true;
            }
        }
        
        return false;
    }
    
    public void validateElementNames(Element element) throws SecurityException {
        validateElementName(element.getTagName());
        
        // Validate child elements recursively
        NodeList children = element.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                validateElementNames((Element) child);
            }
        }
    }
    
    private void validateElementName(String elementName) throws SecurityException {
        if (!SAFE_ELEMENT_NAME.matcher(elementName).matches()) {
            throw new SecurityException("Invalid element name: " + elementName);
        }
    }
}

Data Sanitization

public class XMLDataSanitizer {
    
    private static final Map<String, String> XML_ENTITIES = Map.of(
        "<", "&lt;",
        ">", "&gt;",
        "&", "&amp;",
        "\"", "&quot;",
        "'", "&apos;"
    );
    
    public String sanitizeXMLContent(String content) {
        if (content == null) {
            return null;
        }
        
        String sanitized = content;
        
        // Escape XML special characters
        for (Map.Entry<String, String> entity : XML_ENTITIES.entrySet()) {
            sanitized = sanitized.replace(entity.getKey(), entity.getValue());
        }
        
        // Remove potential script content
        sanitized = removeScriptContent(sanitized);
        
        // Validate length
        if (sanitized.length() > 10000) { // 10KB limit for text content
            throw new SecurityException("Text content exceeds maximum length");
        }
        
        return sanitized;
    }
    
    private String removeScriptContent(String content) {
        // Remove potential script-like content
        String cleaned = content.replaceAll("(?i)<script[^>]*>.*?</script>", "");
        cleaned = cleaned.replaceAll("(?i)javascript:", "");
        cleaned = cleaned.replaceAll("(?i)vbscript:", "");
        cleaned = cleaned.replaceAll("(?i)onload=", "");
        cleaned = cleaned.replaceAll("(?i)onerror=", "");
        
        return cleaned;
    }
    
    public String sanitizeAttributeValue(String value) {
        if (value == null) {
            return null;
        }
        
        // Escape quotes and special characters
        String sanitized = value.replace("\"", "&quot;")
                               .replace("'", "&apos;")
                               .replace("<", "&lt;")
                               .replace(">", "&gt;")
                               .replace("&", "&amp;");
        
        // Validate length
        if (sanitized.length() > 1000) { // 1KB limit for attributes
            throw new SecurityException("Attribute value exceeds maximum length");
        }
        
        return sanitized;
    }
}

Schema Validation Security

Secure Schema Validation

public class SecureSchemaValidator {
    private final Map<String, Schema> schemaCache = new ConcurrentHashMap<>();
    
    public ValidationResult validateWithSchema(Document document, String schemaPath) {
        try {
            Schema schema = getSecureSchema(schemaPath);
            Validator validator = schema.newValidator();
            
            // Set secure error handler
            validator.setErrorHandler(new SecurityAwareErrorHandler());
            
            // Validate document
            validator.validate(new DOMSource(document));
            
            return new ValidationResult(true, "Document is valid against schema");
            
        } catch (SAXException e) {
            return new ValidationResult(false, "Schema validation failed: " + e.getMessage());
        } catch (IOException e) {
            return new ValidationResult(false, "IO error during validation: " + e.getMessage());
        }
    }
    
    private Schema getSecureSchema(String schemaPath) throws SAXException {
        return schemaCache.computeIfAbsent(schemaPath, path -> {
            try {
                SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
                
                // Configure factory securely
                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);
                
                // Load schema from trusted location only
                if (!isTrustedSchemaPath(path)) {
                    throw new SecurityException("Schema path not in trusted location: " + path);
                }
                
                return factory.newSchema(new File(path));
                
            } catch (SAXException e) {
                throw new RuntimeException("Failed to load schema: " + path, e);
            }
        });
    }
    
    private boolean isTrustedSchemaPath(String path) {
        // Validate schema comes from trusted location
        Path schemaPath = Paths.get(path).normalize();
        Path trustedDir = Paths.get("/app/schemas").normalize();
        
        return schemaPath.startsWith(trustedDir);
    }
}

public class SecurityAwareErrorHandler implements ErrorHandler {
    @Override
    public void warning(SAXParseException exception) throws SAXException {
        // Log warning but continue
        System.err.println("Schema validation warning: " + exception.getMessage());
    }
    
    @Override
    public void error(SAXParseException exception) throws SAXException {
        // Log error and fail validation
        System.err.println("Schema validation error: " + exception.getMessage());
        throw exception;
    }
    
    @Override
    public void fatalError(SAXParseException exception) throws SAXException {
        // Log fatal error and fail validation
        System.err.println("Schema validation fatal error: " + exception.getMessage());
        throw exception;
    }
}

Secure XML Transmission

XML Encryption

public class XMLEncryptionHandler {
    private static final String ENCRYPTION_ALGORITHM = "AES/CBC/PKCS5Padding";
    private static final String KEY_ALGORITHM = "AES";
    
    public Document encryptSensitiveElements(Document document, SecretKey key, List<String> sensitiveElements) {
        try {
            // Initialize XML encryption
            XMLCipher xmlCipher = XMLCipher.getInstance(XMLCipher.AES_128);
            xmlCipher.init(XMLCipher.ENCRYPT_MODE, key);
            
            // Encrypt each sensitive element
            for (String elementName : sensitiveElements) {
                NodeList elements = document.getElementsByTagName(elementName);
                
                for (int i = 0; i < elements.getLength(); i++) {
                    Element element = (Element) elements.item(i);
                    
                    // Encrypt the element
                    Document encryptedDoc = xmlCipher.doFinal(document, element);
                    
                    // Replace original element with encrypted version
                    Node encryptedElement = encryptedDoc.getDocumentElement();
                    document.adoptNode(encryptedElement);
                    element.getParentNode().replaceChild(encryptedElement, element);
                }
            }
            
            return document;
            
        } catch (Exception e) {
            throw new RuntimeException("Failed to encrypt XML elements", e);
        }
    }
    
    public Document decryptDocument(Document encryptedDocument, SecretKey key) {
        try {
            // Initialize XML decryption
            XMLCipher xmlCipher = XMLCipher.getInstance();
            xmlCipher.init(XMLCipher.DECRYPT_MODE, key);
            
            // Find encrypted elements
            NodeList encryptedElements = encryptedDocument.getElementsByTagNameNS(
                EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_ENCRYPTEDDATA);
            
            // Decrypt each encrypted element
            for (int i = 0; i < encryptedElements.getLength(); i++) {
                Element encryptedElement = (Element) encryptedElements.item(i);
                
                // Decrypt the element
                Document decryptedDoc = xmlCipher.doFinal(encryptedDocument, encryptedElement);
                
                // Replace encrypted element with decrypted version
                Node decryptedElement = decryptedDoc.getDocumentElement();
                encryptedDocument.adoptNode(decryptedElement);
                encryptedElement.getParentNode().replaceChild(decryptedElement, encryptedElement);
            }
            
            return encryptedDocument;
            
        } catch (Exception e) {
            throw new RuntimeException("Failed to decrypt XML document", e);
        }
    }
}

Digital Signatures

public class XMLDigitalSignature {
    
    public Document signDocument(Document document, PrivateKey privateKey) {
        try {
            // Initialize XML signature
            XMLSignatureFactory factory = XMLSignatureFactory.getInstance("DOM");
            
            // Create reference to document
            Reference ref = factory.newReference("", 
                factory.newDigestMethod(DigestMethod.SHA256, null),
                Arrays.asList(factory.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null)),
                null, null);
            
            // Create signed info
            SignedInfo signedInfo = factory.newSignedInfo(
                factory.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null),
                factory.newSignatureMethod(SignatureMethod.RSA_SHA256, null),
                Arrays.asList(ref));
            
            // Create key info
            KeyInfoFactory keyInfoFactory = factory.getKeyInfoFactory();
            KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Arrays.asList(
                keyInfoFactory.newKeyValue(getPublicKey(privateKey))));
            
            // Create signature
            XMLSignature signature = factory.newXMLSignature(signedInfo, keyInfo);
            
            // Sign the document
            DOMSignContext signContext = new DOMSignContext(privateKey, document.getDocumentElement());
            signature.sign(signContext);
            
            return document;
            
        } catch (Exception e) {
            throw new RuntimeException("Failed to sign XML document", e);
        }
    }
    
    public boolean verifySignature(Document signedDocument) {
        try {
            // Find signature element
            NodeList signatureNodes = signedDocument.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
            
            if (signatureNodes.getLength() == 0) {
                return false;
            }
            
            // Create validation context
            DOMValidateContext validateContext = new DOMValidateContext(
                new KeySelector() {
                    public KeySelectorResult select(KeyInfo keyInfo, 
                                                  KeySelector.Purpose purpose,
                                                  AlgorithmMethod method, 
                                                  XMLCryptoContext context) {
                        // Implement key selection logic
                        return new SimpleKeySelectorResult(getValidationKey());
                    }
                }, signatureNodes.item(0));
            
            // Validate signature
            XMLSignatureFactory factory = XMLSignatureFactory.getInstance("DOM");
            XMLSignature signature = factory.unmarshalXMLSignature(validateContext);
            
            return signature.validate(validateContext);
            
        } catch (Exception e) {
            System.err.println("Signature verification failed: " + e.getMessage());
            return false;
        }
    }
    
    private PublicKey getPublicKey(PrivateKey privateKey) {
        // Implementation to get public key from private key
        return null; // Placeholder
    }
    
    private PublicKey getValidationKey() {
        // Implementation to get validation key
        return null; // Placeholder
    }
}

Access Control and Authentication

Role-Based XML Access

public class XMLAccessController {
    private final Map<String, Set<String>> rolePermissions = new HashMap<>();
    
    public XMLAccessController() {
        initializePermissions();
    }
    
    private void initializePermissions() {
        // Admin can access all elements
        rolePermissions.put("admin", Set.of("*"));
        
        // User can access public elements only
        rolePermissions.put("user", Set.of("title", "author", "description"));
        
        // Guest has read-only access to basic info
        rolePermissions.put("guest", Set.of("title", "author"));
    }
    
    public Document filterDocumentByRole(Document document, String userRole) {
        Set<String> allowedElements = rolePermissions.getOrDefault(userRole, Set.of());
        
        if (allowedElements.contains("*")) {
            return document; // Admin has full access
        }
        
        try {
            Document filteredDoc = createFilteredDocument(document, allowedElements);
            return filteredDoc;
        } catch (Exception e) {
            throw new SecurityException("Failed to filter document for role: " + userRole, e);
        }
    }
    
    private Document filteredDocument(Document original, Set<String> allowedElements) 
            throws ParserConfigurationException {
        
        DocumentBuilder builder = SecureXMLParserFactory.createSecureDocumentBuilder();
        Document filtered = builder.newDocument();
        
        // Copy root element
        Element originalRoot = original.getDocumentElement();
        Element filteredRoot = filtered.createElement(originalRoot.getTagName());
        filtered.appendChild(filteredRoot);
        
        // Copy allowed child elements
        copyAllowedElements(originalRoot, filteredRoot, filtered, allowedElements);
        
        return filtered;
    }
    
    private void copyAllowedElements(Element source, Element target, Document targetDoc, Set<String> allowedElements) {
        NodeList children = source.getChildNodes();
        
        for (int i = 0; i < children.getLength(); i++) {
            Node child = children.item(i);
            
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                Element childElement = (Element) child;
                String tagName = childElement.getTagName();
                
                if (allowedElements.contains(tagName)) {
                    // Copy allowed element
                    Element newElement = targetDoc.createElement(tagName);
                    newElement.setTextContent(childElement.getTextContent());
                    
                    // Copy attributes if allowed
                    copyAllowedAttributes(childElement, newElement, allowedElements);
                    
                    target.appendChild(newElement);
                    
                    // Recursively copy children
                    copyAllowedElements(childElement, newElement, targetDoc, allowedElements);
                }
            }
        }
    }
    
    private void copyAllowedAttributes(Element source, Element target, Set<String> allowedElements) {
        NamedNodeMap attributes = source.getAttributes();
        
        for (int i = 0; i < attributes.getLength(); i++) {
            Attr attr = (Attr) attributes.item(i);
            String attrName = attr.getName();
            
            // Only copy public attributes
            if (!attrName.startsWith("private") && !attrName.startsWith("admin")) {
                target.setAttribute(attrName, attr.getValue());
            }
        }
    }
}

Security Testing

Security Test Framework

public class XMLSecurityTester {
    
    public void runSecurityTests() {
        System.out.println("Running XML Security Tests");
        System.out.println("=========================");
        
        testXXEPrevention();
        testInputValidation();
        testSizeRestrictyions();
        testMalformedXMLHandling();
        testEntityExpansionPrevention();
    }
    
    private void testXXEPrevention() {
        System.out.println("\nTesting XXE Prevention:");
        
        String xxePayload = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                           "<!DOCTYPE foo [\n" +
                           "  <!ENTITY xxe SYSTEM \"file:///etc/passwd\">\n" +
                           "]>\n" +
                           "<root>&xxe;</root>";
        
        try {
            DocumentBuilder secureBuilder = SecureXMLParserFactory.createSecureDocumentBuilder();
            secureBuilder.parse(new ByteArrayInputStream(xxePayload.getBytes()));
            System.out.println("❌ XXE test failed - parser allowed external entity");
        } catch (Exception e) {
            System.out.println("✅ XXE test passed - external entity blocked: " + e.getMessage());
        }
    }
    
    private void testInputValidation() {
        System.out.println("\nTesting Input Validation:");
        
        XMLInputValidator validator = new XMLInputValidator();
        
        // Test oversized input
        String largeInput = "x".repeat(20 * 1024 * 1024); // 20MB
        ValidationResult result = validator.validateXMLInput(largeInput);
        
        if (!result.isValid()) {
            System.out.println("✅ Size validation passed - large input rejected");
        } else {
            System.out.println("❌ Size validation failed - large input accepted");
        }
        
        // Test malicious content
        String maliciousInput = "<root><!ENTITY attack SYSTEM \"file:///etc/passwd\"></root>";
        result = validator.validateXMLInput(maliciousInput);
        
        if (!result.isValid()) {
            System.out.println("✅ Content validation passed - malicious input rejected");
        } else {
            System.out.println("❌ Content validation failed - malicious input accepted");
        }
    }
    
    private void testEntityExpansionPrevention() {
        System.out.println("\nTesting Entity Expansion Prevention:");
        
        String billionLaughsAttack = "<?xml version=\"1.0\"?>\n" +
                                   "<!DOCTYPE lolz [\n" +
                                   "  <!ENTITY lol \"lol\">\n" +
                                   "  <!ENTITY lol2 \"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;\">\n" +
                                   "  <!ENTITY lol3 \"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\">\n" +
                                   "]>\n" +
                                   "<lolz>&lol3;</lolz>";
        
        try {
            DocumentBuilder secureBuilder = SecureXMLParserFactory.createSecureDocumentBuilder();
            secureBuilder.parse(new ByteArrayInputStream(billionLaughsAttack.getBytes()));
            System.out.println("❌ Entity expansion test failed - attack succeeded");
        } catch (Exception e) {
            System.out.println("✅ Entity expansion test passed - attack blocked: " + e.getMessage());
        }
    }
}

Security Checklist

Essential Security Measures

  • [ ] XXE attacks prevention implemented
  • [ ] Input validation and size limits enforced
  • [ ] Secure parser configuration applied
  • [ ] Schema validation from trusted sources only
  • [ ] Sensitive data encryption implemented
  • [ ] Digital signatures for integrity verification
  • [ ] Access control based on user roles
  • [ ] Security testing integrated into CI/CD
  • [ ] Error handling doesn't leak sensitive information
  • [ ] Logging captures security events appropriately

Common Security Anti-Patterns

Avoid:

  • Using default parser configurations
  • Processing untrusted XML without validation
  • Exposing detailed error messages to users
  • Allowing unlimited document sizes
  • Using XML for sensitive data without encryption
  • Trusting client-side validation only
  • Ignoring security updates for XML libraries

Implement:

  • Secure parser configuration
  • Input validation and sanitization
  • Proper error handling and logging
  • Size and complexity limits
  • Encryption for sensitive data
  • Server-side validation
  • Regular security updates

Additional Resources

OWASP XML Security Cheat Sheet

NIST XML Signature Security

W3C XML Encryption Specification