Browse Source

Added heuristic check for XXE/XEE attacks with PHP-FPM

Enrico Zimuel 12 years ago
parent
commit
49ba9641e6
1 changed files with 71 additions and 19 deletions
  1. 71 19
      library/Zend/Xml/Security.php

+ 71 - 19
library/Zend/Xml/Security.php

@@ -28,8 +28,24 @@
  */
 class Zend_Xml_Security
 {
+    const ENTITY_DETECT = 'Detected use of ENTITY in XML, disabled to prevent XXE/XEE attacks';
+
     /**
-     * Scan XML string for potential XXE and XEE attacks 
+     * Heuristic scan to detect entity in XML
+     *
+     * @param  string $xml
+     * @throws Zend_Xml_Exception
+     */
+    protected static function heuristicScan($xml)
+    {
+        if (strpos($xml, '<!ENTITY') !== false) {
+            require_once 'Exception.php';
+            throw new Zend_Xml_Exception(self::ENTITY_DETECT);
+        }
+    }
+
+    /**
+     * Scan XML string for potential XXE and XEE attacks
      *
      * @param   string $xml
      * @param   DomDocument $dom
@@ -38,40 +54,63 @@ class Zend_Xml_Security
      */
     public static function scan($xml, DOMDocument $dom = null)
     {
+        // If running with PHP-FPM we perform an heuristic scan
+        // We cannot use libxml_disable_entity_loader because of this bug
+        // @see https://bugs.php.net/bug.php?id=64938
+        if (self::isPhpFpm()) {
+            self::heuristicScan($xml);
+        }
+
         if (null === $dom) {
             $simpleXml = true;
             $dom = new DOMDocument();
-        } 
+        }
+
+        if (!self::isPhpFpm()) {
+            $loadEntities = libxml_disable_entity_loader(true);
+            $useInternalXmlErrors = libxml_use_internal_errors(true);
+        }
 
-        // Disable entity load
-        $loadEntities = libxml_disable_entity_loader(true);
-        $useInternalXmlErrors = libxml_use_internal_errors(true);
+        // Load XML with network access disabled (LIBXML_NONET)
+        // error disabled with @ for PHP-FPM scenario
+        set_error_handler(function ($errno, $errstr) {
+            if (substr_count($errstr, 'DOMDocument::loadXML()') > 0) {
+                return true;
+            }
+            return false;
+        }, E_WARNING);
+        $result = $dom->loadXml($xml, LIBXML_NONET);
+        restore_error_handler();
 
-        if (!$dom->loadXml($xml)) {
+        if (!$result) {
             // Entity load to previous setting
-            libxml_disable_entity_loader($loadEntities);
-            libxml_use_internal_errors($useInternalXmlErrors);
+            if (!self::isPhpFpm()) {
+                libxml_disable_entity_loader($loadEntities);
+                libxml_use_internal_errors($useInternalXmlErrors);
+            }
             return false;
         }
 
-        // Scan for potential XEE attacks using Entity
-        foreach ($dom->childNodes as $child) {
-            if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
-                if ($child->entities->length > 0) {
-                    require_once 'Exception.php';
-                    throw new Zend_Xml_Exception(
-                        'Detected use of ENTITY_NODE in XML, disabled to prevent XEE attacks'
-                    );
+        // Scan for potential XEE attacks using ENTITY, if not PHP-FPM
+        if (!self::isPhpFpm()) {
+            foreach ($dom->childNodes as $child) {
+                if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
+                    if ($child->entities->length > 0) {
+                        require_once 'Exception.php';
+                        throw new Zend_Xml_Exception(self::ENTITY_DETECT);
+                    }
                 }
             }
         }
 
         // Entity load to previous setting
-        libxml_disable_entity_loader($loadEntities);
-        libxml_use_internal_errors($useInternalXmlErrors);
+        if (!self::isPhpFpm()) {
+            libxml_disable_entity_loader($loadEntities);
+            libxml_use_internal_errors($useInternalXmlErrors);
+        }
 
         if (isset($simpleXml)) {
-            $result = simplexml_import_dom($dom); 
+            $result = simplexml_import_dom($dom);
             if (!$result instanceof SimpleXMLElement) {
                 return false;
             }
@@ -98,4 +137,17 @@ class Zend_Xml_Security
         }
         return self::scan(file_get_contents($file), $dom);
     }
+
+    /**
+     * Return true if PHP is running with PHP-FPM
+     *
+     * @return boolean
+     */
+    public static function isPhpFpm()
+    {
+        if (substr(php_sapi_name(), 0, 3) === 'fpm') {
+            return true;
+        }
+        return false;
+    }
 }