From bc95d82c443a61917298fcd002ba8bd478e9cb12 Mon Sep 17 00:00:00 2001 From: Patrick Figel Date: Tue, 18 Feb 2020 20:44:11 +0100 Subject: [PATCH] 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 --- CRM/Core/Invoke.php | 43 ++++++++++ .../Security/PharExtensionInterceptor.php | 82 +++++++++++++++++++ composer.json | 3 +- composer.lock | 66 +++++++++++++-- 4 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 Civi/Core/Security/PharExtensionInterceptor.php diff --git a/CRM/Core/Invoke.php b/CRM/Core/Invoke.php index 1ea5fa8b16..2cd7117fa8 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 8d94f744f9..231aa2ec47 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 a699277482..9b76c40f6f 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": "50e07f942de796516f50da3b20ce018b", + "content-hash": "7eef3494b6480d6dcb1bea4a73375960", "packages": [ { "name": "cache/integration-tests", @@ -166,16 +166,16 @@ }, { "name": "civicrm/composer-downloads-plugin", - "version": "v2.0.0", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/civicrm/composer-downloads-plugin.git", - "reference": "869b7a12f57b2d912f0ea77d5c33c1518b8de27d" + "reference": "8722bc7d547315be39397a3078bb51ee053ca269" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/civicrm/composer-downloads-plugin/zipball/869b7a12f57b2d912f0ea77d5c33c1518b8de27d", - "reference": "869b7a12f57b2d912f0ea77d5c33c1518b8de27d", + "url": "https://api.github.com/repos/civicrm/composer-downloads-plugin/zipball/8722bc7d547315be39397a3078bb51ee053ca269", + "reference": "8722bc7d547315be39397a3078bb51ee053ca269", "shasum": "" }, "require": { @@ -213,7 +213,7 @@ } ], "description": "Composer plugin for downloading additional files within any composer package.", - "time": "2019-08-22T10:56:51+00:00" + "time": "2019-08-28T00:33:51+00:00" }, { "name": "cweagans/composer-patches", @@ -2380,9 +2380,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" @@ -2414,6 +2412,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", -- 2.25.1