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