1 |
cd5c298a
|
Geoffroy Desvernay
|
[![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 |
6b24a280
|
Assos Assos
|
[![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 |
cd5c298a
|
Geoffroy Desvernay
|
|
5 |
|
|
# PHP Phar Stream Wrapper
|
6 |
|
|
|
7 |
|
|
## Abstract & History
|
8 |
|
|
|
9 |
|
|
Based 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)
|
11 |
|
|
allowing to hide Phar files inside valid image resources, the TYPO3 project
|
12 |
|
|
decided back then to introduce a `PharStreamWrapper` to intercept invocations
|
13 |
|
|
of the `phar://` stream in PHP and only allow usage for defined locations in
|
14 |
|
|
the file system.
|
15 |
|
|
|
16 |
|
|
Since the TYPO3 mission statement is **inspiring people to share**, we thought
|
17 |
|
|
it would be helpful for others to release our `PharStreamWrapper` as standalone
|
18 |
|
|
package to the PHP community.
|
19 |
|
|
|
20 |
|
|
The mentioned security issue was reported to TYPO3 on 10th June 2018 by Sam Thomas
|
21 |
|
|
and 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
|
23 |
|
|
July 2018.
|
24 |
|
|
|
25 |
|
|
* https://blog.secarma.co.uk/labs/near-phar-dangerous-unserialization-wherever-you-are
|
26 |
|
|
* https://youtu.be/GePBmsNJw6Y
|
27 |
6b24a280
|
Assos Assos
|
* 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 |
cd5c298a
|
Geoffroy Desvernay
|
|
31 |
|
|
## License
|
32 |
|
|
|
33 |
|
|
In general the TYPO3 core is released under the GNU General Public License version
|
34 |
|
|
2 or any later version (`GPL-2.0-or-later`). In order to avoid licensing issues and
|
35 |
|
|
incompatibilities this `PharStreamWrapper` is licenced under the MIT License. In case
|
36 |
|
|
you duplicate or modify source code, credits are not required but really appreciated.
|
37 |
|
|
|
38 |
|
|
## Credits
|
39 |
|
|
|
40 |
|
|
Thanks to [Alex Pott](https://github.com/alexpott), Drupal for creating
|
41 |
|
|
back-ports of all sources in order to provide compatibility with PHP v5.3.
|
42 |
|
|
|
43 |
|
|
## Installation
|
44 |
|
|
|
45 |
|
|
The `PharStreamWrapper` is provided as composer package `typo3/phar-stream-wrapper`
|
46 |
|
|
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).
|
47 |
|
|
|
48 |
|
|
### Installation for PHP v7.0
|
49 |
|
|
|
50 |
|
|
```
|
51 |
|
|
composer require typo3/phar-stream-wrapper ^3.0
|
52 |
|
|
```
|
53 |
|
|
|
54 |
|
|
### Installation for PHP v5.3
|
55 |
|
|
|
56 |
|
|
```
|
57 |
|
|
composer require typo3/phar-stream-wrapper ^2.0
|
58 |
|
|
```
|
59 |
|
|
|
60 |
|
|
## Example
|
61 |
|
|
|
62 |
|
|
The following example is bundled within this package, the shown
|
63 |
|
|
`PharExtensionInterceptor` denies all stream wrapper invocations files
|
64 |
|
|
not having the `.phar` suffix. Interceptor logic has to be individual and
|
65 |
|
|
adjusted to according requirements.
|
66 |
|
|
|
67 |
|
|
```
|
68 |
|
|
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
|
69 |
fbb66ca6
|
Assos Assos
|
\TYPO3\PharStreamWrapper\Manager::initialize(
|
70 |
cd5c298a
|
Geoffroy Desvernay
|
$behavior->withAssertion(new PharExtensionInterceptor())
|
71 |
|
|
);
|
72 |
|
|
|
73 |
|
|
if (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 |
fbb66ca6
|
Assos Assos
|
## Interceptors
|
97 |
cd5c298a
|
Geoffroy Desvernay
|
|
98 |
|
|
The following interceptor is shipped with the package and ready to use in order
|
99 |
|
|
to block any Phar invocation of files not having a `.phar` suffix. Besides that
|
100 |
|
|
individual interceptors are possible of course.
|
101 |
|
|
|
102 |
|
|
```
|
103 |
|
|
class 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 |
fbb66ca6
|
Assos Assos
|
### ConjunctionInterceptor
|
144 |
|
|
|
145 |
|
|
This interceptor combines multiple interceptors implementing `Assertable`.
|
146 |
|
|
It 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 |
|
|
|
160 |
|
|
This (basic) interceptor just checks whether the invoked Phar archive has
|
161 |
|
|
an according `.phar` file extension. Resolving symbolic links as well as
|
162 |
|
|
Phar 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 |
|
|
|
173 |
|
|
This interceptor is actually checking serialized Phar meta-data against
|
174 |
|
|
PHP objects and would consider a Phar archive malicious in case not only
|
175 |
|
|
scalar values are found. A custom low-level `Phar\Reader` is used in order to
|
176 |
|
|
avoid 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');
|
203 |
|
|
var_dump($reader->resolveContainer()->getManifest()->deserializeMetaData());
|
204 |
|
|
```
|
205 |
|
|
|
206 |
cd5c298a
|
Geoffroy Desvernay
|
## Helper
|
207 |
|
|
|
208 |
fbb66ca6
|
Assos Assos
|
* `Helper::determineBaseFile(string $path): string`: Determines base file that can be
|
209 |
cd5c298a
|
Geoffroy Desvernay
|
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 |
|
|
|
219 |
|
|
In 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). |