Skip to content
🔵Info0.0

PHP XXE Prevention

Secure XML parsing configuration for PHP applications using libxml2, SimpleXML, DOMDocument, and XMLReader.

CWE-611: Improper Restriction of XML External Entity ReferenceOWASP Top 10:2021 - A05: Security Misconfiguration

Overview

PHP uses the libxml2 library for XML parsing across all its XML functions. This provides a centralized security mechanism: libxml_disable_entity_loader().

Key PHP XML APIs:

  • DOMDocument - DOM parser (most common)
  • SimpleXML - Simple API for XML
  • XMLReader - Stream-based reader
  • xml_parse() - Event-based SAX parser

Critical Security Function: libxml_disable_entity_loader(true) - Disables loading of external entities

Important Notes:

  • PHP < 8.0: Must call libxml_disable_entity_loader(true)
  • PHP 8.0+: libxml_disable_entity_loader() is DEPRECATED (safe by default)
  • LIBXML_NOENT flag: NEVER use this - enables entity expansion
  • Default behavior varies by PHP version

Vulnerable DOMDocument

PHPvulnerable.php⚠️ Vulnerable
1<?php
2// VULNERABLE: Default settings in PHP < 8.0
3$xml = $_POST['xml'];
4
5$dom = new DOMDocument();
6
7// DANGEROUS: LIBXML_NOENT enables entity expansion
8$dom->loadXML($xml, LIBXML_NOENT);
9// This will process external entities and expand them!
10
11// Also vulnerable: Default loadXML without flags
12$dom->loadXML($xml);
13// In PHP < 8.0, this may process external entities
14
15echo $dom->textContent;
16// XXE payload will disclose files or perform SSRF
17?>

Secure DOMDocument (PHP < 8.0)

PHPsecure.phpâś“ Secure
1<?php
2$xml = $_POST['xml'];
3
4// SECURE: Disable external entity loading (PHP < 8.0)
5libxml_disable_entity_loader(true);
6
7$dom = new DOMDocument();
8
9// Load XML without entity expansion flags
10// Do NOT use LIBXML_NOENT
11$dom->loadXML($xml, LIBXML_DTDLOAD | LIBXML_DTDATTR);
12
13// Additional security: Suppress errors to avoid information disclosure
14libxml_use_internal_errors(true);
15
16if ($dom->loadXML($xml, LIBXML_DTDLOAD | LIBXML_DTDATTR)) {
17    // Process XML safely
18    echo $dom->textContent;
19} else {
20    // Handle errors without exposing details
21    http_response_code(400);
22    echo "Invalid XML";
23}
24
25// Clear errors
26libxml_clear_errors();
27?>

Secure Configuration (PHP 8.0+)

PHPphp8-secure.phpâś“ Secure
1<?php
2// PHP 8.0+ is secure by default
3// libxml_disable_entity_loader() is deprecated and no longer needed
4
5$xml = $_POST['xml'];
6
7$dom = new DOMDocument();
8
9// Suppress errors for security
10libxml_use_internal_errors(true);
11
12// Load XML - external entities disabled by default in PHP 8.0+
13if ($dom->loadXML($xml)) {
14    // Safe to process
15    echo $dom->textContent;
16} else {
17    // Get errors but don't expose them to users
18    $errors = libxml_get_errors();
19    libxml_clear_errors();
20    
21    // Log errors securely
22    error_log("XML parse error: " . print_r($errors, true));
23    
24    http_response_code(400);
25    echo "Invalid XML format";
26}
27
28// NEVER use LIBXML_NOENT flag - it's dangerous in all PHP versions
29// $dom->loadXML($xml, LIBXML_NOENT);  // DON'T DO THIS
30?>

Secure SimpleXML

PHPsimplexml-secure.phpâś“ Secure
1<?php
2$xml = $_POST['xml'];
3
4// For PHP < 8.0
5libxml_disable_entity_loader(true);
6libxml_use_internal_errors(true);
7
8// Secure SimpleXML usage
9$simple = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NONET);
10
11if ($simple === false) {
12    // Parse failed - handle securely
13    libxml_clear_errors();
14    http_response_code(400);
15    echo "Invalid XML";
16    exit;
17}
18
19// Access elements safely
20foreach ($simple->item as $item) {
21    echo htmlspecialchars($item->name) . "<br>";
22}
23
24// Alternative: Disable all external resource loading
25$options = LIBXML_NONET | LIBXML_NOCDATA | LIBXML_NOERROR | LIBXML_NOWARNING;
26$simple = simplexml_load_string($xml, 'SimpleXMLElement', $options);
27?>

Secure XMLReader

