security/core#60 - Fix PHP Object Injection via Phar Deserialization
authorPatrick Figel <pfigel@greenpeace.org>
Tue, 18 Feb 2020 19:44:11 +0000 (20:44 +0100)
committerSeamus Lee <seamuslee001@gmail.com>
Thu, 16 Apr 2020 01:03:21 +0000 (11:03 +1000)
This mitigates Phar deserialization vulnerabilities by registering an
alternative Phar stream wrapper that filters out insecure Phar files.

PHP makes it possible to trigger Object Injection vulnerabilities by using
a side-effect of the phar:// stream wrapper that unserializes Phar
metadata. To mitigate this vulnerability, projects such as TYPO3 and Drupal
have implemented an alternative Phar stream wrapper that disallows
inclusion of phar files based on certain parameters. This change implements
a similar approach for Civi in environments where the vulnerability isn't
mitigated by the CMS.

Fixes security/core#60

CRM/Core/Invoke.php
Civi/Core/Security/PharExtensionInterceptor.php [new file with mode: 0644]
composer.json
composer.lock

index 0e04bb7d1b0503c474201d520a0d42293838d1ec..52c2fdf55115c5cf584b94ea3e794bf7092b1f8e 100644 (file)
@@ -150,6 +150,47 @@ class CRM_Core_Invoke {
     return $item;
   }
 
+  /**
+   * Register an alternative phar:// stream wrapper to filter out insecure Phars
+   *
+   * PHP makes it possible to trigger Object Injection vulnerabilities by using
+   * a side-effect of the phar:// stream wrapper that unserializes Phar
+   * metadata. To mitigate this vulnerability, projects such as TYPO3 and Drupal
+   * have implemented an alternative Phar stream wrapper that disallows
+   * inclusion of phar files based on certain parameters.
+   *
+   * This code attempts to register the TYPO3 Phar stream wrapper using the
+   * interceptor defined in \Civi\Core\Security\PharExtensionInterceptor. In an
+   * environment where the stream wrapper was already registered via
+   * \TYPO3\PharStreamWrapper\Manager (i.e. Drupal), this code does not do
+   * anything. In other environments (e.g. WordPress, at the time of this
+   * writing), the TYPO3 library is used to register the interceptor to mitigate
+   * the vulnerability.
+   */
+  private static function registerPharHandler() {
+    try {
+      // try to get the existing stream wrapper, registered e.g. by Drupal
+      \TYPO3\PharStreamWrapper\Manager::instance();
+    }
+    catch (\LogicException $e) {
+      if ($e->getCode() === 1535189872) {
+        // no phar stream wrapper was registered by \TYPO3\PharStreamWrapper\Manager.
+        // This means we're probably not on Drupal and need to register our own.
+        \TYPO3\PharStreamWrapper\Manager::initialize(
+          (new \TYPO3\PharStreamWrapper\Behavior())
+            ->withAssertion(new \Civi\Core\Security\PharExtensionInterceptor())
+        );
+        if (in_array('phar', stream_get_wrappers())) {
+          stream_wrapper_unregister('phar');
+          stream_wrapper_register('phar', \TYPO3\PharStreamWrapper\PharStreamWrapper::class);
+        }
+      } else {
+        // this is not an exception we can handle
+        throw $e;
+      }
+    }
+  }
+
   /**
    * Given a menu item, call the appropriate controller and return the response
    *
@@ -161,6 +202,8 @@ class CRM_Core_Invoke {
     $ids = new CRM_Core_IDS();
     $ids->check($item);
 
+    self::registerPharHandler();
+
     $config = CRM_Core_Config::singleton();
     if ($config->userFramework == 'Joomla' && $item) {
       $config->userFrameworkURLVar = 'task';
diff --git a/Civi/Core/Security/PharExtensionInterceptor.php b/Civi/Core/Security/PharExtensionInterceptor.php
new file mode 100644 (file)
index 0000000..d2773db
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+namespace Civi\Core\Security;
+
+use TYPO3\PharStreamWrapper\Assertable;
+use TYPO3\PharStreamWrapper\Helper;
+use TYPO3\PharStreamWrapper\Exception;
+
+/**
+ * An alternate PharExtensionInterceptor to support phar-based CLI tools.
+ *
+ * This is largely based on Drupal\Core\Security\PharExtensionInterceptor,
+ * originally licensed under GPL2+
+ *
+ * @see \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor
+ */
+class PharExtensionInterceptor implements Assertable {
+
+  /**
+   * Determines whether phar file is allowed to execute.
+   *
+   * The phar file is allowed to execute if:
+   * - the base file name has a ".phar" suffix.
+   * - it is the CLI tool that has invoked the interceptor.
+   *
+   * @param string $path
+   *   The path of the phar file to check.
+   * @param string $command
+   *   The command being carried out.
+   *
+   * @return bool
+   *   TRUE if the phar file is allowed to execute.
+   *
+   * @throws Exception
+   *   Thrown when the file is not allowed to execute.
+   */
+  public function assert(string $path, string $command): bool {
+    if ($this->baseFileContainsPharExtension($path)) {
+      return TRUE;
+    }
+    throw new Exception(
+      sprintf(
+        'Unexpected file extension in "%s"',
+        $path
+      ),
+      1535198703
+    );
+  }
+
+  /**
+   * Determines if a path has a .phar extension or invoked execution.
+   *
+   * @param string $path
+   *   The path of the phar file to check.
+   *
+   * @return bool
+   *   TRUE if the file has a .phar extension or if the execution has been
+   *   invoked by the phar file.
+   */
+  private function baseFileContainsPharExtension($path) {
+    $baseFile = Helper::determineBaseFile($path);
+    if ($baseFile === NULL) {
+      return FALSE;
+    }
+    // If the stream wrapper is registered by invoking a phar file that does
+    // not not have .phar extension then this should be allowed. For
+    // example, some CLI tools recommend removing the extension.
+    $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
+    // Find the last entry in the backtrace containing a 'file' key as
+    // sometimes the last caller is executed outside the scope of a file. For
+    // example, this occurs with shutdown functions.
+    do {
+      $caller = array_pop($backtrace);
+    } while (empty($caller['file']) && !empty($backtrace));
+    if (isset($caller['file']) && $baseFile === Helper::determineBaseFile($caller['file'])) {
+      return TRUE;
+    }
+    $fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION);
+    return strtolower($fileExtension) === 'phar';
+  }
+
+}
index 9debce181a32e140b7af3b58b21a5acaadd4185c..a57bd35b5d5a1cd1da58126b0c191f0867ea0954 100644 (file)
@@ -74,7 +74,8 @@
     "civicrm/composer-downloads-plugin": "^2.0",
     "league/csv": "^9.2",
     "tplaner/when": "~3.0.0",
