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