1[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/TYPO3/phar-stream-wrapper/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/TYPO3/phar-stream-wrapper/?branch=master) 2[![GitHub Build Status Ubuntu](https://github.com/typo3/phar-stream-wrapper/actions/workflows/tests-ubuntu.yml/badge.svg)](https://github.com/typo3/phar-stream-wrapper/actions/workflows/tests-ubuntu.yml) 3[![GitHub Build Status Windows](https://github.com/typo3/phar-stream-wrapper/actions/workflows/tests-windows.yml/badge.svg)](https://github.com/typo3/phar-stream-wrapper/actions/workflows/tests-windows.yml) 4[![Downloads](https://poser.pugx.org/typo3/phar-stream-wrapper/downloads.svg)](https://packagist.org/packages/typo3/phar-stream-wrapper) 5 6# PHP Phar Stream Wrapper 7 8## Abstract & History 9 10Based on Sam Thomas' findings concerning 11[insecure deserialization in combination with obfuscation strategies](https://www.secarma.com/labs/near-phar-dangerous-unserialization-wherever-you-are.html) 12allowing to hide Phar files inside valid image resources, the TYPO3 project 13decided back then to introduce a `PharStreamWrapper` to intercept invocations 14of the `phar://` stream in PHP and only allow usage for defined locations in 15the file system. 16 17Since the TYPO3 mission statement is **inspiring people to share**, we thought 18it would be helpful for others to release our `PharStreamWrapper` as standalone 19package to the PHP community. 20 21The mentioned security issue was reported to TYPO3 on 10th June 2018 by Sam Thomas 22and has been addressed concerning the specific attack vector and for this generic 23`PharStreamWrapper` in TYPO3 versions 7.6.30 LTS, 8.7.17 LTS and 9.3.1 on 12th 24July 2018. 25 26* https://blog.secarma.co.uk/labs/near-phar-dangerous-unserialization-wherever-you-are 27* https://youtu.be/GePBmsNJw6Y 28* https://typo3.org/security/advisory/typo3-psa-2018-001/ 29* https://typo3.org/security/advisory/typo3-psa-2019-007/ 30* https://typo3.org/security/advisory/typo3-psa-2019-008/ 31 32## License 33 34In general the TYPO3 core is released under the GNU General Public License version 352 or any later version (`GPL-2.0-or-later`). In order to avoid licensing issues and 36incompatibilities this `PharStreamWrapper` is licenced under the MIT License. In case 37you duplicate or modify source code, credits are not required but really appreciated. 38 39## Credits 40 41Thanks to [Alex Pott](https://github.com/alexpott), Drupal for creating 42back-ports of all sources in order to provide compatibility with PHP v5.3. 43 44## Installation 45 46The `PharStreamWrapper` is provided as composer package `typo3/phar-stream-wrapper` 47and 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). 48 49### Installation for PHP v7.0 50 51``` 52composer require typo3/phar-stream-wrapper ^3.0 53``` 54 55### Installation for PHP v5.3 56 57``` 58composer require typo3/phar-stream-wrapper ^2.0 59``` 60 61## Example 62 63The following example is bundled within this package, the shown 64`PharExtensionInterceptor` denies all stream wrapper invocations files 65not having the `.phar` suffix. Interceptor logic has to be individual and 66adjusted to according requirements. 67 68``` 69\TYPO3\PharStreamWrapper\Manager::initialize( 70 (new \TYPO3\PharStreamWrapper\Behavior()) 71 ->withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor()) 72); 73 74if (in_array('phar', stream_get_wrappers())) { 75 stream_wrapper_unregister('phar'); 76 stream_wrapper_register('phar', \TYPO3\PharStreamWrapper\PharStreamWrapper::class); 77} 78``` 79 80* `PharStreamWrapper` defined as class reference will be instantiated each time 81 `phar://` streams shall be processed. 82* `Manager` as singleton pattern being called by `PharStreamWrapper` instances 83 in order to retrieve individual behavior and settings. 84* `Behavior` holds reference to interceptor(s) that shall assert correct/allowed 85 invocation of a given `$path` for a given `$command`. Interceptors implement 86 the interface `Assertable`. Interceptors can act individually on following 87 commands or handle all of them in case not defined specifically: 88 + `COMMAND_DIR_OPENDIR` 89 + `COMMAND_MKDIR` 90 + `COMMAND_RENAME` 91 + `COMMAND_RMDIR` 92 + `COMMAND_STEAM_METADATA` 93 + `COMMAND_STREAM_OPEN` 94 + `COMMAND_UNLINK` 95 + `COMMAND_URL_STAT` 96 97## Interceptors 98 99The following interceptor is shipped with the package and ready to use in order 100to block any Phar invocation of files not having a `.phar` suffix. Besides that 101individual interceptors are possible of course. 102 103``` 104class PharExtensionInterceptor implements Assertable 105{ 106 /** 107 * Determines whether the base file name has a ".phar" suffix. 108 * 109 * @param string $path 110 * @param string $command 111 * @return bool 112 * @throws Exception 113 */ 114 public function assert(string $path, string $command): bool 115 { 116 if ($this->baseFileContainsPharExtension($path)) { 117 return true; 118 } 119 throw new Exception( 120 sprintf( 121 'Unexpected file extension in "%s"', 122 $path 123 ), 124 1535198703 125 ); 126 } 127 128 /** 129 * @param string $path 130 * @return bool 131 */ 132 private function baseFileContainsPharExtension(string $path): bool 133 { 134 $baseFile = Helper::determineBaseFile($path); 135 if ($baseFile === null) { 136 return false; 137 } 138 $fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION); 139 return strtolower($fileExtension) === 'phar'; 140 } 141} 142``` 143 144### ConjunctionInterceptor 145 146This interceptor combines multiple interceptors implementing `Assertable`. 147It succeeds when all nested interceptors succeed as well (logical `AND`). 148 149``` 150\TYPO3\PharStreamWrapper\Manager::initialize( 151 (new \TYPO3\PharStreamWrapper\Behavior()) 152 ->withAssertion(new ConjunctionInterceptor([ 153 new PharExtensionInterceptor(), 154 new PharMetaDataInterceptor(), 155 ])) 156); 157``` 158 159### PharExtensionInterceptor 160 161This (basic) interceptor just checks whether the invoked Phar archive has 162an according `.phar` file extension. Resolving symbolic links as well as 163Phar internal alias resolving are considered as well. 164 165``` 166\TYPO3\PharStreamWrapper\Manager::initialize( 167 (new \TYPO3\PharStreamWrapper\Behavior()) 168 ->withAssertion(new PharExtensionInterceptor()) 169); 170``` 171 172### PharMetaDataInterceptor 173 174This interceptor is actually checking serialized Phar meta-data against 175PHP objects and would consider a Phar archive malicious in case not only 176scalar values are found. A custom low-level `Phar\Reader` is used in order to 177avoid using PHP's `Phar` object which would trigger the initial vulnerability. 178 179``` 180\TYPO3\PharStreamWrapper\Manager::initialize( 181 (new \TYPO3\PharStreamWrapper\Behavior()) 182 ->withAssertion(new PharMetaDataInterceptor()) 183); 184``` 185 186## Reader 187 188* `Phar\Reader::__construct(string $fileName)`: Creates low-level reader for Phar archive 189* `Phar\Reader::resolveContainer(): Phar\Container`: Resolves model representing Phar archive 190* `Phar\Container::getStub(): Phar\Stub`: Resolves (plain PHP) stub section of Phar archive 191* `Phar\Container::getManifest(): Phar\Manifest`: Resolves parsed Phar archive manifest as 192 documented at http://php.net/manual/en/phar.fileformat.manifestfile.php 193* `Phar\Stub::getMappedAlias(): string`: Resolves internal Phar archive alias defined in stub 194 using `Phar::mapPhar('alias.phar')` - actually the plain PHP source is analyzed here 195* `Phar\Manifest::getAlias(): string` - Resolves internal Phar archive alias defined in manifest 196 using `Phar::setAlias('alias.phar')` 197* `Phar\Manifest::getMetaData(): string`: Resolves serialized Phar archive meta-data 198* `Phar\Manifest::deserializeMetaData(): mixed`: Resolves deserialized Phar archive meta-data 199 containing only scalar values - in case an object is determined, an according 200 `Phar\DeserializationException` will be thrown 201 202``` 203$reader = new Phar\Reader('example.phar'); 204var_dump($reader->resolveContainer()->getManifest()->deserializeMetaData()); 205``` 206 207## Helper 208 209* `Helper::determineBaseFile(string $path): string`: Determines base file that can be 210 accessed using the regular file system. For instance the following path 211 `phar:///home/user/bundle.phar/content.txt` would be resolved to 212 `/home/user/bundle.phar`. 213* `Helper::resetOpCache()`: Resets PHP's OPcache if enabled as work-around for 214 issues in `include()` or `require()` calls and OPcache delivering wrong 215 results. More details can be found in PHP's bug tracker, for instance like 216 https://bugs.php.net/bug.php?id=66569 217 218## Security Contact 219 220In case of finding additional security issues in the TYPO3 project or in this 221`PharStreamWrapper` package in particular, please get in touch with the 222[TYPO3 Security Team](mailto:security@typo3.org). 223