From: Patrick Figel Date: Tue, 18 Feb 2020 19:44:11 +0000 (+0100) Subject: security/core#60 - Fix PHP Object Injection via Phar Deserialization X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=2d38c68770e123f24f3b5d1db1ce0944148de6be;p=civicrm-core.git security/core#60 - Fix PHP Object Injection via Phar Deserialization 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 --- diff --git a/CRM/Core/Invoke.php b/CRM/Core/Invoke.php index 0e04bb7d1b..52c2fdf551 100644 --- a/CRM/Core/Invoke.php +++ b/CRM/Core/Invoke.php @@ -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 index 0000000000..d2773db119 --- /dev/null +++ b/Civi/Core/Security/PharExtensionInterceptor.php @@ -0,0 +1,82 @@ +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'; + } + +} diff --git a/composer.json b/composer.json index 9debce181a..a57bd35b5d 100644 --- a/composer.json +++ b/composer.json @@ -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": [ diff --git a/composer.lock b/composer.lock index 4ec8844931..2d5bc5bce2 100644 --- a/composer.lock +++ b/composer.lock @@ -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", @@ -2431,9 +2431,7 @@ "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" @@ -2465,6 +2463,56 @@ "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",