PHPxmlreader-secure.phpâś“ Secure
1<?php
2$xml = $_POST['xml'];
3
4// For PHP < 8.0
5libxml_disable_entity_loader(true);
6
7// XMLReader secure configuration
8$reader = new XMLReader();
9$reader->xml($xml);
10
11// Disable external entity resolution
12$reader->setParserProperty(XMLReader::LOADDTD, false);
13$reader->setParserProperty(XMLReader::DEFAULTATTRS, false);
14$reader->setParserProperty(XMLReader::VALIDATE, false);
15$reader->setParserProperty(XMLReader::SUBST_ENTITIES, false);  // Don't expand entities
16
17while ($reader->read()) {
18    if ($reader->nodeType == XMLReader::ELEMENT) {
19        echo "Element: " . htmlspecialchars($reader->name) . "<br>";
20        
21        // Read element value safely
22        if ($reader->hasValue) {
23            echo "Value: " . htmlspecialchars($reader->value) . "<br>";
24        }
25    }
26}
27
28$reader->close();
29?>

libxml Flags Explained

DANGEROUS FLAGS (Never Use):

LIBXML_NOENT - Substitute entities (enables XXE)

  • Expands all entities during parsing
  • Makes XXE exploitation trivial
  • NEVER use this flag

LIBXML_DTDLOAD - Load external DTD

  • Can load DTDs from external sources
  • Dangerous when combined with entity expansion
  • Avoid unless absolutely necessary

SAFE FLAGS (Recommended):

LIBXML_NONET - Disable network access

  • Prevents loading external entities via network
  • Highly recommended for security
  • Use in combination with other protections

LIBXML_NOCDATA - Merge CDATA as text

  • No security impact
  • Optional based on application needs

LIBXML_NOERROR / LIBXML_NOWARNING - Suppress errors

  • Prevents information disclosure in error messages
  • Use with libxml_use_internal_errors(true)

Best Practice Combination:

$options = LIBXML_NONET | LIBXML_NOCDATA;
// With libxml_disable_entity_loader(true) in PHP < 8.0

PHP Version-Specific Guidance

PHP 5.x - 7.4:

Required security measures:

  1. Call libxml_disable_entity_loader(true) globally
  2. Never use LIBXML_NOENT flag
  3. Use LIBXML_NONET to block network access
  4. Use libxml_use_internal_errors(true) to suppress error disclosure

Example:

libxml_disable_entity_loader(true);
libxml_use_internal_errors(true);
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NONET);

PHP 8.0+:

Default security:

  • External entity loading disabled by default
  • libxml_disable_entity_loader() is deprecated
  • No need to call it (will trigger deprecation warning)

Still required:

  1. Never use LIBXML_NOENT flag
  2. Use LIBXML_NONET for defense in depth
  3. Use libxml_use_internal_errors(true)

Example:

libxml_use_internal_errors(true);
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NONET);

Migration from PHP 7.4 to 8.0: Remove libxml_disable_entity_loader(true) calls to avoid deprecation warnings.

Input Validation Layer

PHPSecureXMLParser.phpâś“ Secure
1<?php
2class SecureXMLParser {
3    
4    public static function parse($xmlData) {
5        // Input validation before parsing
6        if (self::containsDangerousPatterns($xmlData)) {
7            throw new Exception("Potentially malicious XML detected");
8        }
9        
10        // Size limit (prevent DoS)
11        if (strlen($xmlData) > 1000000) {  // 1MB limit
12            throw new Exception("XML too large");
13        }
14        
15        // Configure secure parsing
16        if (PHP_VERSION_ID < 80000) {
17            libxml_disable_entity_loader(true);
18        }
19        
20        libxml_use_internal_errors(true);
21        
22        $dom = new DOMDocument();
23        $success = $dom->loadXML($xmlData, LIBXML_NONET | LIBXML_NOCDATA);
24        
25        if (!$success) {
26            $errors = libxml_get_errors();
27            libxml_clear_errors();
28            error_log("XML parse error: " . print_r($errors, true));
29            throw new Exception("Invalid XML format");
30        }
31        
32        return $dom;
33    }
34    
35    private static function containsDangerousPatterns($xml) {
36        // Reject XML with DOCTYPE or ENTITY declarations
37        $dangerous_patterns = [
38            '/<!DOCTYPE/i',
39            '/<!ENTITY/i',
40            '/SYSTEM\s+["\']file:/i',
41            '/SYSTEM\s+["\']http:/i',
42            '/SYSTEM\s+["\']ftp:/i'
43        ];
44        
45        foreach ($dangerous_patterns as $pattern) {
46            if (preg_match($pattern, $xml)) {
47                return true;
48            }
49        }
50        
51        return false;
52    }
53}
54
55// Usage
56try {
57    $dom = SecureXMLParser::parse($_POST['xml']);
58    // Process safely
59    echo $dom->textContent;
60} catch (Exception $e) {
61    http_response_code(400);
62    echo "Error processing XML";
63}

