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(
"<", "<",
">", ">",
"&", "&",
"\"", """,
"'", "'"
);
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("\"", """)
.replace("'", "'")
.replace("<", "<")
.replace(">", ">")
.replace("&", "&");
// 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