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:

  1. XMLDocument: A DOM-style parser that is NOT safe by default. It resolves external entities unless explicitly configured otherwise.

  2. 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:

  1. Always use .nodeLoadExternalEntitiesNever - Explicitly disable external entity loading when using XMLDocument.

  2. Prefer XMLParser when possible - Use XMLParser for SAX-style parsing as it is safe by default.

  3. Never set shouldResolveExternalEntities to true - This property should always remain false.

  4. Avoid .nodePreserveAll - This option enables multiple features including DTD processing.

  5. Validate XML against a schema - Use XSD validation to ensure XML structure is expected.

  6. Limit input size - Implement size limits to prevent billion laughs attacks.

  7. 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
}

References