Framework Integration (Laravel/Symfony)

PHPXMLController.phpâś“ Secure
1<?php
2// Laravel Controller Example
3namespace App\Http\Controllers;
4
5use Illuminate\Http\Request;
6use Illuminate\Support\Facades\Log;
7
8class XMLController extends Controller
9{
10    public function process(Request $request)
11    {
12        $xmlData = $request->getContent();
13        
14        // Validate content type
15        if ($request->header('Content-Type') !== 'application/xml') {
16            return response()->json(['error' => 'Invalid content type'], 400);
17        }
18        
19        // Size validation
20        if (strlen($xmlData) > 1048576) {  // 1MB
21            return response()->json(['error' => 'XML too large'], 413);
22        }
23        
24        try {
25            // Secure parsing
26            if (PHP_VERSION_ID < 80000) {
27                libxml_disable_entity_loader(true);
28            }
29            
30            libxml_use_internal_errors(true);
31            
32            $dom = new \DOMDocument();
33            if (!$dom->loadXML($xmlData, LIBXML_NONET)) {
34                $errors = libxml_get_errors();
35                libxml_clear_errors();
36                
37                Log::warning('XML parse error', ['errors' => $errors]);
38                return response()->json(['error' => 'Invalid XML'], 400);
39            }
40            
41            // Process XML
42            $result = $this->processDocument($dom);
43            
44            return response()->json(['result' => $result]);
45            
46        } catch (\Exception $e) {
47            Log::error('XML processing error: ' . $e->getMessage());
48            return response()->json(['error' => 'Processing failed'], 500);
49        }
50    }
51    
52    private function processDocument(\DOMDocument $dom)
53    {
54        // Safe processing logic
55        return ['status' => 'success'];
56    }
57}

Security Testing

PHPXXEPreventionTest.phpâś“ Secure
1<?php
2// PHPUnit test for XXE prevention
3use PHPUnit\Framework\TestCase;
4
5class XXEPreventionTest extends TestCase
6{
7    public function testXXEBlocked()
8    {
9        $xxePayload = '<?xml version="1.0"?>
10            <!DOCTYPE root [
11              <!ENTITY xxe SYSTEM "file:///etc/passwd">
12            ]>
13            <root>
14              <data>&xxe;</data>
15            </root>';
16        
17        if (PHP_VERSION_ID < 80000) {
18            libxml_disable_entity_loader(true);
19        }
20        
21        libxml_use_internal_errors(true);
22        
23        $dom = new DOMDocument();
24        $result = $dom->loadXML($xxePayload, LIBXML_NONET);
25        
26        // Should parse but not expand entities
27        $this->assertTrue($result);
28        
29        // Content should NOT contain /etc/passwd contents
30        $content = $dom->textContent;
31        $this->assertStringNotContainsString('root:x:0:0', $content);
32        
33        libxml_clear_errors();
34    }
35    
36    public function testValidXMLAccepted()
37    {
38        $validXml = '<root><data>test</data></root>';
39        
40        if (PHP_VERSION_ID < 80000) {
41            libxml_disable_entity_loader(true);
42        }
43        
44        $dom = new DOMDocument();
45        $result = $dom->loadXML($validXml, LIBXML_NONET);
46        
47        $this->assertTrue($result);
48        $this->assertEquals('test', $dom->textContent);
49    }
50}
51?>

PHP XXE Prevention Checklist

âś… For PHP < 8.0:

  • Call libxml_disable_entity_loader(true) before parsing XML
  • Never use LIBXML_NOENT flag
  • Use LIBXML_NONET flag for network blocking
  • Use libxml_use_internal_errors(true) to suppress error disclosure
  • Clear errors with libxml_clear_errors()

âś… For PHP 8.0+:

  • Remove libxml_disable_entity_loader() calls (deprecated)
  • Never use LIBXML_NOENT flag
  • Use LIBXML_NONET flag for defense in depth
  • Use libxml_use_internal_errors(true)

âś… Input Validation:

  • Reject XML containing <!DOCTYPE if not required
  • Reject XML containing <!ENTITY declarations
  • Implement size limits (prevent DoS)
  • Validate XML against expected schema

âś… Testing:

  • Unit tests with XXE payloads
  • Integration tests with security scanner
  • Regular penetration testing
  • Monitor error logs for XXE attempts

âś… Defense in Depth:

  • Network egress filtering
  • File system permissions
  • WAF rules for XXE patterns
  • Security headers (X-Content-Type-Options, etc.)