-    "xkerman/restricted-unserialize": "~1.1"
+    "xkerman/restricted-unserialize": "~1.1",
+    "typo3/phar-stream-wrapper": "^3.0"
   },
   "scripts": {
     "post-install-cmd": [
index 4ec8844931d8a1605f027f4768abf0d3b4e179a8..2d5bc5bce25b6738d3363180751d22ced5fd4dd8 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "9c8e8054f45d5bdd4e18f45701527d2b",
+    "content-hash": "94187b287c901c73a271a012b55ae19a",
     "packages": [
         {
             "name": "adrienrn/php-mimetyper",
             "version": "3.0.0+php53",
             "dist": {
                 "type": "zip",
-                "url": "https://github.com/tplaner/When/archive/c1ec099f421bff354cc5c929f83b94031423fc80.zip",
-                "reference": null,
-                "shasum": null
+                "url": "https://github.com/tplaner/When/archive/c1ec099f421bff354cc5c929f83b94031423fc80.zip"
             },
             "require": {
                 "php": ">=5.3.0"
                 "time"
             ]
         },
+        {
+            "name": "typo3/phar-stream-wrapper",
+            "version": "v3.1.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/TYPO3/phar-stream-wrapper.git",
+                "reference": "e0c1b495cfac064f4f5c4bcb6bf67bb7f345ed04"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/TYPO3/phar-stream-wrapper/zipball/e0c1b495cfac064f4f5c4bcb6bf67bb7f345ed04",
+                "reference": "e0c1b495cfac064f4f5c4bcb6bf67bb7f345ed04",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "php": "^7.0"
+            },
+            "require-dev": {
+                "ext-xdebug": "*",
+                "phpunit/phpunit": "^6.5"
+            },
+            "suggest": {
+                "ext-fileinfo": "For PHP builtin file type guessing, otherwise uses internal processing"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "v3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "TYPO3\\PharStreamWrapper\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Interceptors for PHP's native phar:// stream handling",
+            "homepage": "https://typo3.org/",
+            "keywords": [
+                "phar",
+                "php",
+                "security",
+                "stream-wrapper"
+            ],
+            "time": "2019-12-10T11:53:27+00:00"
+        },
         {
             "name": "xkerman/restricted-unserialize",
             "version": "1.1.12",