XML External Entity (XXE)
ID |
swift.xxe |
Severity |
critical |
Resource |
Misconfiguration |
Language |
Swift |
Tags |
CWE:611, CWE:776, NIST.SP.800-53, OWASP:2021:A5, PCI-DSS:6.5.1 |
Description
XML External Entity (XXE) vulnerabilities occur when an XML parser is configured to process external entities without proper restrictions. When processing untrusted XML input, this misconfiguration allows attackers to:
-
Read local files: Access sensitive files on the server’s filesystem
-
Perform SSRF attacks: Make requests to internal network resources
-
Cause denial of service: Use entity expansion attacks like "Billion Laughs"
-
Execute remote code: In some configurations with expect:// or other protocols
The vulnerability is particularly dangerous because it exploits a feature of XML itself—the ability to define entities that reference external resources. When an XML parser processes a document containing malicious entity definitions, it follows those references, potentially exposing sensitive data or consuming excessive resources.
Rationale
XML parsers in Swift come in different flavors with different default security postures:
-
XMLDocument: A DOM-style parser that is NOT safe by default. It resolves external entities unless explicitly configured otherwise.
-
XMLParser: A SAX-style streaming parser that is safe by default. It does not expand external entities.
The following is an example of vulnerable code using XMLDocument without proper configuration:
import Foundation
func parseXML(xmlString: String) {
do {
// VULNERABLE: No options specified - external entities will be resolved
let xmlDoc = try XMLDocument(xmlString: xmlString)
// Process the document
let rootElement = xmlDoc.rootElement()
print(rootElement?.stringValue ?? "")
} catch {
print("Error parsing XML: \(error)")
}
}
// Attacker can provide malicious XML:
let maliciousXML = """
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>
"""
parseXML(xmlString: maliciousXML)
// This will read and include the contents of /etc/passwd
Another vulnerable pattern explicitly enables external entity resolution:
import Foundation
func parseXMLFromURL(url: URL) {
do {
// VULNERABLE: Using options that allow external entities
let xmlDoc = try XMLDocument(
contentsOf: url,
options: [.nodePreserveAll] // This includes DTD and entity processing
)
// Process the document
processDocument(xmlDoc)
} catch {
print("Error: \(error)")
}
}
Setting the shouldResolveExternalEntities property to true:
import Foundation
func configureParser(xmlString: String) {
do {
let xmlDoc = try XMLDocument(xmlString: xmlString, options: [])
// VULNERABLE: Explicitly enabling external entity resolution
xmlDoc.shouldResolveExternalEntities = true
// Now the document will resolve external entities
processDocument(xmlDoc)
} catch {
print("Error: \(error)")
}
}
Example of a "Billion Laughs" DTD bomb attack:
let billionLaughsXML = """
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>
"""
// VULNERABLE: Parser will expand entities exponentially
let xmlDoc = try XMLDocument(xmlString: billionLaughsXML)
// This causes exponential memory consumption and DoS
Remediation
To prevent XXE vulnerabilities, always configure XML parsers to disable external entity processing.
Here is the revised, secure code example using XMLDocument:
import Foundation
func parseXMLSafely(xmlString: String) {
do {
// SAFE: Explicitly disable external entity loading
let xmlDoc = try XMLDocument(
xmlString: xmlString,
options: .nodeLoadExternalEntitiesNever
)
// Process the document safely
let rootElement = xmlDoc.rootElement()
print(rootElement?.stringValue ?? "")
} catch {
print("Error parsing XML: \(error)")
}
}
For parsing XML from a URL:
import Foundation
func parseXMLFromURLSafely(url: URL) {
do {
// SAFE: Disable external entity loading
let xmlDoc = try XMLDocument(
contentsOf: url,
options: .nodeLoadExternalEntitiesNever
)
processDocument(xmlDoc)
} catch {
print("Error: \(error)")
}
}
For parsing XML from Data:
import Foundation
func parseXMLFromDataSafely(data: Data) {
do {
// SAFE: Disable external entity loading
let xmlDoc = try XMLDocument(
data: data,
options: .nodeLoadExternalEntitiesNever
)
processDocument(xmlDoc)
} catch {
print("Error: \(error)")
}
}
Using XMLParser (safe by default):
import Foundation
class SafeXMLParserDelegate: NSObject, XMLParserDelegate {
func parser(_ parser: XMLParser, didStartElement elementName: String,
namespaceURI: String?, qualifiedName qName: String?,
attributes attributeDict: [String : String] = [:]) {
// Handle element start
print("Started element: \(elementName)")
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
// Handle character data
print("Found characters: \(string)")
}
}
func parseWithXMLParser(xmlString: String) {
// SAFE: XMLParser does not resolve external entities by default
let parser = XMLParser(data: xmlString.data(using: .utf8)!)
let delegate = SafeXMLParserDelegate()
parser.delegate = delegate
if parser.parse() {
print("Parsing succeeded")
} else {
print("Parsing failed: \(parser.parserError?.localizedDescription ?? "")")
}
}
Combining options safely:
import Foundation
func parseXMLWithMultipleOptions(xmlString: String) {
do {
// SAFE: Combine options with explicit external entity disabling
let options: XMLNode.Options = [
.nodeLoadExternalEntitiesNever, // Disable external entities
.nodePreserveWhitespace, // Other options as needed
.documentTidyXML // Tidy the XML
]
let xmlDoc = try XMLDocument(xmlString: xmlString, options: options)
processDocument(xmlDoc)
} catch {
print("Error: \(error)")
}
}
Explicitly ensuring safe configuration:
import Foundation
func ensureSafeXMLDocument(xmlString: String) -> XMLDocument? {
do {
let xmlDoc = try XMLDocument(
xmlString: xmlString,
options: .nodeLoadExternalEntitiesNever
)
// Double-check the property is false
if xmlDoc.shouldResolveExternalEntities {
print("Warning: External entities are enabled!")
xmlDoc.shouldResolveExternalEntities = false
}
return xmlDoc
} catch {
print("Error: \(error)")
return nil
}
}
Best practices for secure XML parsing:
-
Always use
.nodeLoadExternalEntitiesNever- Explicitly disable external entity loading when usingXMLDocument. -
Prefer
XMLParserwhen possible - Use XMLParser for SAX-style parsing as it is safe by default. -
Never set
shouldResolveExternalEntitiesto true - This property should always remain false. -
Avoid
.nodePreserveAll- This option enables multiple features including DTD processing. -
Validate XML against a schema - Use XSD validation to ensure XML structure is expected.
-
Limit input size - Implement size limits to prevent billion laughs attacks.
-
Use allowlist validation - Validate that XML content matches expected patterns.
Example of comprehensive secure implementation:
import Foundation
class SecureXMLParser {
private let maxInputSize: Int
init(maxInputSize: Int = 1_000_000) { // 1 MB default limit
self.maxInputSize = maxInputSize
}
func parseXML(xmlString: String) throws -> XMLDocument {
// Validate input size
guard xmlString.utf8.count <= maxInputSize else {
throw XMLParseError.inputTooLarge
}
// Parse with safe options
let options: XMLNode.Options = .nodeLoadExternalEntitiesNever
let xmlDoc = try XMLDocument(xmlString: xmlString, options: options)
// Ensure external entities are disabled
if xmlDoc.shouldResolveExternalEntities {
xmlDoc.shouldResolveExternalEntities = false
}
// Validate against expected structure
try validateStructure(xmlDoc)
return xmlDoc
}
private func validateStructure(_ document: XMLDocument) throws {
// Implement structure validation
guard let root = document.rootElement() else {
throw XMLParseError.invalidStructure
}
// Add specific validations as needed
}
}
enum XMLParseError: Error {
case inputTooLarge
case invalidStructure
}