Chris@17: [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/TYPO3/phar-stream-wrapper/badges/quality-score.png?b=v2)](https://scrutinizer-ci.com/g/TYPO3/phar-stream-wrapper/?branch=v2) Chris@17: [![Travis CI Build Status](https://travis-ci.org/TYPO3/phar-stream-wrapper.svg?branch=v2)](https://travis-ci.org/TYPO3/phar-stream-wrapper) Chris@17: Chris@17: # PHP Phar Stream Wrapper Chris@17: Chris@17: ## Abstract & History Chris@17: Chris@17: Based on Sam Thomas' findings concerning Chris@17: [insecure deserialization in combination with obfuscation strategies](https://blog.secarma.co.uk/labs/near-phar-dangerous-unserialization-wherever-you-are) Chris@17: allowing to hide Phar files inside valid image resources, the TYPO3 project Chris@17: decided back then to introduce a `PharStreamWrapper` to intercept invocations Chris@17: of the `phar://` stream in PHP and only allow usage for defined locations in Chris@17: the file system. Chris@17: Chris@17: Since the TYPO3 mission statement is **inspiring people to share**, we thought Chris@17: it would be helpful for others to release our `PharStreamWrapper` as standalone Chris@17: package to the PHP community. Chris@17: Chris@17: The mentioned security issue was reported to TYPO3 on 10th June 2018 by Sam Thomas Chris@17: and has been addressed concerning the specific attack vector and for this generic Chris@17: `PharStreamWrapper` in TYPO3 versions 7.6.30 LTS, 8.7.17 LTS and 9.3.1 on 12th Chris@17: July 2018. Chris@17: Chris@17: * https://typo3.org/security/advisory/typo3-core-sa-2018-002/ Chris@17: * https://blog.secarma.co.uk/labs/near-phar-dangerous-unserialization-wherever-you-are Chris@17: * https://youtu.be/GePBmsNJw6Y Chris@17: Chris@17: ## License Chris@17: Chris@17: In general the TYPO3 core is released under the GNU General Public License version Chris@17: 2 or any later version (`GPL-2.0-or-later`). In order to avoid licensing issues and Chris@17: incompatibilities this `PharStreamWrapper` is licenced under the MIT License. In case Chris@17: you duplicate or modify source code, credits are not required but really appreciated. Chris@17: Chris@17: ## Credits Chris@17: Chris@17: Thanks to [Alex Pott](https://github.com/alexpott), Drupal for creating Chris@17: back-ports of all sources in order to provide compatibility with PHP v5.3. Chris@17: Chris@17: ## Installation Chris@17: Chris@17: The `PharStreamWrapper` is provided as composer package `typo3/phar-stream-wrapper` Chris@17: and has minimum requirements of PHP v5.3 ([`v2`](https://github.com/TYPO3/phar-stream-wrapper/tree/v2) branch) and PHP v7.0 ([`master`](https://github.com/TYPO3/phar-stream-wrapper) branch). Chris@17: Chris@17: ### Installation for PHP v7.0 Chris@17: Chris@17: ``` Chris@17: composer require typo3/phar-stream-wrapper ^3.0 Chris@17: ``` Chris@17: Chris@17: ### Installation for PHP v5.3 Chris@17: Chris@17: ``` Chris@17: composer require typo3/phar-stream-wrapper ^2.0 Chris@17: ``` Chris@17: Chris@17: ## Example Chris@17: Chris@17: The following example is bundled within this package, the shown Chris@17: `PharExtensionInterceptor` denies all stream wrapper invocations files Chris@17: not having the `.phar` suffix. Interceptor logic has to be individual and Chris@17: adjusted to according requirements. Chris@17: Chris@17: ``` Chris@17: $behavior = new \TYPO3\PharStreamWrapper\Behavior(); Chris@18: \TYPO3\PharStreamWrapper\Manager::initialize( Chris@17: $behavior->withAssertion(new PharExtensionInterceptor()) Chris@17: ); Chris@17: Chris@17: if (in_array('phar', stream_get_wrappers())) { Chris@17: stream_wrapper_unregister('phar'); Chris@17: stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); Chris@17: } Chris@17: ``` Chris@17: Chris@17: * `PharStreamWrapper` defined as class reference will be instantiated each time Chris@17: `phar://` streams shall be processed. Chris@17: * `Manager` as singleton pattern being called by `PharStreamWrapper` instances Chris@17: in order to retrieve individual behavior and settings. Chris@17: * `Behavior` holds reference to interceptor(s) that shall assert correct/allowed Chris@17: invocation of a given `$path` for a given `$command`. Interceptors implement Chris@17: the interface `Assertable`. Interceptors can act individually on following Chris@17: commands or handle all of them in case not defined specifically: Chris@17: + `COMMAND_DIR_OPENDIR` Chris@17: + `COMMAND_MKDIR` Chris@17: + `COMMAND_RENAME` Chris@17: + `COMMAND_RMDIR` Chris@17: + `COMMAND_STEAM_METADATA` Chris@17: + `COMMAND_STREAM_OPEN` Chris@17: + `COMMAND_UNLINK` Chris@17: + `COMMAND_URL_STAT` Chris@17: Chris@18: ## Interceptors Chris@17: Chris@17: The following interceptor is shipped with the package and ready to use in order Chris@17: to block any Phar invocation of files not having a `.phar` suffix. Besides that Chris@17: individual interceptors are possible of course. Chris@17: Chris@17: ``` Chris@17: class PharExtensionInterceptor implements Assertable Chris@17: { Chris@17: /** Chris@17: * Determines whether the base file name has a ".phar" suffix. Chris@17: * Chris@17: * @param string $path Chris@17: * @param string $command Chris@17: * @return bool Chris@17: * @throws Exception Chris@17: */ Chris@17: public function assert($path, $command) Chris@17: { Chris@17: if ($this->baseFileContainsPharExtension($path)) { Chris@17: return true; Chris@17: } Chris@17: throw new Exception( Chris@17: sprintf( Chris@17: 'Unexpected file extension in "%s"', Chris@17: $path Chris@17: ), Chris@17: 1535198703 Chris@17: ); Chris@17: } Chris@17: Chris@17: /** Chris@17: * @param string $path Chris@17: * @return bool Chris@17: */ Chris@17: private function baseFileContainsPharExtension($path) Chris@17: { Chris@17: $baseFile = Helper::determineBaseFile($path); Chris@17: if ($baseFile === null) { Chris@17: return false; Chris@17: } Chris@17: $fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION); Chris@17: return strtolower($fileExtension) === 'phar'; Chris@17: } Chris@17: } Chris@17: ``` Chris@17: Chris@18: ### ConjunctionInterceptor Chris@18: Chris@18: This interceptor combines multiple interceptors implementing `Assertable`. Chris@18: It succeeds when all nested interceptors succeed as well (logical `AND`). Chris@18: Chris@18: ``` Chris@18: $behavior = new \TYPO3\PharStreamWrapper\Behavior(); Chris@18: \TYPO3\PharStreamWrapper\Manager::initialize( Chris@18: $behavior->withAssertion(new ConjunctionInterceptor(array( Chris@18: new PharExtensionInterceptor(), Chris@18: new PharMetaDataInterceptor() Chris@18: ))) Chris@18: ); Chris@18: ``` Chris@18: Chris@18: ### PharExtensionInterceptor Chris@18: Chris@18: This (basic) interceptor just checks whether the invoked Phar archive has Chris@18: an according `.phar` file extension. Resolving symbolic links as well as Chris@18: Phar internal alias resolving are considered as well. Chris@18: Chris@18: ``` Chris@18: $behavior = new \TYPO3\PharStreamWrapper\Behavior(); Chris@18: \TYPO3\PharStreamWrapper\Manager::initialize( Chris@18: $behavior->withAssertion(new PharExtensionInterceptor()) Chris@18: ); Chris@18: ``` Chris@18: Chris@18: ### PharMetaDataInterceptor Chris@18: Chris@18: This interceptor is actually checking serialized Phar meta-data against Chris@18: PHP objects and would consider a Phar archive malicious in case not only Chris@18: scalar values are found. A custom low-level `Phar\Reader` is used in order to Chris@18: avoid using PHP's `Phar` object which would trigger the initial vulnerability. Chris@18: Chris@18: ``` Chris@18: $behavior = new \TYPO3\PharStreamWrapper\Behavior(); Chris@18: \TYPO3\PharStreamWrapper\Manager::initialize( Chris@18: $behavior->withAssertion(new PharMetaDataInterceptor()) Chris@18: ); Chris@18: ``` Chris@18: Chris@18: ## Reader Chris@18: Chris@18: * `Phar\Reader::__construct(string $fileName)`: Creates low-level reader for Phar archive Chris@18: * `Phar\Reader::resolveContainer(): Phar\Container`: Resolves model representing Phar archive Chris@18: * `Phar\Container::getStub(): Phar\Stub`: Resolves (plain PHP) stub section of Phar archive Chris@18: * `Phar\Container::getManifest(): Phar\Manifest`: Resolves parsed Phar archive manifest as Chris@18: documented at http://php.net/manual/en/phar.fileformat.manifestfile.php Chris@18: * `Phar\Stub::getMappedAlias(): string`: Resolves internal Phar archive alias defined in stub Chris@18: using `Phar::mapPhar('alias.phar')` - actually the plain PHP source is analyzed here Chris@18: * `Phar\Manifest::getAlias(): string` - Resolves internal Phar archive alias defined in manifest Chris@18: using `Phar::setAlias('alias.phar')` Chris@18: * `Phar\Manifest::getMetaData(): string`: Resolves serialized Phar archive meta-data Chris@18: * `Phar\Manifest::deserializeMetaData(): mixed`: Resolves deserialized Phar archive meta-data Chris@18: containing only scalar values - in case an object is determined, an according Chris@18: `Phar\DeserializationException` will be thrown Chris@18: Chris@18: ``` Chris@18: $reader = new Phar\Reader('example.phar'); Chris@18: var_dump($reader->resolveContainer()->getManifest()->deserializeMetaData()); Chris@18: ``` Chris@18: Chris@17: ## Helper Chris@17: Chris@18: * `Helper::determineBaseFile(string $path): string`: Determines base file that can be Chris@17: accessed using the regular file system. For instance the following path Chris@17: `phar:///home/user/bundle.phar/content.txt` would be resolved to Chris@17: `/home/user/bundle.phar`. Chris@17: * `Helper::resetOpCache()`: Resets PHP's OPcache if enabled as work-around for Chris@17: issues in `include()` or `require()` calls and OPcache delivering wrong Chris@17: results. More details can be found in PHP's bug tracker, for instance like Chris@17: https://bugs.php.net/bug.php?id=66569 Chris@17: Chris@17: ## Security Contact Chris@17: Chris@17: In case of finding additional security issues in the TYPO3 project or in this Chris@17: `PharStreamWrapper` package in particular, please get in touch with the Chris@17: [TYPO3 Security Team](mailto:security@typo3.org).