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