PHP XXE Prevention
Secure XML parsing configuration for PHP applications using libxml2, SimpleXML, DOMDocument, and XMLReader.
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
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)
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+)
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
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
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:
- Call libxml_disable_entity_loader(true) globally
- Never use LIBXML_NOENT flag
- Use LIBXML_NONET to block network access
- 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:
- Never use LIBXML_NOENT flag
- Use LIBXML_NONET for defense in depth
- 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
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)
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
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.)